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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
//! Async client for the free Netherlands postcode API at <https://postcode.tech>.
//!
//! There are two methods, one to find the street and city matching the supplied postcode and house number, and one that also includes the municipality, province and coordinates. If no address can be found for the postcode and house number combination, `None` is returned.
//!
//! # Example
//! ```rust,no_run
//! # use std::error::Error;
//! # use postcode_nl::*;
//! # #[tokio::main]
//! # async fn main() -> Result<(), Box<dyn Error>> {
//! // Initialize a client
//! let client = PostcodeClient::new("YOUR_API_TOKEN");
//!
//! // Find the address matching on a postcode and house number
//! let (address, limits) = client.get_address("1012RJ", 147).await?;
//!
//! // Find the address and additional location information such as municipality, province and coordinates
//! let (address, limits) = client.get_extended_address("1012RJ", 147).await?;
//! # Ok(())
//! # }
//! ```
//!
//! # Usage Limits
//! As of the latest release of this crate, API usage is limited to 10,000 requests per day as well as 600 requests per 30 seconds. Please do not abuse this free service and ruin it for everyone else. [`ApiLimits`], included with the address response as shown above, reports the API limits (extracted from the response headers). The library validates the inputs in order to avoid making requests with invalid inputs, which would count towards the usage limits.
//!
//! # Disclaimer
//! I am not affiliated with the API provider and as such cannot make guarantees to the correctness of the results or the availability of the underlying service. Refer to <https://postcode.tech> for the service terms and conditions.

use internals::{call_api, IntoInternal, PostcodeApiFullResponse, PostcodeApiSimpleResponse};
use regex::Regex;
use reqwest::{Client, StatusCode};
use thiserror::Error;

mod internals;

/// The client that calls the API.
pub struct PostcodeClient {
    api_token: String,
    client: Client,
}

/// Simple address response.
#[derive(Debug, Clone)]
pub struct Address {
    pub street: String,
    pub house_number: u32,
    pub postcode: String,
    pub city: String,
}

/// Extended address response.
#[derive(Debug, Clone)]
pub struct ExtendedAddress {
    pub street: String,
    pub house_number: u32,
    pub postcode: String,
    pub city: String,
    pub municipality: String,
    pub province: String,
    pub coordinates: Coordinates,
}

/// Coordinates of the address
#[derive(Debug, Clone)]
pub struct Coordinates {
    pub lat: f32,
    pub lon: f32,
}

/// Usage limits of the API, returned with every request
#[derive(Debug, Clone)]
pub struct ApiLimits {
    pub ratelimit_limit: u32,
    pub ratelimit_remaining: u32,
    pub api_limit: u32,
    pub api_remaining: u32,
    pub api_reset: String,
}

impl PostcodeClient {
    /// Initialize a new client with an API token.
    /// ```rust,no_run
    /// # use std::error::Error;
    /// # use postcode_nl::*;
    /// # fn main()  {
    /// let client = PostcodeClient::new("YOUR_API_TOKEN");
    /// # }
    /// ```
    pub fn new(api_token: &str) -> Self {
        let client = Client::new();

        Self {
            api_token: api_token.to_string(),
            client,
        }
    }

    /// Find the address matching the given postcode and house number. Postcodes are formatted 1234AB or 1234 AB (with a single space). House numbers must be integers and not include postfix characters. Returns `None` when the address could not be found.
    /// ```rust,no_run
    /// # use std::error::Error;
    /// # use postcode_nl::*;
    /// # #[tokio::main]
    /// # async fn main() -> Result<(), Box<dyn Error>> {
    /// # let client: PostcodeClient = PostcodeClient::new("YOUR_API_TOKEN");
    /// let (address, limits) = client.get_address("1012RJ", 147).await?;
    /// # Ok(())
    /// # }
    /// ```
    pub async fn get_address(
        &self,
        postcode: &str,
        house_number: u32,
    ) -> Result<(Option<Address>, ApiLimits), PostcodeError> {
        let postcode = Self::validate_postcode_input(postcode)?;

        let response = call_api(&self.client, &self.api_token, postcode, house_number, false).await?;

        let limits = response.headers().try_into()?;
        let address = if response.status() == StatusCode::OK {
            Some(
                response
                    .json::<PostcodeApiSimpleResponse>()
                    .await
                    .map_err(|e| {
                        PostcodeError::InvalidApiResponse(format! {"Failed to deserialize API response, {e}"})
                    })?
                    .into_internal(postcode, house_number),
            )
        } else {
            None
        };

        Ok((address, limits))
    }

    /// Find the address, municipality, province and coordinates matching the given postcode and house number. Postcodes are formatted 1234AB or 1234 AB (with a single space). House numbers must be integers and not include postfix characters. Returns `None` when the address could not be found.
    /// ```rust,no_run
    /// # use std::error::Error;
    /// # use postcode_nl::*;
    /// # #[tokio::main]
    /// # async fn main() -> Result<(), Box<dyn Error>> {
    /// # let client: PostcodeClient = PostcodeClient::new("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx");
    /// let (address, limits) = client.get_extended_address("1012RJ", 147).await?;
    /// # Ok(())
    /// # }
    /// ```
    pub async fn get_extended_address(
        &self,
        postcode: &str,
        house_number: u32,
    ) -> Result<(Option<ExtendedAddress>, ApiLimits), PostcodeError> {
        let postcode = Self::validate_postcode_input(postcode)?;

        let response = call_api(&self.client, &self.api_token, postcode, house_number, true).await?;

        let limits = response.headers().try_into()?;
        let address = if response.status() == StatusCode::OK {
            Some(
                response
                    .json::<PostcodeApiFullResponse>()
                    .await
                    .map_err(|e| {
                        PostcodeError::InvalidApiResponse(format! {"Failed to deserialize API response, {e}"})
                    })?
                    .into(),
            )
        } else {
            None
        };

        Ok((address, limits))
    }

    fn validate_postcode_input(postcode: &str) -> Result<&str, PostcodeError> {
        let postcode_pattern = Regex::new(r"^\d{4} {0,1}[a-zA-Z]{2}$").unwrap();
        if postcode_pattern.is_match(postcode) {
            Ok(postcode)
        } else {
            Err(PostcodeError::InvalidInput(format!(
                "Postcodes should be formatted as `1234AB` or `1234 AB`, input: {postcode}"
            )))
        }
    }
}

/// Possible errors when fetching an address.
#[derive(Debug, Error)]
pub enum PostcodeError {
    /// The supplied postcode does not have the correct format: 1234AB or 1234 AB (with one space).
    #[error("Invalid input")]
    InvalidInput(String),
    /// The API did not respond to the request.
    #[error("Did not get response from API")]
    NoApiResponse(String),
    /// The API response body could not be parsed.
    #[error("Failed to parse API response")]
    InvalidApiResponse(String),
    /// The API responded that the inputs are incorrect. This should not happen and instead [`PostcodeError::InvalidInput`] should be returned.
    #[error("API returned that inputs are invalid")]
    InvalidData(String),
    /// The API responded with 429 TOO MANY REQUESTS. You've exceeded the API limits.
    #[error("API limits exceeded")]
    TooManyRequests(String),
    /// The API returned an unexpected error code.
    #[error("API returned an error")]
    OtherApiError(String),
}