weather_cli/
lib.rs

1use std::{
2    env,
3    fs::File,
4    io::{Read, Write},
5};
6
7use anyhow::{anyhow, Context, Result};
8
9pub mod api_usage;
10pub mod cli;
11pub mod user_setup;
12
13#[cfg(test)]
14mod testing;
15
16mod program_info;
17mod types;
18
19pub mod constants {
20    /// JSON file name for an API key.
21    pub const API_JSON_NAME: &str = "api";
22
23    /// JSON file name for user setting.
24    pub const USER_SETTING_JSON_NAME: &str = "setting";
25
26    /// ## Current weather data
27    ///
28    /// Access current weather data for any location on Earth!
29    /// We collect and process weather data from different sources such as
30    /// global and local weather models, satellites, radars and a vast network
31    /// of weather stations. Data is available in JSON, XML, or HTML format.
32    /// API Documentation: [https://openweathermap.org/api/current](https://openweathermap.org/api/current)
33    ///
34    /// - `{lat_value}`: Latitude value of the location.
35    /// - `{lon_value}`: Longitude value of the location.
36    /// - `{api_key}`: OpenWeatherMap API key.
37    /// - `{unit}`: The desired measurement unit.
38    ///   - ex. `standard`, `metric`, `imperial`
39    ///   - MORE INFO: [https://openweathermap.org/weather-data](https://openweathermap.org/weather-data)
40    ///
41    /// ### Example Usage
42    /// ```
43    /// # use weather_cli::constants::WEATHER_API_URL;
44    /// let url = WEATHER_API_URL
45    ///     .replace("{LAT_VALUE}", "37.3361663")
46    ///     .replace("{LON_VALUE}", "-121.890591")
47    ///     .replace("{API_KEY}", "EXAMPLE_KEY")
48    ///     .replace("{UNIT}", "imperial");
49    ///
50    /// assert_eq!(url, "https://api.openweathermap.org/data/2.5/weather?lat=37.3361663&lon=-121.890591&appid=EXAMPLE_KEY&units=imperial");
51    /// ```
52    pub const WEATHER_API_URL: &str = "https://api.openweathermap.org/data/2.5/weather?lat={LAT_VALUE}&lon={LON_VALUE}&appid={API_KEY}&units={UNIT}";
53
54    /// ## Geocoding API
55    ///
56    /// Geocoding API is a simple tool that we have developed to ease
57    /// the search for locations while working with geographic names and coordinates.
58    /// API Documentation: [https://openweathermap.org/api/geocoding-api](https://openweathermap.org/api/geocoding-api)
59    ///
60    /// - `{QUERY}`: City search query.
61    /// - `{API_KEY}`: OpenWeatherMap API key.
62    ///
63    /// ### Example Usage
64    /// ```
65    /// # use weather_cli::constants::GEOLOCATION_API_URL;
66    /// let url = GEOLOCATION_API_URL
67    ///     .replace("{QUERY}", "Toronto")
68    ///     .replace("{API_KEY}", "EXAMPLE_KEY");
69    ///
70    /// assert_eq!(url, "http://api.openweathermap.org/geo/1.0/direct?q=Toronto&limit=10&appid=EXAMPLE_KEY");
71    /// ```
72    pub const GEOLOCATION_API_URL: &str =
73        "http://api.openweathermap.org/geo/1.0/direct?q={QUERY}&limit=10&appid={API_KEY}";
74}
75
76/// Returns executable directory.
77pub fn get_executable_directory() -> Result<String> {
78    let executable_path =
79        env::current_exe().context("Failed to get the executable file directory!")?;
80    let executable_directory = executable_path
81        .parent()
82        .context("Failed to get the executable directory!")?;
83
84    if let Some(dir_str) = executable_directory.to_str() {
85        return Ok(dir_str.to_string());
86    }
87
88    Err(anyhow!("Unable to get the executable directory."))
89}
90
91/// Returns `std::fs::File` type value of a JSON file.
92pub fn get_json_file(json_suffix: &str) -> Result<File> {
93    let executable_dir = get_executable_directory()?;
94
95    let file = match File::open(format!(
96        "{}/{}",
97        executable_dir,
98        make_json_file_name(json_suffix)
99    )) {
100        Ok(f) => f,
101        Err(_) => {
102            let mut new_file = File::create(format!(
103                "{}/{}",
104                executable_dir,
105                make_json_file_name(json_suffix)
106            ))
107            .context("Failed to create a json file.")?;
108            new_file
109                .write_all("{}".as_bytes())
110                .context("Failed to create a json file.")?;
111
112            File::open(format!(
113                "{}/{}",
114                executable_dir,
115                make_json_file_name(json_suffix)
116            ))
117            .context("Failed to get the json file.")?
118        }
119    };
120
121    Ok(file)
122}
123
124/// Complete a JSON file name.
125/// ## Example
126/// ```
127/// # use weather_cli::make_json_file_name;
128/// assert_eq!(make_json_file_name("api"), "weather-cli-api.json");
129/// ```
130pub fn make_json_file_name(suffix: &str) -> String {
131    format!("weather-cli-{}.json", suffix)
132}
133
134pub enum ErrorMessageType {
135    SettingRead,
136    ApiResponseRead,
137    InvalidApiKey,
138}
139
140fn get_file_read_error_message(error_type: ErrorMessageType, context: Option<&str>) -> String {
141    match (error_type, context) {
142        (ErrorMessageType::SettingRead, Some(context)) => {
143            if context == "api" {
144                format!(
145                    "Failed to read {}. Please make sure to setup your API key.",
146                    make_json_file_name(context)
147                )
148            } else {
149                format!("Failed to read the following file: {}", context)
150            }
151        }
152        (ErrorMessageType::ApiResponseRead, Some(context)) => {
153            format!("The given '{}' JSON input may be invalid.", context)
154        }
155        (ErrorMessageType::InvalidApiKey, None) => {
156            "API Key is invalid. Please try again.".to_string()
157        }
158        _ => unreachable!(),
159    }
160}
161
162/// Read a JSON file and return the string.
163pub fn read_json_file<T: serde::de::DeserializeOwned>(json_name: &str) -> Result<T> {
164    let mut file = get_json_file(json_name)?;
165    let mut json_string = String::new();
166    file.read_to_string(&mut json_string)?;
167
168    let api_key_data: T = serde_json::from_str(&json_string).context(
169        get_file_read_error_message(ErrorMessageType::SettingRead, Some(json_name)),
170    )?; // ERROR
171
172    Ok(api_key_data)
173}
174
175/// Reads a JSON file and returns serialized data.
176pub fn read_json_response<T: serde::de::DeserializeOwned>(
177    response: &str,
178    error_message_type: ErrorMessageType,
179    error_context: &str,
180) -> Result<T> {
181    use serde_json::Value;
182
183    let api_response: Value = serde_json::from_str(response).context(
184        get_file_read_error_message(ErrorMessageType::ApiResponseRead, Some(error_context)),
185    )?;
186
187    // Invalid API key error.
188    if let Some(401) = api_response["cod"].as_i64() {
189        return Err(anyhow!(get_file_read_error_message(
190            ErrorMessageType::InvalidApiKey,
191            None
192        )));
193    }
194
195    let response_data: T = serde_json::from_str(response).context(get_file_read_error_message(
196        error_message_type,
197        Some(error_context),
198    ))?;
199
200    Ok(response_data)
201}
202
203/// URL placeholder information.
204///
205/// ## Example Usage
206/// ```no_run
207/// # use weather_cli::URLPlaceholder;
208/// URLPlaceholder {
209///     placeholder: "{LAT_VALUE}".to_string(),
210///     value: "37.3361663".to_string(),
211/// };
212/// ```
213pub struct URLPlaceholder {
214    pub placeholder: String,
215    pub value: String,
216}
217
218/// Replaces URL placeholders with given values.
219pub fn replace_url_placeholders(url: &str, url_placeholders: &[URLPlaceholder]) -> String {
220    let mut replaced_url = String::from(url);
221    for url_placeholder in url_placeholders {
222        replaced_url = replaced_url.replace(&url_placeholder.placeholder, &url_placeholder.value);
223    }
224
225    replaced_url
226}