Remove old library crate
- Replace crate `owm_api25` with new crate
This commit is contained in:
parent
e8a2707987
commit
a5a7e6a5db
@ -1,11 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "owm_api25"
|
|
||||||
version = "0.0.1"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
toml = "0.9.6"
|
|
||||||
dirs = "6.0.0"
|
|
||||||
serde = { version = "1.0.225", features = ["derive"] }
|
|
||||||
serde_json = "1.0.145"
|
|
||||||
chrono = { version = "0.4.42", features = ["serde"] }
|
|
@ -1,796 +0,0 @@
|
|||||||
//! # Current Weather Data
|
|
||||||
//!
|
|
||||||
//! Data structures and utilities for parsing and working with responses
|
|
||||||
//! from the **`OpenWeatherMap` Current Weather API**.
|
|
||||||
//!
|
|
||||||
//! See: <https://openweathermap.org/current>
|
|
||||||
//!
|
|
||||||
//! ## Features
|
|
||||||
//!
|
|
||||||
//! - Complete type-safe representation of all API response fields
|
|
||||||
//! - Optional fields for resilience against API changes
|
|
||||||
//! - Convenient accessor methods
|
|
||||||
//! - Chrono integration for date/time handling
|
|
||||||
//! - Comprehensive error handling
|
|
||||||
//!
|
|
||||||
//! ## Example
|
|
||||||
//!
|
|
||||||
//! ```no_run
|
|
||||||
//! use owm_api25::current::WeatherResponse;
|
|
||||||
//!
|
|
||||||
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
//! let json = r#"
|
|
||||||
//! {
|
|
||||||
//! "coord": {"lon": 7.367, "lat": 45.133},
|
|
||||||
//! "weather": [{
|
|
||||||
//! "id": 501,
|
|
||||||
//! "main": "Rain",
|
|
||||||
//! "description": "moderate rain",
|
|
||||||
//! "icon": "10d"
|
|
||||||
//! }],
|
|
||||||
//! "main": {
|
|
||||||
//! "temp": 284.2,
|
|
||||||
//! "feels_like": 282.93,
|
|
||||||
//! "pressure": 1021,
|
|
||||||
//! "humidity": 60
|
|
||||||
//! },
|
|
||||||
//! "name": "Turin",
|
|
||||||
//! "cod": 200
|
|
||||||
//! }"#;
|
|
||||||
//!
|
|
||||||
//! let data: WeatherResponse = serde_json::from_str(json)?;
|
|
||||||
//! assert!(data.is_success());
|
|
||||||
//! println!("{} → {}", data.city().unwrap_or("Unknown"), data.summary());
|
|
||||||
//! # Ok(())
|
|
||||||
//! # }
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! ## Error Handling
|
|
||||||
//!
|
|
||||||
//! The API may return error responses with `cod` values other than 200.
|
|
||||||
//! Always check [`WeatherResponse::is_success()`] before using the data.
|
|
||||||
//!
|
|
||||||
//! ```no_run
|
|
||||||
//! use owm_api25::current::WeatherResponse;
|
|
||||||
//!
|
|
||||||
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
//! let error_response = r#"{"cod": 404, "message": "city not found"}"#;
|
|
||||||
//! let data: WeatherResponse = serde_json::from_str(error_response)?;
|
|
||||||
//!
|
|
||||||
//! if !data.is_success() {
|
|
||||||
//! eprintln!("API error: {}", data.error_message().unwrap_or("Unknown error"));
|
|
||||||
//! }
|
|
||||||
//! # Ok(())
|
|
||||||
//! # }
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! ## See Also
|
|
||||||
//! - [`forecast`](crate::forecast) for multi-day weather data
|
|
||||||
//! - [`query`](crate::query) for building request URLs
|
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// Geographic coordinates (longitude and latitude)
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// use owm_api25::current::Coord;
|
|
||||||
///
|
|
||||||
/// let coord = Coord { lon: -0.1257, lat: 51.5085 };
|
|
||||||
/// assert_eq!(coord.lon, -0.1257);
|
|
||||||
/// assert_eq!(coord.lat, 51.5085);
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
|
|
||||||
pub struct Coord {
|
|
||||||
/// Longitude of the location (-180 to 180)
|
|
||||||
pub lon: f64,
|
|
||||||
/// Latitude of the location (-90 to 90)
|
|
||||||
pub lat: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Coord {
|
|
||||||
/// Creates new coordinates from longitude and latitude
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// use owm_api25::current::Coord;
|
|
||||||
///
|
|
||||||
/// let london = Coord::new(-0.1257, 51.5085);
|
|
||||||
/// ```
|
|
||||||
#[must_use]
|
|
||||||
pub fn new(lon: f64, lat: f64) -> Self {
|
|
||||||
Self { lon, lat }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Weather condition information
|
|
||||||
///
|
|
||||||
/// Contains details about current weather conditions including
|
|
||||||
/// human-readable descriptions and icon identifiers.
|
|
||||||
///
|
|
||||||
/// See: [Weather condition codes](https://openweathermap.org/weather-conditions)
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
|
|
||||||
pub struct Weather {
|
|
||||||
/// Weather condition ID
|
|
||||||
///
|
|
||||||
/// See: [Weather condition codes](https://openweathermap.org/weather-conditions)
|
|
||||||
pub id: u32,
|
|
||||||
/// Group of weather parameters (e.g. Rain, Snow, Clouds)
|
|
||||||
pub main: String,
|
|
||||||
/// Human-readable weather condition description
|
|
||||||
///
|
|
||||||
/// This field can be localized based on the API request.
|
|
||||||
pub description: String,
|
|
||||||
/// Weather icon ID (for retrieving icon assets)
|
|
||||||
///
|
|
||||||
/// Combine with base URL: `https://openweathermap.org/img/wn/{icon}@2x.png`
|
|
||||||
pub icon: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Weather {
|
|
||||||
/// Returns the URL to the weather icon image
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// use owm_api25::current::Weather;
|
|
||||||
///
|
|
||||||
/// let weather = Weather {
|
|
||||||
/// id: 501,
|
|
||||||
/// main: "Rain".to_string(),
|
|
||||||
/// description: "moderate rain".to_string(),
|
|
||||||
/// icon: "10d".to_string(),
|
|
||||||
/// };
|
|
||||||
///
|
|
||||||
/// assert_eq!(
|
|
||||||
/// weather.icon_url(),
|
|
||||||
/// "https://openweathermap.org/img/wn/10d@2x.png"
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
#[must_use]
|
|
||||||
pub fn icon_url(&self) -> String {
|
|
||||||
format!("https://openweathermap.org/img/wn/{}@2x.png", self.icon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Main weather parameters including temperature, pressure, and humidity
|
|
||||||
///
|
|
||||||
/// All temperature values are in the units specified in the API request
|
|
||||||
/// (Kelvin, Celsius, or Fahrenheit).
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
|
|
||||||
pub struct Main {
|
|
||||||
/// Temperature in requested units (Kelvin, Celsius, or Fahrenheit)
|
|
||||||
#[serde(default)]
|
|
||||||
pub temp: Option<f32>,
|
|
||||||
/// "Feels like" temperature, accounting for human perception of weather
|
|
||||||
#[serde(default)]
|
|
||||||
pub feels_like: Option<f32>,
|
|
||||||
/// Minimum observed temperature (within large urban areas)
|
|
||||||
#[serde(default)]
|
|
||||||
pub temp_min: Option<f32>,
|
|
||||||
/// Maximum observed temperature (within large urban areas)
|
|
||||||
#[serde(default)]
|
|
||||||
pub temp_max: Option<f32>,
|
|
||||||
/// Atmospheric pressure at sea level (hPa)
|
|
||||||
#[serde(default)]
|
|
||||||
pub pressure: Option<u32>,
|
|
||||||
/// Humidity percentage (0-100)
|
|
||||||
#[serde(default)]
|
|
||||||
pub humidity: Option<u8>,
|
|
||||||
/// Atmospheric pressure at sea level (hPa)
|
|
||||||
#[serde(default)]
|
|
||||||
pub sea_level: Option<u32>,
|
|
||||||
/// Atmospheric pressure at ground level (hPa)
|
|
||||||
#[serde(default)]
|
|
||||||
pub grnd_level: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Main {
|
|
||||||
/// Returns the temperature difference between max and min
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// use owm_api25::current::Main;
|
|
||||||
///
|
|
||||||
/// let main = Main {
|
|
||||||
/// temp: Some(20.0),
|
|
||||||
/// temp_min: Some(15.0),
|
|
||||||
/// temp_max: Some(25.0),
|
|
||||||
/// ..Default::default()
|
|
||||||
/// };
|
|
||||||
///
|
|
||||||
/// assert_eq!(main.temp_range(), Some(10.0));
|
|
||||||
/// ```
|
|
||||||
#[must_use]
|
|
||||||
pub fn temp_range(&self) -> Option<f32> {
|
|
||||||
match (self.temp_min, self.temp_max) {
|
|
||||||
(Some(min), Some(max)) => Some(max - min),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wind information including speed, direction, and gusts
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
|
|
||||||
pub struct Wind {
|
|
||||||
/// Wind speed in chosen units (m/s by default)
|
|
||||||
#[serde(default)]
|
|
||||||
pub speed: Option<f32>,
|
|
||||||
/// Wind direction in degrees (meteorological, 0-360)
|
|
||||||
///
|
|
||||||
/// - 0°: North
|
|
||||||
/// - 90°: East
|
|
||||||
/// - 180°: South
|
|
||||||
/// - 270°: West
|
|
||||||
#[serde(default)]
|
|
||||||
pub deg: Option<u16>,
|
|
||||||
/// Wind gust speed in chosen units
|
|
||||||
#[serde(default)]
|
|
||||||
pub gust: Option<f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Wind {
|
|
||||||
/// Returns wind direction as a cardinal direction (N, NE, E, etc.)
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// use owm_api25::current::Wind;
|
|
||||||
///
|
|
||||||
/// let north_wind = Wind { deg: Some(0), ..Default::default() };
|
|
||||||
/// assert_eq!(north_wind.direction(), Some("N"));
|
|
||||||
///
|
|
||||||
/// let east_wind = Wind { deg: Some(90), ..Default::default() };
|
|
||||||
/// assert_eq!(east_wind.direction(), Some("E"));
|
|
||||||
/// ```
|
|
||||||
#[must_use]
|
|
||||||
pub fn direction(&self) -> Option<&'static str> {
|
|
||||||
let deg = self.deg?;
|
|
||||||
let directions = [
|
|
||||||
"N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW",
|
|
||||||
"NW", "NNW",
|
|
||||||
];
|
|
||||||
let index = (((u32::from(deg) * 16 + 11) / 22) % 16) as usize;
|
|
||||||
Some(directions[index])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Cloud cover information
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
|
|
||||||
pub struct Clouds {
|
|
||||||
/// Cloudiness percentage (0-100)
|
|
||||||
#[serde(default)]
|
|
||||||
pub all: Option<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Clouds {
|
|
||||||
/// Returns true if cloud cover indicates clear skies (<= 20%)
|
|
||||||
#[must_use]
|
|
||||||
pub fn is_clear(&self) -> bool {
|
|
||||||
self.all.is_some_and(|cover| cover <= 20)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if cloud cover indicates overcast (>= 80%)
|
|
||||||
#[must_use]
|
|
||||||
pub fn is_overcast(&self) -> bool {
|
|
||||||
self.all.is_some_and(|cover| cover >= 80)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Precipitation information for rain or snow
|
|
||||||
///
|
|
||||||
/// Contains precipitation volumes over different time periods.
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
|
|
||||||
pub struct Precipitation {
|
|
||||||
/// Precipitation volume over the last 1 hour (mm)
|
|
||||||
#[serde(rename = "1h", default)]
|
|
||||||
pub one_hour: Option<f32>,
|
|
||||||
/// Precipitation volume over the last 3 hours (mm)
|
|
||||||
#[serde(rename = "3h", default)]
|
|
||||||
pub three_hour: Option<f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Precipitation {
|
|
||||||
/// Returns the precipitation intensity category
|
|
||||||
///
|
|
||||||
/// - None: No precipitation data
|
|
||||||
/// - "Light": < 2.5 mm/h
|
|
||||||
/// - "Moderate": 2.5 - 7.5 mm/h
|
|
||||||
/// - "Heavy": > 7.5 mm/h
|
|
||||||
#[must_use]
|
|
||||||
pub fn intensity(&self) -> Option<&'static str> {
|
|
||||||
let rate = self.one_hour.or(self.three_hour.map(|v| v / 3.0))?;
|
|
||||||
|
|
||||||
if rate < 2.5 {
|
|
||||||
Some("Light")
|
|
||||||
} else if rate <= 7.5 {
|
|
||||||
Some("Moderate")
|
|
||||||
} else {
|
|
||||||
Some("Heavy")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// System and location information
|
|
||||||
///
|
|
||||||
/// Contains country, sunrise/sunset times, and internal API parameters.
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
|
|
||||||
pub struct Sys {
|
|
||||||
/// Internal parameter
|
|
||||||
#[serde(default)]
|
|
||||||
pub r#type: Option<u8>,
|
|
||||||
/// Internal parameter
|
|
||||||
#[serde(default)]
|
|
||||||
pub id: Option<u32>,
|
|
||||||
/// Country code (ISO 3166-1 alpha-2, e.g. "GB", "JP")
|
|
||||||
#[serde(default)]
|
|
||||||
pub country: Option<String>,
|
|
||||||
/// Sunrise time (UTC Unix timestamp)
|
|
||||||
#[serde(with = "chrono::serde::ts_seconds_option")]
|
|
||||||
pub sunrise: Option<DateTime<Utc>>,
|
|
||||||
/// Sunset time (UTC Unix timestamp)
|
|
||||||
#[serde(with = "chrono::serde::ts_seconds_option")]
|
|
||||||
pub sunset: Option<DateTime<Utc>>,
|
|
||||||
/// Internal parameter (often used for error messages)
|
|
||||||
#[serde(default)]
|
|
||||||
pub message: Option<f64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sys {
|
|
||||||
/// Returns true if it's currently daytime at the location
|
|
||||||
///
|
|
||||||
/// Compares current UTC time with sunrise and sunset times.
|
|
||||||
#[must_use]
|
|
||||||
pub fn is_daytime(&self) -> Option<bool> {
|
|
||||||
let now = Utc::now();
|
|
||||||
match (self.sunrise, self.sunset) {
|
|
||||||
(Some(sunrise), Some(sunset)) => Some(now >= sunrise && now <= sunset),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the duration of daylight (sunset - sunrise)
|
|
||||||
///
|
|
||||||
/// Returns `None` if either sunrise or sunset data is missing.
|
|
||||||
#[must_use]
|
|
||||||
pub fn daylight_duration(&self) -> Option<chrono::Duration> {
|
|
||||||
Some(self.sunset? - self.sunrise?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Full response structure for the **`OpenWeatherMap` Current Weather API**
|
|
||||||
///
|
|
||||||
/// Every field is optional to ensure resilience against missing or
|
|
||||||
/// undocumented fields in the live API responses.
|
|
||||||
///
|
|
||||||
/// ## Success vs Error Responses
|
|
||||||
///
|
|
||||||
/// - **Success**: `cod == 200`, weather data is populated
|
|
||||||
/// - **Error**: `cod != 200`, `message` contains error description
|
|
||||||
///
|
|
||||||
/// ## Example
|
|
||||||
///
|
|
||||||
/// ### Success Response
|
|
||||||
/// ```
|
|
||||||
/// use owm_api25::current::WeatherResponse;
|
|
||||||
///
|
|
||||||
/// let json = r#"{
|
|
||||||
/// "coord": {"lon": -0.1257, "lat": 51.5085},
|
|
||||||
/// "weather": [{
|
|
||||||
/// "id": 300,
|
|
||||||
/// "main": "Drizzle",
|
|
||||||
/// "description": "light intensity drizzle",
|
|
||||||
/// "icon": "09d"
|
|
||||||
/// }],
|
|
||||||
/// "main": {
|
|
||||||
/// "temp": 280.32,
|
|
||||||
/// "pressure": 1012,
|
|
||||||
/// "humidity": 81
|
|
||||||
/// },
|
|
||||||
/// "name": "London",
|
|
||||||
/// "cod": 200
|
|
||||||
/// }"#;
|
|
||||||
///
|
|
||||||
/// let response: WeatherResponse = serde_json::from_str(json).unwrap();
|
|
||||||
/// assert!(response.is_success());
|
|
||||||
/// assert_eq!(response.city(), Some("London"));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ### Error Response
|
|
||||||
/// ```
|
|
||||||
/// use owm_api25::current::WeatherResponse;
|
|
||||||
///
|
|
||||||
/// let json = r#"{
|
|
||||||
/// "cod": 404,
|
|
||||||
/// "message": "city not found"
|
|
||||||
/// }"#;
|
|
||||||
///
|
|
||||||
/// let response: WeatherResponse = serde_json::from_str(json).unwrap();
|
|
||||||
/// assert!(!response.is_success());
|
|
||||||
/// assert_eq!(response.error_message(), Some("city not found"));
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
|
|
||||||
pub struct WeatherResponse {
|
|
||||||
/// Geographic coordinates
|
|
||||||
#[serde(default)]
|
|
||||||
pub coord: Option<Coord>,
|
|
||||||
/// Weather conditions array (usually contains one element)
|
|
||||||
#[serde(default)]
|
|
||||||
pub weather: Vec<Weather>,
|
|
||||||
/// Internal parameter
|
|
||||||
#[serde(default)]
|
|
||||||
pub base: Option<String>,
|
|
||||||
/// Main weather parameters
|
|
||||||
#[serde(default)]
|
|
||||||
pub main: Option<Main>,
|
|
||||||
/// Visibility in meters (maximum value is 10,000 m)
|
|
||||||
#[serde(default)]
|
|
||||||
pub visibility: Option<u32>,
|
|
||||||
/// Wind information
|
|
||||||
#[serde(default)]
|
|
||||||
pub wind: Option<Wind>,
|
|
||||||
/// Cloud cover information
|
|
||||||
#[serde(default)]
|
|
||||||
pub clouds: Option<Clouds>,
|
|
||||||
/// Rain precipitation data
|
|
||||||
#[serde(default)]
|
|
||||||
pub rain: Option<Precipitation>,
|
|
||||||
/// Snow precipitation data
|
|
||||||
#[serde(default)]
|
|
||||||
pub snow: Option<Precipitation>,
|
|
||||||
/// Time of data calculation (UTC Unix timestamp)
|
|
||||||
#[serde(with = "chrono::serde::ts_seconds_option")]
|
|
||||||
pub dt: Option<DateTime<Utc>>,
|
|
||||||
/// System information (country, sunrise, sunset, etc.)
|
|
||||||
#[serde(default)]
|
|
||||||
pub sys: Option<Sys>,
|
|
||||||
/// Shift in seconds from UTC
|
|
||||||
#[serde(default)]
|
|
||||||
pub timezone: Option<i32>,
|
|
||||||
/// City ID
|
|
||||||
#[serde(default)]
|
|
||||||
pub id: Option<u64>,
|
|
||||||
/// City name
|
|
||||||
#[serde(default)]
|
|
||||||
pub name: Option<String>,
|
|
||||||
/// Internal parameter (HTTP-like status code)
|
|
||||||
///
|
|
||||||
/// - `200`: Success
|
|
||||||
/// - `4xx`: Client errors (e.g., 404 "city not found")
|
|
||||||
/// - `5xx`: Server errors
|
|
||||||
#[serde(default)]
|
|
||||||
pub cod: Option<u16>,
|
|
||||||
/// Error message (present when API call fails)
|
|
||||||
#[serde(default)]
|
|
||||||
pub message: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WeatherResponse {
|
|
||||||
/// Returns the first (primary) weather condition, if present
|
|
||||||
#[must_use]
|
|
||||||
pub fn primary_weather(&self) -> Option<&Weather> {
|
|
||||||
self.weather.first()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks whether the API response indicates success (`cod == 200`)
|
|
||||||
///
|
|
||||||
/// Always check this before accessing weather data fields.
|
|
||||||
#[must_use]
|
|
||||||
pub fn is_success(&self) -> bool {
|
|
||||||
matches!(self.cod, Some(200))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the error message if the API call failed
|
|
||||||
///
|
|
||||||
/// Returns `None` for successful responses.
|
|
||||||
#[must_use]
|
|
||||||
pub fn error_message(&self) -> Option<&str> {
|
|
||||||
if self.is_success() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
self.message.as_deref()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the current temperature, if available
|
|
||||||
#[must_use]
|
|
||||||
pub fn temperature(&self) -> Option<f32> {
|
|
||||||
self.main.as_ref().and_then(|m| m.temp)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the "feels like" temperature, if available
|
|
||||||
#[must_use]
|
|
||||||
pub fn feels_like(&self) -> Option<f32> {
|
|
||||||
self.main.as_ref().and_then(|m| m.feels_like)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the city name
|
|
||||||
#[must_use]
|
|
||||||
pub fn city(&self) -> Option<&str> {
|
|
||||||
self.name.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the 2-letter country code (ISO 3166-1 alpha-2), if available
|
|
||||||
#[must_use]
|
|
||||||
pub fn country(&self) -> Option<&str> {
|
|
||||||
self.sys.as_ref()?.country.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the atmospheric pressure in hPa, if available
|
|
||||||
#[must_use]
|
|
||||||
pub fn pressure(&self) -> Option<u32> {
|
|
||||||
self.main.as_ref().and_then(|m| m.pressure)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the humidity percentage (0-100), if available
|
|
||||||
#[must_use]
|
|
||||||
pub fn humidity(&self) -> Option<u8> {
|
|
||||||
self.main.as_ref().and_then(|m| m.humidity)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the wind speed, if available
|
|
||||||
#[must_use]
|
|
||||||
pub fn wind_speed(&self) -> Option<f32> {
|
|
||||||
self.wind.as_ref().and_then(|w| w.speed)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the wind direction in degrees, if available
|
|
||||||
#[must_use]
|
|
||||||
pub fn wind_deg(&self) -> Option<u16> {
|
|
||||||
self.wind.as_ref().and_then(|w| w.deg)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the cloudiness percentage (0-100), if available
|
|
||||||
#[must_use]
|
|
||||||
pub fn cloudiness(&self) -> Option<u8> {
|
|
||||||
self.clouds.as_ref().and_then(|c| c.all)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns precipitation intensity category (Rain or Snow)
|
|
||||||
#[must_use]
|
|
||||||
pub fn precipitation_intensity(&self) -> Option<&'static str> {
|
|
||||||
self.rain
|
|
||||||
.as_ref()
|
|
||||||
.and_then(Precipitation::intensity)
|
|
||||||
.or_else(|| self.snow.as_ref().and_then(Precipitation::intensity))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the visibility in meters, if available
|
|
||||||
#[must_use]
|
|
||||||
pub fn visibility(&self) -> Option<u32> {
|
|
||||||
self.visibility
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Produces a compact, human-readable weather summary
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// use owm_api25::current::WeatherResponse;
|
|
||||||
///
|
|
||||||
/// let json = r#"{
|
|
||||||
/// "weather": [{"description": "clear sky"}],
|
|
||||||
/// "main": {"temp": 293.15},
|
|
||||||
/// "cod": 200
|
|
||||||
/// }"#;
|
|
||||||
///
|
|
||||||
/// let response: WeatherResponse = serde_json::from_str(json).unwrap();
|
|
||||||
/// assert_eq!(response.summary(), "clear sky, 293.1°");
|
|
||||||
/// ```
|
|
||||||
#[must_use]
|
|
||||||
pub fn summary(&self) -> String {
|
|
||||||
let temp = self
|
|
||||||
.temperature()
|
|
||||||
.map_or("-".into(), |t| format!("{t:.1}°"));
|
|
||||||
let desc = self
|
|
||||||
.primary_weather()
|
|
||||||
.map_or("N/A", |w| w.description.as_str());
|
|
||||||
format!("{desc}, {temp}")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a detailed multi-line weather report
|
|
||||||
///
|
|
||||||
/// Includes temperature, conditions, wind, pressure, and humidity.
|
|
||||||
#[must_use]
|
|
||||||
pub fn detailed_report(&self) -> String {
|
|
||||||
let city = self.city().unwrap_or("Unknown location");
|
|
||||||
let temp = self
|
|
||||||
.temperature()
|
|
||||||
.map_or("?".to_string(), |t| format!("{t:.1}°"));
|
|
||||||
let feels_like = self
|
|
||||||
.feels_like()
|
|
||||||
.map_or("?".to_string(), |t| format!("{t:.1}°"));
|
|
||||||
let desc = self
|
|
||||||
.primary_weather()
|
|
||||||
.map_or("Unknown", |w| w.description.as_str());
|
|
||||||
let wind = self
|
|
||||||
.wind_speed()
|
|
||||||
.map_or("?".to_string(), |s| format!("{s:.1} m/s"));
|
|
||||||
let pressure = self
|
|
||||||
.pressure()
|
|
||||||
.map_or("?".to_string(), |p| format!("{p} hPa"));
|
|
||||||
let humidity = self.humidity().map_or("?".to_string(), |h| format!("{h}%"));
|
|
||||||
|
|
||||||
format!(
|
|
||||||
"Weather in {city}:\n\
|
|
||||||
• Temperature: {temp} (feels like {feels_like})\n\
|
|
||||||
• Conditions: {desc}\n\
|
|
||||||
• Wind: {wind}\n\
|
|
||||||
• Pressure: {pressure}\n\
|
|
||||||
• Humidity: {humidity}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_success_response() {
|
|
||||||
let json = r#"
|
|
||||||
{
|
|
||||||
"coord": {"lon": -0.1257, "lat": 51.5085},
|
|
||||||
"weather": [{
|
|
||||||
"id": 300,
|
|
||||||
"main": "Drizzle",
|
|
||||||
"description": "light intensity drizzle",
|
|
||||||
"icon": "09d"
|
|
||||||
}],
|
|
||||||
"main": {
|
|
||||||
"temp": 280.32,
|
|
||||||
"feels_like": 278.15,
|
|
||||||
"pressure": 1012,
|
|
||||||
"humidity": 81
|
|
||||||
},
|
|
||||||
"wind": {
|
|
||||||
"speed": 4.1,
|
|
||||||
"deg": 240
|
|
||||||
},
|
|
||||||
"name": "London",
|
|
||||||
"cod": 200
|
|
||||||
}"#;
|
|
||||||
|
|
||||||
let response: WeatherResponse = serde_json::from_str(json).unwrap();
|
|
||||||
|
|
||||||
assert!(response.is_success());
|
|
||||||
assert_eq!(response.city(), Some("London"));
|
|
||||||
assert_eq!(response.temperature(), Some(280.32));
|
|
||||||
assert_eq!(response.feels_like(), Some(278.15));
|
|
||||||
assert_eq!(response.pressure(), Some(1012));
|
|
||||||
assert_eq!(response.humidity(), Some(81));
|
|
||||||
assert_eq!(response.wind_speed(), Some(4.1));
|
|
||||||
|
|
||||||
let weather = response.primary_weather().unwrap();
|
|
||||||
assert_eq!(weather.main, "Drizzle");
|
|
||||||
assert_eq!(weather.description, "light intensity drizzle");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_error_response() {
|
|
||||||
let json = r#"
|
|
||||||
{
|
|
||||||
"cod": 404,
|
|
||||||
"message": "city not found"
|
|
||||||
}"#;
|
|
||||||
|
|
||||||
let response: WeatherResponse = serde_json::from_str(json).unwrap();
|
|
||||||
|
|
||||||
assert!(!response.is_success());
|
|
||||||
assert_eq!(response.error_message(), Some("city not found"));
|
|
||||||
assert_eq!(response.city(), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_partial_response() {
|
|
||||||
// Test that missing fields don't cause deserialization to fail
|
|
||||||
let json = r#"{"cod": 200, "name": "Paris"}"#;
|
|
||||||
let response: WeatherResponse = serde_json::from_str(json).unwrap();
|
|
||||||
|
|
||||||
assert!(response.is_success());
|
|
||||||
assert_eq!(response.city(), Some("Paris"));
|
|
||||||
assert_eq!(response.temperature(), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_wind_direction() {
|
|
||||||
assert_eq!(
|
|
||||||
Wind {
|
|
||||||
deg: Some(0),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.direction(),
|
|
||||||
Some("N")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Wind {
|
|
||||||
deg: Some(90),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.direction(),
|
|
||||||
Some("E")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Wind {
|
|
||||||
deg: Some(180),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.direction(),
|
|
||||||
Some("S")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Wind {
|
|
||||||
deg: Some(270),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.direction(),
|
|
||||||
Some("W")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Wind {
|
|
||||||
deg: Some(45),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.direction(),
|
|
||||||
Some("NE")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_precipitation_intensity() {
|
|
||||||
let light = Precipitation {
|
|
||||||
one_hour: Some(1.0),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
assert_eq!(light.intensity(), Some("Light"));
|
|
||||||
|
|
||||||
let moderate = Precipitation {
|
|
||||||
one_hour: Some(5.0),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
assert_eq!(moderate.intensity(), Some("Moderate"));
|
|
||||||
|
|
||||||
let heavy = Precipitation {
|
|
||||||
one_hour: Some(10.0),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
assert_eq!(heavy.intensity(), Some("Heavy"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sys_daytime_none() {
|
|
||||||
let sys = Sys {
|
|
||||||
sunrise: None,
|
|
||||||
sunset: None,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
assert_eq!(sys.is_daytime(), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_wind_direction_out_of_bounds() {
|
|
||||||
assert_eq!(
|
|
||||||
Wind {
|
|
||||||
deg: Some(361),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.direction(),
|
|
||||||
Some("N")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Wind {
|
|
||||||
deg: Some(720),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.direction(),
|
|
||||||
Some("N")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_precipitation_three_hour_only() {
|
|
||||||
let p = Precipitation {
|
|
||||||
one_hour: None,
|
|
||||||
three_hour: Some(6.0),
|
|
||||||
};
|
|
||||||
assert_eq!(p.intensity(), Some("Moderate")); // 6/3 = 2.0 → Light
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct ForecastWeather {
|
|
||||||
pub id: u32,
|
|
||||||
pub main: String,
|
|
||||||
pub description: String,
|
|
||||||
pub icon: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct ForecastMain {
|
|
||||||
pub temp: f32,
|
|
||||||
pub pressure: f32,
|
|
||||||
pub humidity: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct ForecastEntry {
|
|
||||||
pub dt: u64,
|
|
||||||
pub main: ForecastMain,
|
|
||||||
pub weather: Vec<ForecastWeather>,
|
|
||||||
pub dt_txt: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct ForecastResponse {
|
|
||||||
pub cod: String,
|
|
||||||
pub cnt: u32,
|
|
||||||
pub list: Vec<ForecastEntry>,
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
pub mod current;
|
|
||||||
pub mod forecast;
|
|
||||||
pub mod query;
|
|
@ -1,253 +0,0 @@
|
|||||||
//! Query construction for `OpenWeatherMap` API v2.5
|
|
||||||
//!
|
|
||||||
//! Provides types and utilities for building request URLs for the
|
|
||||||
//! `/weather` and `/forecast` endpoints.
|
|
||||||
//!
|
|
||||||
//! ## Examples
|
|
||||||
//!
|
|
||||||
//! ```no_run
|
|
||||||
//! use owm_api25::query::{QueryParams, Units, WEATHER_URL};
|
|
||||||
//!
|
|
||||||
//! let query = QueryParams {
|
|
||||||
//! api_key: "MY_KEY".into(),
|
|
||||||
//! city_name: Some("London".into()),
|
|
||||||
//! ..Default::default()
|
|
||||||
//! };
|
|
||||||
//!
|
|
||||||
//! let url = query.weather_url().unwrap();
|
|
||||||
//! assert!(url.contains("q=London"));
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
/// Base URLs for `OpenWeatherMap` API v2.5
|
|
||||||
pub const BASE_URL: &str = "https://api.openweathermap.org/data/2.5";
|
|
||||||
pub const WEATHER_URL: &str = "https://api.openweathermap.org/data/2.5/weather";
|
|
||||||
pub const FORECAST_URL: &str = "https://api.openweathermap.org/data/2.5/forecast";
|
|
||||||
|
|
||||||
/// Units of measurement for temperature and wind speed.
|
|
||||||
///
|
|
||||||
/// - **Standard**: Kelvin (temperature), m/s (wind)
|
|
||||||
/// - **Metric**: Celsius, m/s
|
|
||||||
/// - **Imperial**: Fahrenheit, miles/hour
|
|
||||||
///
|
|
||||||
/// See: <https://openweathermap.org/current#data>
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// use owm_api25::query::Units;
|
|
||||||
/// assert_eq!(Units::Metric.to_string(), "metric");
|
|
||||||
/// ```
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub enum Units {
|
|
||||||
Standard,
|
|
||||||
Metric,
|
|
||||||
Imperial,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Units {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Units::Standard => write!(f, "standard"),
|
|
||||||
Units::Metric => write!(f, "metric"),
|
|
||||||
Units::Imperial => write!(f, "imperial"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Query parameters for `OpenWeatherMap` API.
|
|
||||||
///
|
|
||||||
/// **Note:** One of `city_id`, `city_name`, `(lat, lon)` pair, or `zip` **must** be provided,
|
|
||||||
/// otherwise URL building will fail.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct QueryParams {
|
|
||||||
/// Your `OpenWeatherMap` API key
|
|
||||||
pub api_key: String,
|
|
||||||
|
|
||||||
/// City ID (optional)
|
|
||||||
///
|
|
||||||
/// You can make an API call by city ID to get an unambiguous result for your city.
|
|
||||||
/// The list of city IDs (`city.list.json.gz`) can be downloaded [here](http://bulk.openweathermap.org/sample/).
|
|
||||||
pub city_id: Option<String>,
|
|
||||||
|
|
||||||
/// City name (optional)
|
|
||||||
///
|
|
||||||
/// You can call by city name, or city name + state code + country code.
|
|
||||||
/// Searching by state is only available for locations in the USA.
|
|
||||||
/// See: <https://openweathermap.org/current#name>
|
|
||||||
pub city_name: Option<String>,
|
|
||||||
|
|
||||||
/// Latitude (optional, must be used with `lon`)
|
|
||||||
pub lat: Option<f32>,
|
|
||||||
|
|
||||||
/// Longitude (optional, must be used with `lat`)
|
|
||||||
pub lon: Option<f32>,
|
|
||||||
|
|
||||||
/// Zip code (optional)
|
|
||||||
///
|
|
||||||
/// Format: `"94040,us"`. If the country code is not specified, it defaults to USA.
|
|
||||||
pub zip: Option<String>,
|
|
||||||
|
|
||||||
/// Units for temperature and wind (default `"Standard"`)
|
|
||||||
pub units: Units,
|
|
||||||
|
|
||||||
/// Language code for descriptions (default `"en"`)
|
|
||||||
pub lang: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl QueryParams {
|
|
||||||
/// Build query URL for the `/weather` endpoint.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns an `Err` if no valid location is specified
|
|
||||||
/// (`city_id`, `city_name`, `lat`/`lon`, or `zip` are all None).
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use owm_api25::query::{QueryParams, Units, WEATHER_URL};
|
|
||||||
///
|
|
||||||
/// let query = QueryParams {
|
|
||||||
/// api_key: "MY_API_KEY".to_string(),
|
|
||||||
/// city_name: Some("London".to_string()),
|
|
||||||
/// ..Default::default()
|
|
||||||
/// };
|
|
||||||
///
|
|
||||||
/// let url = query.weather_url().unwrap();
|
|
||||||
/// assert!(url.contains("q=London"));
|
|
||||||
/// assert!(url.contains("appid=MY_API_KEY"));
|
|
||||||
/// ```
|
|
||||||
pub fn weather_url(&self) -> Result<String, String> {
|
|
||||||
self.build_url(WEATHER_URL)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build query URL for the `/forecast` endpoint.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns an `Err` if no valid location is specified.
|
|
||||||
/// See `weather_url`.
|
|
||||||
pub fn forecast_url(&self) -> Result<String, String> {
|
|
||||||
self.build_url(FORECAST_URL)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Internal helper to build query URL.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns an `Err` if no valid location is specified (`city_id`, `city_name`, `lat`/`lon`, or `zip`).
|
|
||||||
fn build_url(&self, base: &str) -> Result<String, String> {
|
|
||||||
let mut params = vec![
|
|
||||||
format!("appid={}", self.api_key),
|
|
||||||
format!("units={}", self.units),
|
|
||||||
format!("lang={}", self.lang),
|
|
||||||
"mode=json".to_string(),
|
|
||||||
];
|
|
||||||
|
|
||||||
if let Some(ref id) = self.city_id {
|
|
||||||
params.push(format!("id={id}"));
|
|
||||||
} else if let Some(ref name) = self.city_name {
|
|
||||||
params.push(format!("q={name}"));
|
|
||||||
} else if let (Some(lat), Some(lon)) = (self.lat, self.lon) {
|
|
||||||
params.push(format!("lat={lat}&lon={lon}"));
|
|
||||||
} else if let Some(ref zip) = self.zip {
|
|
||||||
params.push(format!("zip={zip}"));
|
|
||||||
} else {
|
|
||||||
return Err("No valid location field found".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(format!("{base}?{}", params.join("&")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Default values for query parameters
|
|
||||||
impl Default for QueryParams {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
api_key: String::new(),
|
|
||||||
city_id: None,
|
|
||||||
city_name: None,
|
|
||||||
lat: None,
|
|
||||||
lon: None,
|
|
||||||
zip: None,
|
|
||||||
units: Units::Standard,
|
|
||||||
lang: "en".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_weather_url_city_name() {
|
|
||||||
let query = QueryParams {
|
|
||||||
api_key: "KEY".into(),
|
|
||||||
city_name: Some("Paris".into()),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let url = query.weather_url().unwrap();
|
|
||||||
assert!(url.contains("q=Paris"));
|
|
||||||
assert!(url.contains("appid=KEY"));
|
|
||||||
assert!(url.starts_with(WEATHER_URL));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_weather_url_city_id() {
|
|
||||||
let query = QueryParams {
|
|
||||||
api_key: "KEY".into(),
|
|
||||||
city_id: Some("123".into()),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let url = query.weather_url().unwrap();
|
|
||||||
assert!(url.contains("id=123"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_weather_url_lat_lon() {
|
|
||||||
let query = QueryParams {
|
|
||||||
api_key: "KEY".into(),
|
|
||||||
lat: Some(10.0),
|
|
||||||
lon: Some(20.0),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let url = query.weather_url().unwrap();
|
|
||||||
assert!(url.contains("lat=10") && url.contains("lon=20"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_weather_url_zip() {
|
|
||||||
let query = QueryParams {
|
|
||||||
api_key: "KEY".into(),
|
|
||||||
zip: Some("94040,us".into()),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let url = query.weather_url().unwrap();
|
|
||||||
assert!(url.contains("zip=94040,us"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_weather_url_error() {
|
|
||||||
let query = QueryParams {
|
|
||||||
api_key: "KEY".into(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
assert!(query.weather_url().is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_forecast_url() {
|
|
||||||
let query = QueryParams {
|
|
||||||
api_key: "KEY".into(),
|
|
||||||
city_name: Some("Berlin".into()),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let url = query.forecast_url().unwrap();
|
|
||||||
assert!(url.contains("q=Berlin"));
|
|
||||||
assert!(url.starts_with(FORECAST_URL));
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,7 +4,7 @@ version = "0.0.1"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
owm_api25 = { path = "../owm_api25" }
|
owm-rs = { git = "https://git.candifloss.cc/candifloss/OpenWeatherMapSDK.git" }
|
||||||
owm_widg_config = { path = "../owm_widg_config" }
|
owm_widg_config = { path = "../owm_widg_config" }
|
||||||
reqwest = {version = "0.12.23", features = ["blocking", "json"] }
|
reqwest = {version = "0.12.23", features = ["blocking", "json"] }
|
||||||
toml = "0.9.7"
|
toml = "0.9.7"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user