1use reqwest::{header::HeaderMap, Client, Response, StatusCode, Url};
2use serde::Deserialize;
3
4use crate::{Address, ApiLimits, Coordinates, ExtendedAddress, PostcodeError};
5
6pub const API_URL_SIMPLE: &str = "https://postcode.tech/api/v1/postcode";
7pub const API_URL_FULL: &str = "https://postcode.tech/api/v1/postcode/full";
8
9#[derive(Deserialize, Debug, Clone)]
10pub(crate) struct PostcodeApiSimpleResponse {
11 pub street: String,
12 pub city: String,
13}
14
15#[derive(Deserialize, Debug)]
16pub(crate) struct PostcodeApiFullResponse {
17 pub postcode: String,
18 pub number: u32,
19 pub street: String,
20 pub city: String,
21 pub municipality: String,
22 pub province: String,
23 pub geo: Geo,
24}
25
26#[derive(Deserialize, Debug)]
27pub(crate) struct Geo {
28 pub lat: f32,
29 pub lon: f32,
30}
31
32pub(crate) async fn call_api(
33 client: &Client,
34 token: &str,
35 postcode: &str,
36 house_number: u32,
37 full: bool,
38) -> Result<Response, PostcodeError> {
39 let url = if full { API_URL_FULL } else { API_URL_SIMPLE };
40 let url = Url::parse_with_params(url, &[("postcode", postcode), ("number", &house_number.to_string())]).unwrap();
41
42 let response = client
43 .get(url)
44 .bearer_auth(token)
45 .send()
46 .await
47 .map_err(|e| PostcodeError::NoApiResponse(format!("Error contacting API, {e}")))?;
48
49 match response.status() {
50 StatusCode::OK => (),
51 StatusCode::NOT_FOUND => (), StatusCode::TOO_MANY_REQUESTS => return Err(PostcodeError::TooManyRequests("API limits exceeded".to_string())),
53 _ => {
54 return Err(PostcodeError::OtherApiError(format!(
55 "Received error from API, code: {}, {}",
56 response.status(),
57 response.text().await.unwrap()
58 )))
59 }
60 }
61
62 Ok(response)
63}
64
65impl TryFrom<&HeaderMap> for ApiLimits {
66 type Error = PostcodeError;
67
68 fn try_from(headers: &HeaderMap) -> Result<Self, PostcodeError> {
69 let ratelimit_limit = extract_header_u32(headers, "x-ratelimit-limit")?;
70 let ratelimit_remaining = extract_header_u32(headers, "x-ratelimit-remaining")?;
71 let api_limit = extract_header_u32(headers, "x-api-limit")?;
72 let api_remaining = extract_header_u32(headers, "x-api-remaining")?;
73 let api_reset = extract_header_string(headers, "x-api-reset")?;
74
75 Ok(Self {
76 ratelimit_limit,
77 ratelimit_remaining,
78 api_limit,
79 api_remaining,
80 api_reset,
81 })
82 }
83}
84
85fn extract_header_u32(headers: &HeaderMap, header_key: &str) -> Result<u32, PostcodeError> {
86 let value = headers
87 .get(header_key)
88 .ok_or_else(|| PostcodeError::InvalidApiResponse("API did not return API limits".to_string()))?
89 .to_str()
90 .map_err(|_e| PostcodeError::InvalidApiResponse("Failed to parse API rate limit from header".to_string()))?
91 .parse::<u32>()
92 .map_err(|_e| PostcodeError::InvalidApiResponse("Failed to parse API rate limit from header".to_string()))?;
93
94 Ok(value)
95}
96
97fn extract_header_string(headers: &HeaderMap, header_key: &str) -> Result<String, PostcodeError> {
98 let value = headers
99 .get(header_key)
100 .ok_or_else(|| PostcodeError::InvalidApiResponse("API did not return API limits".to_string()))?
101 .to_str()
102 .map_err(|_e| PostcodeError::InvalidApiResponse("Failed to parse API reset frequency from header".to_string()))?
103 .to_string();
104
105 Ok(value)
106}
107
108pub(crate) trait IntoInternal<T> {
109 fn into_internal(self, postcode: &str, house_number: u32) -> T;
110}
111
112impl IntoInternal<Address> for PostcodeApiSimpleResponse {
113 fn into_internal(self, postcode: &str, house_number: u32) -> Address {
114 Address {
115 street: self.street,
116 house_number,
117 postcode: postcode.to_string(),
118 city: self.city,
119 }
120 }
121}
122
123impl From<PostcodeApiFullResponse> for ExtendedAddress {
124 fn from(p: PostcodeApiFullResponse) -> Self {
125 Self {
126 street: p.street,
127 house_number: p.number,
128 postcode: p.postcode,
129 city: p.city,
130 municipality: p.municipality,
131 province: p.province,
132 coordinates: p.geo.into(),
133 }
134 }
135}
136
137impl From<Geo> for Coordinates {
138 fn from(g: Geo) -> Self {
139 Self { lat: g.lat, lon: g.lon }
140 }
141}