diff --git a/owm_api25/src/query.rs b/owm_api25/src/query.rs index c1db86a..aa33be2 100644 --- a/owm_api25/src/query.rs +++ b/owm_api25/src/query.rs @@ -1,11 +1,45 @@ +//! 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 -#[derive(Debug)] +/// Units of measurement for temperature and wind speed. +/// +/// - **Standard**: Kelvin (temperature), m/s (wind) +/// - **Metric**: Celsius, m/s +/// - **Imperial**: Fahrenheit, miles/hour +/// +/// See: +/// +/// # Example +/// ``` +/// use owm_api25::query::Units; +/// assert_eq!(Units::Metric.to_string(), "metric"); +/// ``` + +#[derive(Debug, Clone, Copy)] pub enum Units { Standard, Metric, @@ -22,16 +56,43 @@ impl fmt::Display for Units { } } -/// Query parameters for `OpenWeatherMap` Free API v2.5 endpoints -#[derive(Debug)] +/// 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, + + /// 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: pub city_name: Option, + + /// Latitude (optional, must be used with `lon`) pub lat: Option, + + /// Longitude (optional, must be used with `lat`) pub lon: Option, + + /// Zip code (optional) + /// + /// Format: `"94040,us"`. If the country code is not specified, it defaults to USA. pub zip: Option, + + /// Units for temperature and wind (default `"Standard"`) pub units: Units, + + /// Language code for descriptions (default `"en"`) pub lang: String, } @@ -41,7 +102,23 @@ impl QueryParams { /// # Errors /// /// Returns an `Err` if no valid location is specified - /// (e.g., `city_id`, `city_name`, `lat`/`lon`, or `zip` are all `None`). + /// (`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 { self.build_url(WEATHER_URL) } @@ -50,12 +127,17 @@ impl QueryParams { /// /// # Errors /// - /// Returns an `Err` if no valid location is specified - /// (e.g., `city_id`, `city_name`, `lat`/`lon`, or `zip` are all `None`). + /// Returns an `Err` if no valid location is specified. + /// See `weather_url`. pub fn forecast_url(&self) -> Result { 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 { let mut params = vec![ format!("appid={}", self.api_key), @@ -79,3 +161,93 @@ impl QueryParams { 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)); + } +}