diff --git a/owm_api25/Cargo.toml b/owm_api25/Cargo.toml deleted file mode 100644 index f6ce458..0000000 --- a/owm_api25/Cargo.toml +++ /dev/null @@ -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"] } diff --git a/owm_api25/src/current.rs b/owm_api25/src/current.rs deleted file mode 100644 index 88ca643..0000000 --- a/owm_api25/src/current.rs +++ /dev/null @@ -1,796 +0,0 @@ -//! # Current Weather Data -//! -//! Data structures and utilities for parsing and working with responses -//! from the **`OpenWeatherMap` Current Weather API**. -//! -//! See: -//! -//! ## 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> { -//! 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> { -//! 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, - /// "Feels like" temperature, accounting for human perception of weather - #[serde(default)] - pub feels_like: Option, - /// Minimum observed temperature (within large urban areas) - #[serde(default)] - pub temp_min: Option, - /// Maximum observed temperature (within large urban areas) - #[serde(default)] - pub temp_max: Option, - /// Atmospheric pressure at sea level (hPa) - #[serde(default)] - pub pressure: Option, - /// Humidity percentage (0-100) - #[serde(default)] - pub humidity: Option, - /// Atmospheric pressure at sea level (hPa) - #[serde(default)] - pub sea_level: Option, - /// Atmospheric pressure at ground level (hPa) - #[serde(default)] - pub grnd_level: Option, -} - -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 { - 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, - /// Wind direction in degrees (meteorological, 0-360) - /// - /// - 0°: North - /// - 90°: East - /// - 180°: South - /// - 270°: West - #[serde(default)] - pub deg: Option, - /// Wind gust speed in chosen units - #[serde(default)] - pub gust: Option, -} - -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, -} - -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, - /// Precipitation volume over the last 3 hours (mm) - #[serde(rename = "3h", default)] - pub three_hour: Option, -} - -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, - /// Internal parameter - #[serde(default)] - pub id: Option, - /// Country code (ISO 3166-1 alpha-2, e.g. "GB", "JP") - #[serde(default)] - pub country: Option, - /// Sunrise time (UTC Unix timestamp) - #[serde(with = "chrono::serde::ts_seconds_option")] - pub sunrise: Option>, - /// Sunset time (UTC Unix timestamp) - #[serde(with = "chrono::serde::ts_seconds_option")] - pub sunset: Option>, - /// Internal parameter (often used for error messages) - #[serde(default)] - pub message: Option, -} - -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 { - 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 { - 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, - /// Weather conditions array (usually contains one element) - #[serde(default)] - pub weather: Vec, - /// Internal parameter - #[serde(default)] - pub base: Option, - /// Main weather parameters - #[serde(default)] - pub main: Option
, - /// Visibility in meters (maximum value is 10,000 m) - #[serde(default)] - pub visibility: Option, - /// Wind information - #[serde(default)] - pub wind: Option, - /// Cloud cover information - #[serde(default)] - pub clouds: Option, - /// Rain precipitation data - #[serde(default)] - pub rain: Option, - /// Snow precipitation data - #[serde(default)] - pub snow: Option, - /// Time of data calculation (UTC Unix timestamp) - #[serde(with = "chrono::serde::ts_seconds_option")] - pub dt: Option>, - /// System information (country, sunrise, sunset, etc.) - #[serde(default)] - pub sys: Option, - /// Shift in seconds from UTC - #[serde(default)] - pub timezone: Option, - /// City ID - #[serde(default)] - pub id: Option, - /// City name - #[serde(default)] - pub name: Option, - /// 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, - /// Error message (present when API call fails) - #[serde(default)] - pub message: Option, -} - -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 { - self.main.as_ref().and_then(|m| m.temp) - } - - /// Returns the "feels like" temperature, if available - #[must_use] - pub fn feels_like(&self) -> Option { - 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 { - self.main.as_ref().and_then(|m| m.pressure) - } - - /// Returns the humidity percentage (0-100), if available - #[must_use] - pub fn humidity(&self) -> Option { - self.main.as_ref().and_then(|m| m.humidity) - } - - /// Returns the wind speed, if available - #[must_use] - pub fn wind_speed(&self) -> Option { - 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 { - self.wind.as_ref().and_then(|w| w.deg) - } - - /// Returns the cloudiness percentage (0-100), if available - #[must_use] - pub fn cloudiness(&self) -> Option { - 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 { - 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 - } -} diff --git a/owm_api25/src/forecast.rs b/owm_api25/src/forecast.rs deleted file mode 100644 index 6e75684..0000000 --- a/owm_api25/src/forecast.rs +++ /dev/null @@ -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, - pub dt_txt: String, -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct ForecastResponse { - pub cod: String, - pub cnt: u32, - pub list: Vec, -} diff --git a/owm_api25/src/lib.rs b/owm_api25/src/lib.rs deleted file mode 100644 index 93889f1..0000000 --- a/owm_api25/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod current; -pub mod forecast; -pub mod query; diff --git a/owm_api25/src/query.rs b/owm_api25/src/query.rs deleted file mode 100644 index aa33be2..0000000 --- a/owm_api25/src/query.rs +++ /dev/null @@ -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: -/// -/// # 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, - - /// 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: - pub city_name: Option, - - /// Latitude (optional, must be used with `lon`) - pub lat: Option, - - /// Longitude (optional, must be used with `lat`) - pub lon: Option, - - /// Zip code (optional) - /// - /// Format: `"94040,us"`. If the country code is not specified, it defaults to USA. - pub zip: Option, - - /// 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 { - 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 { - 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 { - 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)); - } -} diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 43b8053..c7940c5 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -4,7 +4,7 @@ version = "0.0.1" edition = "2024" [dependencies] -owm_api25 = { path = "../owm_api25" } +owm-rs = { git = "https://git.candifloss.cc/candifloss/OpenWeatherMapSDK.git" } owm_widg_config = { path = "../owm_widg_config" } reqwest = {version = "0.12.23", features = ["blocking", "json"] } toml = "0.9.7"