Remove old library crate

- Replace crate `owm_api25` with new crate
This commit is contained in:
Candifloss 2025-10-10 11:54:10 +05:30
parent e8a2707987
commit a5a7e6a5db
6 changed files with 1 additions and 1095 deletions

View File

@ -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"] }

View File

@ -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
}
}

View File

@ -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>,
}

View File

@ -1,3 +0,0 @@
pub mod current;
pub mod forecast;
pub mod query;

View File

@ -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));
}
}

View File

@ -4,7 +4,7 @@ version = "0.0.1"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
owm_api25 = { path = "../owm_api25" } owm-rs = { git = "https://git.candifloss.cc/candifloss/OpenWeatherMapSDK.git" }
owm_widg_config = { path = "../owm_widg_config" } owm_widg_config = { path = "../owm_widg_config" }
reqwest = {version = "0.12.23", features = ["blocking", "json"] } reqwest = {version = "0.12.23", features = ["blocking", "json"] }
toml = "0.9.7" toml = "0.9.7"