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