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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
use reqwest::{header::HeaderMap, Client, Response, StatusCode, Url};
use serde::Deserialize;

use crate::{Address, ApiLimits, Coordinates, ExtendedAddress, PostcodeError};

pub const API_URL_SIMPLE: &str = "https://postcode.tech/api/v1/postcode";
pub const API_URL_FULL: &str = "https://postcode.tech/api/v1/postcode/full";

#[derive(Deserialize, Debug, Clone)]
pub(crate) struct PostcodeApiSimpleResponse {
    pub street: String,
    pub city: String,
}

#[derive(Deserialize, Debug)]
pub(crate) struct PostcodeApiFullResponse {
    pub postcode: String,
    pub number: u32,
    pub street: String,
    pub city: String,
    pub municipality: String,
    pub province: String,
    pub geo: Geo,
}

#[derive(Deserialize, Debug)]
pub(crate) struct Geo {
    pub lat: f32,
    pub lon: f32,
}

pub(crate) async fn call_api(
    client: &Client,
    token: &str,
    postcode: &str,
    house_number: u32,
    full: bool,
) -> Result<Response, PostcodeError> {
    let url = if full { API_URL_FULL } else { API_URL_SIMPLE };
    let url = Url::parse_with_params(url, &[("postcode", postcode), ("number", &house_number.to_string())]).unwrap();

    let response = client
        .get(url)
        .bearer_auth(token)
        .send()
        .await
        .map_err(|e| PostcodeError::NoApiResponse(format!("Error contacting API, {e}")))?;

    match response.status() {
        StatusCode::OK => (),
        StatusCode::NOT_FOUND => (), // This is not an error, it just means the address was not found
        StatusCode::TOO_MANY_REQUESTS => return Err(PostcodeError::TooManyRequests("API limits exceeded".to_string())),
        _ => {
            return Err(PostcodeError::OtherApiError(format!(
                "Received error from API, code: {}, {}",
                response.status(),
                response.text().await.unwrap()
            )))
        }
    }

    Ok(response)
}

impl TryFrom<&HeaderMap> for ApiLimits {
    type Error = PostcodeError;

    fn try_from(headers: &HeaderMap) -> Result<Self, PostcodeError> {
        let ratelimit_limit = extract_header_u32(headers, "x-ratelimit-limit")?;
        let ratelimit_remaining = extract_header_u32(headers, "x-ratelimit-remaining")?;
        let api_limit = extract_header_u32(headers, "x-api-limit")?;
        let api_remaining = extract_header_u32(headers, "x-api-remaining")?;
        let api_reset = extract_header_string(headers, "x-api-reset")?;

        Ok(Self {
            ratelimit_limit,
            ratelimit_remaining,
            api_limit,
            api_remaining,
            api_reset,
        })
    }
}

fn extract_header_u32(headers: &HeaderMap, header_key: &str) -> Result<u32, PostcodeError> {
    let value = headers
        .get(header_key)
        .ok_or_else(|| PostcodeError::InvalidApiResponse("API did not return API limits".to_string()))?
        .to_str()
        .map_err(|_e| PostcodeError::InvalidApiResponse("Failed to parse API rate limit from header".to_string()))?
        .parse::<u32>()
        .map_err(|_e| PostcodeError::InvalidApiResponse("Failed to parse API rate limit from header".to_string()))?;

    Ok(value)
}

fn extract_header_string(headers: &HeaderMap, header_key: &str) -> Result<String, PostcodeError> {
    let value = headers
        .get(header_key)
        .ok_or_else(|| PostcodeError::InvalidApiResponse("API did not return API limits".to_string()))?
        .to_str()
        .map_err(|_e| PostcodeError::InvalidApiResponse("Failed to parse API reset frequency from header".to_string()))?
        .to_string();

    Ok(value)
}

pub(crate) trait IntoInternal<T> {
    fn into_internal(self, postcode: &str, house_number: u32) -> T;
}

impl IntoInternal<Address> for PostcodeApiSimpleResponse {
    fn into_internal(self, postcode: &str, house_number: u32) -> Address {
        Address {
            street: self.street,
            house_number,
            postcode: postcode.to_string(),
            city: self.city,
        }
    }
}

impl From<PostcodeApiFullResponse> for ExtendedAddress {
    fn from(p: PostcodeApiFullResponse) -> Self {
        Self {
            street: p.street,
            house_number: p.number,
            postcode: p.postcode,
            city: p.city,
            municipality: p.municipality,
            province: p.province,
            coordinates: p.geo.into(),
        }
    }
}

impl From<Geo> for Coordinates {
    fn from(g: Geo) -> Self {
        Self { lat: g.lat, lon: g.lon }
    }
}