use std::{collections::HashMap, fmt::Write, panic, string::ToString};
use derive_more::derive::Display;
use serde_json::Value;
use crate::schema::DateTimeJson;
#[derive(Debug)]
pub struct Client {
regions: Vec<Value>,
url: String,
}
#[derive(Debug, Display, Clone, Copy)]
pub enum Endpoint {
#[display("timezone")]
Timezone,
#[display("ip")]
Ip,
}
impl Client {
pub async fn new(endpoint: Endpoint) -> Result<Self, reqwest::Error> {
let regions: Vec<Value> = match endpoint {
Endpoint::Timezone => {
let url = "https://worldtimeapi.org/api/timezone/".to_string();
let response = reqwest::get(&url).await?;
response.json().await?
}
Endpoint::Ip => {
let url = "https://worldtimeapi.org/api/ip".to_string();
let response = reqwest::get(&url).await?;
response.json().await?
}
};
let url: String = format!("https://worldtimeapi.org/api/{endpoint}");
Ok(Self { regions, url })
}
pub async fn get(&self, payload: HashMap<&str, &str>) -> Result<DateTimeJson, reqwest::Error> {
let keys = payload
.keys()
.map(ToString::to_string)
.collect::<Vec<String>>();
let mut args = String::new();
for item in keys.clone() {
if !["area", "location", "region"]
.iter()
.map(ToString::to_string)
.any(|x| x == *item)
{
panic!("Invalid key: {item}");
}
}
if keys.contains(&"area".to_string()) {
write!(args, "/{}", payload["area"]).unwrap();
} else {
panic!("Missing key: area");
}
if keys.contains(&"location".to_string()) {
write!(args, "/{}", payload["location"]).unwrap();
}
if keys.contains(&"location".to_string()) && keys.contains(&"region".to_string()) {
write!(args, "/{}", payload["region"]).unwrap();
} else if !keys.contains(&"location".to_string()) && keys.contains(&"region".to_string()) {
panic!("Missing key: region");
}
let response = reqwest::get(&format!("{}{}", self.url, args))
.await?
.json::<DateTimeJson>()
.await?;
Ok(response)
}
#[must_use]
pub fn regions(&self) -> &[Value] {
self.regions.as_ref()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[tokio::test]
async fn test_client_get_success() {
let mut server = mockito::Server::new_async().await;
let mock_response = r#"
{
"abbreviation": "EDT",
"client_ip": "192.0.2.1",
"datetime": "2024-09-23T14:23:48+00:00",
"day_of_week": 1,
"day_of_year": 266,
"dst": true,
"dst_from": null,
"dst_offset": 3600,
"dst_until": null,
"raw_offset": -18000,
"timezone": "America/New_York",
"unixtime": 1695475428,
"utc_datetime": "2024-09-23T14:23:48Z",
"utc_offset": "-05:00",
"week_number": 39
}
"#;
let _m = server
.mock("GET", "/api/timezone/America/New_York")
.with_status(200)
.with_body(mock_response)
.create();
let client = Client {
regions: vec![],
url: format!("{}/api/timezone", server.url()),
};
let mut payload = HashMap::new();
payload.insert("area", "America");
payload.insert("location", "New_York");
let response = client.get(payload).await.unwrap();
let expected = serde_json::from_str::<DateTimeJson>(mock_response).unwrap();
assert_eq!(response.abbreviation(), expected.abbreviation());
assert_eq!(response.client_ip(), expected.client_ip());
}
#[tokio::test]
#[should_panic(expected = "Invalid key: invalid_key")]
async fn test_client_get_invalid_key() {
let server = mockito::Server::new_async().await;
let client = Client {
regions: vec![],
url: format!("{}/api/timezone", server.url()),
};
let mut payload = HashMap::new();
payload.insert("invalid_key", "America");
client.get(payload).await.unwrap();
}
#[tokio::test]
#[should_panic(expected = "Missing key: area")]
async fn test_client_get_missing_area() {
let server = mockito::Server::new_async().await;
let client = Client {
regions: vec![],
url: format!("{}/api/timezone", server.url()),
};
let mut payload = HashMap::new();
payload.insert("location", "New_York");
client.get(payload).await.unwrap();
}
#[test]
fn test_endpoint_display() {
assert_eq!(Endpoint::Timezone.to_string(), "timezone");
assert_eq!(Endpoint::Ip.to_string(), "ip");
}
}