Add documentation to module current
- Add doc comments - cargo `clippy` fixes - Comment out code in `widget`
This commit is contained in:
parent
d8048daa51
commit
07651af450
@ -1,128 +1,581 @@
|
|||||||
|
//! # 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 chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, 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 {
|
pub struct Coord {
|
||||||
|
/// Longitude of the location (-180 to 180)
|
||||||
pub lon: f64,
|
pub lon: f64,
|
||||||
|
/// Latitude of the location (-90 to 90)
|
||||||
pub lat: f64,
|
pub lat: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
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 {
|
pub struct Weather {
|
||||||
|
/// Weather condition ID
|
||||||
|
///
|
||||||
|
/// See: [Weather condition codes](https://openweathermap.org/weather-conditions)
|
||||||
pub id: u32,
|
pub id: u32,
|
||||||
|
/// Group of weather parameters (e.g. Rain, Snow, Clouds)
|
||||||
pub main: String,
|
pub main: String,
|
||||||
|
/// Human-readable weather condition description
|
||||||
|
///
|
||||||
|
/// This field can be localized based on the API request.
|
||||||
pub description: String,
|
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,
|
pub icon: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
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 {
|
pub struct Main {
|
||||||
|
/// Temperature in requested units (Kelvin, Celsius, or Fahrenheit)
|
||||||
|
#[serde(default)]
|
||||||
pub temp: Option<f32>,
|
pub temp: Option<f32>,
|
||||||
|
/// "Feels like" temperature, accounting for human perception of weather
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub feels_like: Option<f32>,
|
pub feels_like: Option<f32>,
|
||||||
|
/// Minimum observed temperature (within large urban areas)
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub temp_min: Option<f32>,
|
pub temp_min: Option<f32>,
|
||||||
|
/// Maximum observed temperature (within large urban areas)
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub temp_max: Option<f32>,
|
pub temp_max: Option<f32>,
|
||||||
|
/// Atmospheric pressure at sea level (hPa)
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub pressure: Option<u32>,
|
pub pressure: Option<u32>,
|
||||||
|
/// Humidity percentage (0-100)
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub humidity: Option<u8>,
|
pub humidity: Option<u8>,
|
||||||
|
/// Atmospheric pressure at sea level (hPa)
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub sea_level: Option<u32>,
|
pub sea_level: Option<u32>,
|
||||||
|
/// Atmospheric pressure at ground level (hPa)
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub grnd_level: Option<u32>,
|
pub grnd_level: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
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 {
|
pub struct Wind {
|
||||||
|
/// Wind speed in chosen units (m/s by default)
|
||||||
|
#[serde(default)]
|
||||||
pub speed: Option<f32>,
|
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>,
|
pub deg: Option<u16>,
|
||||||
|
/// Wind gust speed in chosen units
|
||||||
|
#[serde(default)]
|
||||||
pub gust: Option<f32>,
|
pub gust: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
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 {
|
pub struct Clouds {
|
||||||
|
/// Cloudiness percentage (0-100)
|
||||||
|
#[serde(default)]
|
||||||
pub all: Option<u8>,
|
pub all: Option<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
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 {
|
pub struct Precipitation {
|
||||||
|
/// Precipitation volume over the last 1 hour (mm)
|
||||||
#[serde(rename = "1h", default)]
|
#[serde(rename = "1h", default)]
|
||||||
pub one_hour: Option<f32>,
|
pub one_hour: Option<f32>,
|
||||||
|
/// Precipitation volume over the last 3 hours (mm)
|
||||||
#[serde(rename = "3h", default)]
|
#[serde(rename = "3h", default)]
|
||||||
pub three_hour: Option<f32>,
|
pub three_hour: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
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 {
|
pub struct Sys {
|
||||||
|
/// Internal parameter
|
||||||
|
#[serde(default)]
|
||||||
pub r#type: Option<u8>,
|
pub r#type: Option<u8>,
|
||||||
|
/// Internal parameter
|
||||||
|
#[serde(default)]
|
||||||
pub id: Option<u32>,
|
pub id: Option<u32>,
|
||||||
|
/// Country code (ISO 3166-1 alpha-2, e.g. "GB", "JP")
|
||||||
|
#[serde(default)]
|
||||||
pub country: Option<String>,
|
pub country: Option<String>,
|
||||||
|
/// Sunrise time (UTC Unix timestamp)
|
||||||
#[serde(with = "chrono::serde::ts_seconds_option")]
|
#[serde(with = "chrono::serde::ts_seconds_option")]
|
||||||
pub sunrise: Option<DateTime<Utc>>,
|
pub sunrise: Option<DateTime<Utc>>,
|
||||||
|
/// Sunset time (UTC Unix timestamp)
|
||||||
#[serde(with = "chrono::serde::ts_seconds_option")]
|
#[serde(with = "chrono::serde::ts_seconds_option")]
|
||||||
pub sunset: Option<DateTime<Utc>>,
|
pub sunset: Option<DateTime<Utc>>,
|
||||||
|
/// Internal parameter (often used for error messages)
|
||||||
|
#[serde(default)]
|
||||||
pub message: Option<f64>,
|
pub message: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
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 {
|
pub struct WeatherResponse {
|
||||||
|
/// Geographic coordinates
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub coord: Option<Coord>,
|
pub coord: Option<Coord>,
|
||||||
|
/// Weather conditions array (usually contains one element)
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub weather: Vec<Weather>,
|
pub weather: Vec<Weather>,
|
||||||
|
/// Internal parameter
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub base: Option<String>,
|
pub base: Option<String>,
|
||||||
|
/// Main weather parameters
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub main: Option<Main>,
|
pub main: Option<Main>,
|
||||||
|
/// Visibility in meters (maximum value is 10,000 m)
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub visibility: Option<u32>,
|
pub visibility: Option<u32>,
|
||||||
#[serde(with = "chrono::serde::ts_seconds_option")]
|
/// Wind information
|
||||||
pub dt: Option<DateTime<Utc>>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub sys: Option<Sys>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub timezone: Option<i32>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub id: Option<u64>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub name: Option<String>,
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub wind: Option<Wind>,
|
pub wind: Option<Wind>,
|
||||||
|
/// Cloud cover information
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub clouds: Option<Clouds>,
|
pub clouds: Option<Clouds>,
|
||||||
|
/// Rain precipitation data
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub rain: Option<Precipitation>,
|
pub rain: Option<Precipitation>,
|
||||||
|
/// Snow precipitation data
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub snow: Option<Precipitation>,
|
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)]
|
#[serde(default)]
|
||||||
pub cod: Option<u16>,
|
pub cod: Option<u16>,
|
||||||
|
/// Error message (present when API call fails)
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub message: Option<String>,
|
pub message: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WeatherResponse {
|
impl WeatherResponse {
|
||||||
|
/// Returns the first (primary) weather condition, if present
|
||||||
|
#[must_use]
|
||||||
pub fn primary_weather(&self) -> Option<&Weather> {
|
pub fn primary_weather(&self) -> Option<&Weather> {
|
||||||
self.weather.first()
|
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 {
|
pub fn is_success(&self) -> bool {
|
||||||
matches!(self.cod, Some(200))
|
matches!(self.cod, Some(200))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn temperature(&self) -> Option<f32> {
|
/// Returns the error message if the API call failed
|
||||||
self.main.as_ref().map(|m| m.temp)?
|
///
|
||||||
|
/// 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> {
|
pub fn city(&self) -> Option<&str> {
|
||||||
self.name.as_deref()
|
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> {
|
pub fn country(&self) -> Option<&str> {
|
||||||
self.sys.as_ref()?.country.as_deref()
|
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 {
|
pub fn summary(&self) -> String {
|
||||||
let temp = self
|
let temp = self
|
||||||
.temperature()
|
.temperature()
|
||||||
@ -132,4 +585,212 @@ impl WeatherResponse {
|
|||||||
.map_or("N/A", |w| w.description.as_str());
|
.map_or("N/A", |w| w.description.as_str());
|
||||||
format!("{desc}, {temp}")
|
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,3 +1,3 @@
|
|||||||
|
pub mod current;
|
||||||
pub mod forecast;
|
pub mod forecast;
|
||||||
pub mod query;
|
pub mod query;
|
||||||
pub mod current;
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
use owm_api25::current::WeatherResponse;
|
//use owm_api25::current::WeatherResponse;
|
||||||
use owm_widg_config::config::Config;
|
use owm_widg_config::config::Config;
|
||||||
use reqwest::blocking;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
@ -16,6 +15,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
match cfg.general.api_version.as_str() {
|
match cfg.general.api_version.as_str() {
|
||||||
"free_2.5" => {
|
"free_2.5" => {
|
||||||
|
/*
|
||||||
let query = owm_api25::query::QueryParams::from(cfg.query_params);
|
let query = owm_api25::query::QueryParams::from(cfg.query_params);
|
||||||
|
|
||||||
let url = query.weather_url()?;
|
let url = query.weather_url()?;
|
||||||
@ -27,6 +27,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
println!("Icon: {}", w.icon);
|
println!("Icon: {}", w.icon);
|
||||||
}
|
}
|
||||||
println!("Temperature: {}°C", resp.main.temp);
|
println!("Temperature: {}°C", resp.main.temp);
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
other => {
|
other => {
|
||||||
return Err(format!("Unsupported api_version: {other}").into());
|
return Err(format!("Unsupported api_version: {other}").into());
|
||||||
|
Loading…
x
Reference in New Issue
Block a user