weather_cli/
api_usage.rs

1use anyhow::{anyhow, Context, Result};
2use chrono::{DateTime, FixedOffset, TimeZone, Utc};
3
4use crate::{
5    constants::GEOLOCATION_API_URL,
6    types::user_settings::{City, Units, UserSetting},
7    ErrorMessageType,
8};
9
10enum EventInfo<T: TimeZone> {
11    Sunrise(DateTime<T>),
12    Sunset(DateTime<T>),
13}
14impl<T: TimeZone> std::fmt::Display for EventInfo<T>
15where
16    T::Offset: std::fmt::Display,
17{
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        let local_time = match self {
20            EventInfo::Sunrise(sunrise_time) => sunrise_time.format("Sunrise: %I:%M %p"),
21            EventInfo::Sunset(sunset_time) => sunset_time.format("Sunset: %I:%M %p"),
22        };
23        write!(f, "{}", local_time)
24    }
25}
26
27/// Returns sunrise and sunset time.
28/// The first element in the returning array should be upcoming one.
29/// ex) It shows sunset time before sunrise time in the afternoon.
30fn convert_utc_to_local_time(
31    sunrise_timestamp: i64,
32    sunset_timestamp: i64,
33    timezone: i32,
34) -> Result<(EventInfo<FixedOffset>, EventInfo<FixedOffset>)> {
35    let timezone = FixedOffset::east_opt(timezone).context("Failed to read timezone value.")?;
36
37    let current_time = Utc::now().with_timezone(&timezone);
38    let sunrise = DateTime::<Utc>::from_timestamp(sunrise_timestamp, 0)
39        .context("Failed to read sunrise time.")?
40        .with_timezone(&timezone);
41    let sunset = DateTime::<Utc>::from_timestamp(sunset_timestamp, 0)
42        .context("Failed to read sunset Time.")?
43        .with_timezone(&timezone);
44
45    // The first element should be the next upcoming event.
46    if current_time < sunrise {
47        Ok((EventInfo::Sunrise(sunrise), EventInfo::Sunset(sunset)))
48    } else if current_time < sunset {
49        Ok((EventInfo::Sunset(sunset), EventInfo::Sunrise(sunrise)))
50    } else {
51        Ok((EventInfo::Sunrise(sunrise), EventInfo::Sunset(sunset)))
52    }
53}
54
55/// Returns a response from the given URL.
56async fn get_response(url: String) -> Result<String> {
57    let resp = reqwest::get(&url).await?;
58    let text = resp.text().await?;
59    Ok(text)
60}
61
62/// Prints weather information from the API.
63pub async fn print_weather_information() -> Result<()> {
64    use crate::{
65        constants::{API_JSON_NAME, USER_SETTING_JSON_NAME, WEATHER_API_URL},
66        read_json_file, read_json_response, replace_url_placeholders,
67        types::{response_types::WeatherApiResponse, user_settings::ApiSetting},
68        URLPlaceholder,
69    };
70
71    let api_json_data = read_json_file::<ApiSetting>(API_JSON_NAME)?;
72    let setting_json_data = read_json_file::<UserSetting>(USER_SETTING_JSON_NAME)?;
73
74    let url = match (&setting_json_data.city, &setting_json_data.units) {
75        (Some(city), Some(unit)) => {
76            let units = unit.to_string();
77
78            replace_url_placeholders(
79                WEATHER_API_URL,
80                &[
81                    URLPlaceholder {
82                        placeholder: "{LAT_VALUE}".to_string(),
83                        value: city.lat.to_string(),
84                    },
85                    URLPlaceholder {
86                        placeholder: "{LON_VALUE}".to_string(),
87                        value: city.lon.to_string(),
88                    },
89                    URLPlaceholder {
90                        placeholder: "{API_KEY}".to_string(),
91                        value: api_json_data.key,
92                    },
93                    URLPlaceholder {
94                        placeholder: "{UNIT}".to_string(),
95                        value: units,
96                    },
97                ],
98            )
99        }
100        _ => {
101            return Err(anyhow!(
102            "Failed to read user setting! Please run 'set-location' command to configure settings."
103        ))
104        }
105    };
106
107    let response = get_response(url).await?;
108    let response_data = read_json_response::<WeatherApiResponse>(
109        &response,
110        ErrorMessageType::ApiResponseRead,
111        "WeatherApiResponse",
112    )?;
113
114    let upcoming_event = convert_utc_to_local_time(
115        response_data.sys.sunrise as i64,
116        response_data.sys.sunset as i64,
117        response_data.timezone,
118    )?;
119
120    // Print the weather information.
121    {
122        /*
123        Example Output:
124        ```
125        Toronto (CA)
126        9.57° / Clouds (overcast clouds)
127        H: 9.57°, L: 9.57°
128
129        - Wind Speed: 4.59 m/s,
130        - Humidity: 61 %,
131        - Pressure: 1017 hPa
132        - Sunrise: 06:22 AM
133          (Sunset: 08:09 PM)
134          ```
135        */
136
137        let selected_city = setting_json_data
138            .city
139            .context("Failed to read city setting data.")?;
140        let selected_unit = setting_json_data
141            .units
142            .context("Failed to read unit setting data.")?;
143        let wind_unit = match selected_unit {
144            Units::Standard => "m/s",
145            Units::Metric => "m/s",
146            Units::Imperial => "mph",
147        };
148
149        let output_messages = [
150            String::new(),
151            format!("{} ({})", selected_city.name, selected_city.country),
152            format!(
153                "{temp}° / {main} ({description})",
154                temp = response_data.main.temp,
155                main = response_data.weather[0].main,
156                description = response_data.weather[0].description
157            ),
158            format!(
159                "H: {max}°, L: {min}°",
160                max = response_data.main.temp_max,
161                min = response_data.main.temp_min
162            ),
163            format!(
164                "\n- Wind Speed: {speed} {wind_speed_unit},",
165                speed = response_data.wind.speed,
166                wind_speed_unit = wind_unit
167            ),
168            format!(
169                "- Humidity: {humidity} %,",
170                humidity = response_data.main.humidity
171            ),
172            format!(
173                "- Pressure: {pressure} hPa",
174                pressure = response_data.main.pressure
175            ),
176            format!("- {}", upcoming_event.0),
177            format!("  ({})", upcoming_event.1),
178        ];
179
180        for item in output_messages {
181            println!("{}", item);
182        }
183
184        Ok(())
185    }
186}
187
188/// Prints cities from a slice argument.
189fn display_cities(city_slice: &[City]) {
190    println!("\n* City list:");
191    for (index, city) in city_slice.iter().enumerate() {
192        println!("{}) {}", index + 1, city);
193    }
194}
195
196/// Displays a prompt message and read user input.
197fn read_user_input(messages: &[&str]) -> Result<String> {
198    use std::io;
199
200    for &message in messages {
201        println!("{}", message);
202    }
203
204    let mut user_input: String = String::new();
205    io::stdin().read_line(&mut user_input)?;
206    println!();
207
208    if user_input.is_empty() {
209        Err(anyhow!("Input is empty!"))
210    } else {
211        Ok(user_input)
212    }
213}
214
215/// Saves user's city and unit preferences.
216fn select_user_preferences(cities: &[City]) -> Result<(String, Units)> {
217    use crate::user_setup::update_user_settings;
218
219    let city: &City = {
220        let user_input = read_user_input(&["Please select your city."])?;
221
222        let parsed_input: usize = user_input.trim().parse()?;
223        if parsed_input > cities.len() {
224            return Err(anyhow!("Invalid city index."));
225        }
226
227        &cities[parsed_input - 1]
228    };
229
230    let units: Units = {
231        let user_input = read_user_input(&[
232            "* Select your preferred unit.",
233            "* MORE INFO: https://openweathermap.org/weather-data",
234            "1) Standard",
235            "2) Metric",
236            "3) Imperial",
237        ])?;
238
239        let parsed_input: usize = user_input
240            .trim()
241            .parse()
242            .context("Failed to parse the input. Make sure it's a valid positive number.")?;
243
244        match parsed_input {
245            1 => Units::Standard,
246            2 => Units::Metric,
247            3 => Units::Imperial,
248            _ => return Err(anyhow!("Input is out of range!")),
249        }
250    };
251
252    let user_setting = UserSetting {
253        city: Some(City {
254            name: city.name.clone(),
255            lat: city.lat,
256            lon: city.lon,
257            country: city.country.clone(),
258        }),
259        units: Some(units.clone()),
260    };
261
262    update_user_settings(&user_setting)?;
263
264    Ok((city.name.clone(), units))
265}
266
267/// Selects a city from a list.
268pub async fn search_city(query: &str) -> Result<()> {
269    use serde_json::Value;
270
271    use crate::{
272        constants::API_JSON_NAME, get_file_read_error_message, read_json_file,
273        replace_url_placeholders, types::user_settings::ApiSetting, URLPlaceholder,
274    };
275
276    if query.is_empty() {
277        return Err(anyhow!("Query cannot be empty."));
278    }
279
280    let api_json_data = read_json_file::<ApiSetting>(API_JSON_NAME)?;
281
282    let url = replace_url_placeholders(
283        GEOLOCATION_API_URL,
284        &[
285            URLPlaceholder {
286                placeholder: "{QUERY}".to_string(),
287                value: query.to_string(),
288            },
289            URLPlaceholder {
290                placeholder: "{API_KEY}".to_string(),
291                value: api_json_data.key.clone(),
292            },
293        ],
294    );
295    let response = get_response(url).await?;
296    let data: Value =
297        serde_json::from_str(&response).context("The given JSON input may be invalid.")?;
298
299    // Invalid API key error.
300    if let Some(401) = data["cod"].as_i64() {
301        return Err(anyhow!(get_file_read_error_message(
302            ErrorMessageType::InvalidApiKey,
303            None
304        )));
305    }
306
307    let mut cities: Vec<City> = vec![];
308
309    for city in data.as_array().unwrap() {
310        cities.push(City {
311            name: city["name"].as_str().unwrap().to_string(),
312            lat: city["lat"].as_f64().unwrap(),
313            lon: city["lon"].as_f64().unwrap(),
314            country: city["country"].as_str().unwrap().to_string(),
315        });
316    }
317    display_cities(&cities);
318
319    match select_user_preferences(&cities) {
320        Ok((city_name, unit_name)) => {
321            println!("{} is now your city!", city_name);
322            println!("I'll use {} for you.", unit_name);
323        }
324        Err(e) => {
325            println!("ERROR: {}", e);
326        }
327    };
328
329    Ok(())
330}