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 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 {
|
||||
/// Longitude of the location (-180 to 180)
|
||||
pub lon: f64,
|
||||
/// Latitude of the location (-90 to 90)
|
||||
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 {
|
||||
/// 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,
|
||||
}
|
||||
|
||||
#[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 {
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
#[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 {
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
#[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 {
|
||||
/// Cloudiness percentage (0-100)
|
||||
#[serde(default)]
|
||||
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 {
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
#[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 {
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
#[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 {
|
||||
/// 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>,
|
||||
#[serde(with = "chrono::serde::ts_seconds_option")]
|
||||
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>,
|
||||
/// 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))
|
||||
}
|
||||
|
||||
pub fn temperature(&self) -> Option<f32> {
|
||||
self.main.as_ref().map(|m| m.temp)?
|
||||
/// 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()
|
||||
@ -132,4 +585,212 @@ impl WeatherResponse {
|
||||
.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,3 +1,3 @@
|
||||
pub mod current;
|
||||
pub mod forecast;
|
||||
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 reqwest::blocking;
|
||||
use std::fs;
|
||||
|
||||
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() {
|
||||
"free_2.5" => {
|
||||
/*
|
||||
let query = owm_api25::query::QueryParams::from(cfg.query_params);
|
||||
|
||||
let url = query.weather_url()?;
|
||||
@ -27,6 +27,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Icon: {}", w.icon);
|
||||
}
|
||||
println!("Temperature: {}°C", resp.main.temp);
|
||||
*/
|
||||
}
|
||||
other => {
|
||||
return Err(format!("Unsupported api_version: {other}").into());
|
||||
|
Loading…
x
Reference in New Issue
Block a user