postcode_nl/
internals.rs

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 => (), // This is not an error, it just means the address was not found
52        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}