1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
use serde::{Deserialize, Deserializer, Serialize};
use chrono::{Datelike, Local, TimeZone};

/// Struct representing a response from Forecast API
#[derive(Serialize, Deserialize, Debug)]
pub struct ForecastAPIResponse {
    #[serde(alias = "location")]
    at: LocationData,
    current: CurrentWeatherData,
    forecast: ForecastData,
}

/// Struct representing the location data
#[derive(Serialize, Deserialize, Debug)]
pub struct LocationData {
    #[serde(alias = "name")]
    location: String,
    #[serde(alias = "localtime_epoch", deserialize_with = "as_date_elements")]
    dt: DateElements,
}

/// Struct representing current weather data (normalized)
#[derive(Serialize, Deserialize, Debug)]
pub struct CurrentWeatherData {
    #[serde(deserialize_with = "as_one_decimal_place")]
    temp_c: String,
    #[serde(deserialize_with = "as_one_decimal_place")]
    temp_f: String,
    #[serde(deserialize_with = "as_one_decimal_place")]
    wind_mph: String,
    #[serde(deserialize_with = "as_one_decimal_place")]
    wind_kph: String,
    #[serde(deserialize_with = "as_one_decimal_place")]
    precip_mm: String,
    #[serde(deserialize_with = "as_one_decimal_place")]
    precip_in: String,
    humidity: u8,
    condition: WeatherCondition,
}

/// Struct representing a multi-day forecast
#[derive(Serialize, Deserialize, Debug)]
pub struct ForecastData {
    #[serde(alias = "forecastday")]
    at: Vec<WeatherReport>,
}

/// Struct representing a weather report
#[derive(Serialize, Deserialize, Debug)]
struct WeatherReport {
    #[serde(alias = "date_epoch", deserialize_with = "as_date_elements")]
    dt: DateElements,
    day: DayWeatherData,
}

/// Struct representing daily weather data
#[derive(Serialize, Deserialize, Debug)]
pub struct DayWeatherData {
    #[serde(deserialize_with = "as_one_decimal_place")]
    avgtemp_c: String,
    #[serde(deserialize_with = "as_one_decimal_place")]
    avgtemp_f: String,
    condition: WeatherCondition,
}

/// Struct with useful date elements
#[derive(Serialize, Deserialize, Debug)]
pub struct DateElements {
    weekday: String,
    weekday_short: String,
    pretty_date: String,
}

/// Struct representing the weather condition
#[derive(Serialize, Deserialize, Debug)]
pub struct WeatherCondition {
    #[serde(alias = "text")]
    description: String,
    #[serde(alias = "code", deserialize_with = "as_weather_icon")]
    symbol: String,
}

/// Deserializer to convert an epoch timestamp into a DateElements struct
fn as_date_elements<'de, D>(deserializer: D) -> Result<DateElements, D::Error>
where
    D: Deserializer<'de>,
{
    let seconds = i64::deserialize(deserializer)?;
    let local = Local.timestamp_opt(seconds, 0).unwrap();
    Ok(DateElements {
        weekday: local.format("%A").to_string(),
        weekday_short: local.weekday().to_string(),
        pretty_date: local.format("%e %B %Y").to_string(),
    })
}

/// Deserializer to convert a floating point value into a String with one decimal place
fn as_one_decimal_place<'de, D>(deserializer: D) -> Result<String, D::Error>
where
    D: Deserializer<'de>,
{
    let v = f32::deserialize(deserializer)?;
    Ok(format!("{:.1}", v))
}

/// Deserializer to convert a u8 weather condition code into a String corresponding to a weather symbol
/// See: https://www.weatherapi.com/docs/weather_conditions.json
/// See: https://fonts.google.com/icons
fn as_weather_icon<'de, D>(deserializer: D) -> Result<String, D::Error>
where
    D: Deserializer<'de>,
{
    let v = u16::deserialize(deserializer)?;
    let icon = match v {
        1003 | 1009 => "partly_cloudy_day",
        1006 => "cloudy",
        1087 | 1273 | 1276 | 1279 | 1282 => "thunderstorm",
        1030 | 1135 | 1147 => "lens_blur", // mist / fog
        1063 | 1150 | 1153 | 1168 | 1171 | 1180 | 1183 | 1186 | 1189 | 1192 | 1195 | 1198 | 1201 | 1240 | 1243 | 1246 => "rainy",
        1066 | 1072 | 1210 | 1216 | 1219 | 1222 | 1225 | 1249 | 1252 | 1255 | 1258 | 1261 | 1264  => "weather_snowy",
        1069 | 1114 | 1117 | 1204 | 1207 | 1237 => "severe_cold", // cold / icy weather warning
        _ => "sunny",
    };
    Ok(icon.to_string())
}