1use std::{collections::HashMap, fmt::Write, panic, string::ToString};
2
3use derive_more::derive::Display;
4use serde_json::Value;
5
6use crate::schema::DateTimeJson;
7
8#[derive(Debug)]
12pub struct Client {
13 regions: Vec<Value>,
14 url: String,
15}
16
17#[derive(Debug, Display, Clone, Copy)]
23pub enum Endpoint {
24 #[display("timezone")]
26 Timezone,
27 #[display("ip")]
29 Ip,
30}
31
32impl Client {
33 pub async fn new(endpoint: Endpoint) -> Result<Self, reqwest::Error> {
41 let regions: Vec<Value> = match endpoint {
43 Endpoint::Timezone => {
44 let url = "https://worldtimeapi.org/api/timezone/".to_string();
45 let response = reqwest::get(&url).await?;
46
47 response.json().await?
48 }
49 Endpoint::Ip => {
50 let url = "https://worldtimeapi.org/api/ip".to_string();
51 let response = reqwest::get(&url).await?;
52
53 response.json().await?
54 }
55 };
56
57 let url: String = format!("https://worldtimeapi.org/api/{endpoint}");
58
59 Ok(Self { regions, url })
60 }
61
62 pub async fn get(&self, payload: HashMap<&str, &str>) -> Result<DateTimeJson, reqwest::Error> {
78 let keys = payload
79 .keys()
80 .map(ToString::to_string)
81 .collect::<Vec<String>>();
82 let mut args = String::new();
83
84 for item in keys.clone() {
85 if !["area", "location", "region"]
86 .iter()
87 .map(ToString::to_string)
88 .any(|x| x == *item)
89 {
90 panic!("Invalid key: {item}");
91 }
92 }
93
94 if keys.contains(&"area".to_string()) {
95 write!(args, "/{}", payload["area"]).unwrap();
96 } else {
97 panic!("Missing key: area");
98 }
99
100 if keys.contains(&"location".to_string()) {
101 write!(args, "/{}", payload["location"]).unwrap();
102 }
103
104 if keys.contains(&"location".to_string()) && keys.contains(&"region".to_string()) {
105 write!(args, "/{}", payload["region"]).unwrap();
106 } else if !keys.contains(&"location".to_string()) && keys.contains(&"region".to_string()) {
107 panic!("Missing key: region");
108 }
109
110 let response = reqwest::get(&format!("{}{}", self.url, args))
111 .await?
112 .json::<DateTimeJson>()
113 .await?;
114 Ok(response)
115 }
116
117 #[must_use]
119 pub fn regions(&self) -> &[Value] {
120 self.regions.as_ref()
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 use std::collections::HashMap;
129
130 #[tokio::test]
131 async fn test_client_get_success() {
132 let mut server = mockito::Server::new_async().await;
133 let mock_response = r#"
134 {
135 "abbreviation": "EDT",
136 "client_ip": "192.0.2.1",
137 "datetime": "2024-09-23T14:23:48+00:00",
138 "day_of_week": 1,
139 "day_of_year": 266,
140 "dst": true,
141 "dst_from": null,
142 "dst_offset": 3600,
143 "dst_until": null,
144 "raw_offset": -18000,
145 "timezone": "America/New_York",
146 "unixtime": 1695475428,
147 "utc_datetime": "2024-09-23T14:23:48Z",
148 "utc_offset": "-05:00",
149 "week_number": 39
150 }
151 "#;
152
153 let _m = server
155 .mock("GET", "/api/timezone/America/New_York")
156 .with_status(200)
157 .with_body(mock_response)
158 .create();
159
160 let client = Client {
161 regions: vec![],
162 url: format!("{}/api/timezone", server.url()),
163 };
164
165 let mut payload = HashMap::new();
166 payload.insert("area", "America");
167 payload.insert("location", "New_York");
168
169 let response = client.get(payload).await.unwrap();
170
171 let expected = serde_json::from_str::<DateTimeJson>(mock_response).unwrap();
173 assert_eq!(response.abbreviation(), expected.abbreviation());
174 assert_eq!(response.client_ip(), expected.client_ip());
175 }
176
177 #[tokio::test]
178 #[should_panic(expected = "Invalid key: invalid_key")]
179 async fn test_client_get_invalid_key() {
180 let server = mockito::Server::new_async().await;
181 let client = Client {
182 regions: vec![],
183 url: format!("{}/api/timezone", server.url()),
184 };
185
186 let mut payload = HashMap::new();
187 payload.insert("invalid_key", "America");
188
189 client.get(payload).await.unwrap();
191 }
192
193 #[tokio::test]
194 #[should_panic(expected = "Missing key: area")]
195 async fn test_client_get_missing_area() {
196 let server = mockito::Server::new_async().await;
197 let client = Client {
198 regions: vec![],
199 url: format!("{}/api/timezone", server.url()),
200 };
201
202 let mut payload = HashMap::new();
203 payload.insert("location", "New_York");
204
205 client.get(payload).await.unwrap();
207 }
208
209 #[test]
210 fn test_endpoint_display() {
211 assert_eq!(Endpoint::Timezone.to_string(), "timezone");
212 assert_eq!(Endpoint::Ip.to_string(), "ip");
213 }
214}