diff --git a/src/free_api_v25/current/mod.rs b/src/free_api_v25/current/mod.rs index 8b80684..4b8dbe5 100644 --- a/src/free_api_v25/current/mod.rs +++ b/src/free_api_v25/current/mod.rs @@ -21,6 +21,16 @@ pub mod clouds; pub mod coord; pub mod main; pub mod precipitation; +pub mod response; pub mod sys; pub mod weather; pub mod wind; + +pub use clouds::*; +pub use coord::*; +pub use main::*; +pub use precipitation::*; +pub use response::*; +pub use sys::*; +pub use weather::*; +pub use wind::*; diff --git a/src/free_api_v25/current/response.rs b/src/free_api_v25/current/response.rs new file mode 100644 index 0000000..b1a7eca --- /dev/null +++ b/src/free_api_v25/current/response.rs @@ -0,0 +1,282 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use super::{Clouds, Coord, Main, Precipitation, Sys, Weather, Wind}; + +/// 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}", + ) + } +}