postcode_nl/
lib.rs

1//! Async client for the free Netherlands postcode API at <https://postcode.tech>.
2//!
3//! 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.
4//!
5//! # Example
6//! ```rust,no_run
7//! # use std::error::Error;
8//! # use postcode_nl::*;
9//! # #[tokio::main]
10//! # async fn main() -> Result<(), Box<dyn Error>> {
11//! // Initialize a client
12//! let client = PostcodeClient::new("YOUR_API_TOKEN");
13//!
14//! // Find the address matching on a postcode and house number
15//! let (address, limits) = client.get_address("1012RJ", 147).await?;
16//!
17//! // Find the address and additional location information such as municipality, province and coordinates
18//! let (address, limits) = client.get_extended_address("1012RJ", 147).await?;
19//! # Ok(())
20//! # }
21//! ```
22//!
23//! # Usage Limits
24//! 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.
25//!
26//! # Disclaimer
27//! 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.
28
29use internals::{call_api, IntoInternal, PostcodeApiFullResponse, PostcodeApiSimpleResponse};
30use regex::Regex;
31use reqwest::{Client, StatusCode};
32use thiserror::Error;
33
34mod internals;
35
36/// The client that calls the API.
37pub struct PostcodeClient {
38    api_token: String,
39    client: Client,
40}
41
42/// Simple address response.
43#[derive(Debug, Clone)]
44pub struct Address {
45    pub street: String,
46    pub house_number: u32,
47    pub postcode: String,
48    pub city: String,
49}
50
51/// Extended address response.
52#[derive(Debug, Clone)]
53pub struct ExtendedAddress {
54    pub street: String,
55    pub house_number: u32,
56    pub postcode: String,
57    pub city: String,
58    pub municipality: String,
59    pub province: String,
60    pub coordinates: Coordinates,
61}
62
63/// Coordinates of the address
64#[derive(Debug, Clone)]
65pub struct Coordinates {
66    pub lat: f32,
67    pub lon: f32,
68}
69
70/// Usage limits of the API, returned with every request
71#[derive(Debug, Clone)]
72pub struct ApiLimits {
73    pub ratelimit_limit: u32,
74    pub ratelimit_remaining: u32,
75    pub api_limit: u32,
76    pub api_remaining: u32,
77    pub api_reset: String,
78}
79
80impl PostcodeClient {
81    /// Initialize a new client with an API token.
82    /// ```rust,no_run
83    /// # use std::error::Error;
84    /// # use postcode_nl::*;
85    /// # fn main()  {
86    /// let client = PostcodeClient::new("YOUR_API_TOKEN");
87    /// # }
88    /// ```
89    pub fn new(api_token: &str) -> Self {
90        let client = Client::new();
91
92        Self {
93            api_token: api_token.to_string(),
94            client,
95        }
96    }
97
98    /// 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.
99    /// ```rust,no_run
100    /// # use std::error::Error;
101    /// # use postcode_nl::*;
102    /// # #[tokio::main]
103    /// # async fn main() -> Result<(), Box<dyn Error>> {
104    /// # let client: PostcodeClient = PostcodeClient::new("YOUR_API_TOKEN");
105    /// let (address, limits) = client.get_address("1012RJ", 147).await?;
106    /// # Ok(())
107    /// # }
108    /// ```
109    pub async fn get_address(
110        &self,
111        postcode: &str,
112        house_number: u32,
113    ) -> Result<(Option<Address>, ApiLimits), PostcodeError> {
114        let postcode = Self::validate_postcode_input(postcode)?;
115
116        let response = call_api(&self.client, &self.api_token, postcode, house_number, false).await?;
117
118        let limits = response.headers().try_into()?;
119        let address = if response.status() == StatusCode::OK {
120            Some(
121                response
122                    .json::<PostcodeApiSimpleResponse>()
123                    .await
124                    .map_err(|e| {
125                        PostcodeError::InvalidApiResponse(format! {"Failed to deserialize API response, {e}"})
126                    })?
127                    .into_internal(postcode, house_number),
128            )
129        } else {
130            None
131        };
132
133        Ok((address, limits))
134    }
135
136    /// 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.
137    /// ```rust,no_run
138    /// # use std::error::Error;
139    /// # use postcode_nl::*;
140    /// # #[tokio::main]
141    /// # async fn main() -> Result<(), Box<dyn Error>> {
142    /// # let client: PostcodeClient = PostcodeClient::new("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx");
143    /// let (address, limits) = client.get_extended_address("1012RJ", 147).await?;
144    /// # Ok(())
145    /// # }
146    /// ```
147    pub async fn get_extended_address(
148        &self,
149        postcode: &str,
150        house_number: u32,
151    ) -> Result<(Option<ExtendedAddress>, ApiLimits), PostcodeError> {
152        let postcode = Self::validate_postcode_input(postcode)?;
153
154        let response = call_api(&self.client, &self.api_token, postcode, house_number, true).await?;
155
156        let limits = response.headers().try_into()?;
157        let address = if response.status() == StatusCode::OK {
158            Some(
159                response
160                    .json::<PostcodeApiFullResponse>()
161                    .await
162                    .map_err(|e| {
163                        PostcodeError::InvalidApiResponse(format! {"Failed to deserialize API response, {e}"})
164                    })?
165                    .into(),
166            )
167        } else {
168            None
169        };
170
171        Ok((address, limits))
172    }
173
174    fn validate_postcode_input(postcode: &str) -> Result<&str, PostcodeError> {
175        let postcode_pattern = Regex::new(r"^\d{4} {0,1}[a-zA-Z]{2}$").unwrap();
176        if postcode_pattern.is_match(postcode) {
177            Ok(postcode)
178        } else {
179            Err(PostcodeError::InvalidInput(format!(
180                "Postcodes should be formatted as `1234AB` or `1234 AB`, input: {postcode}"
181            )))
182        }
183    }
184}
185
186/// Possible errors when fetching an address.
187#[derive(Debug, Error)]
188pub enum PostcodeError {
189    /// The supplied postcode does not have the correct format: 1234AB or 1234 AB (with one space).
190    #[error("Invalid input")]
191    InvalidInput(String),
192    /// The API did not respond to the request.
193    #[error("Did not get response from API")]
194    NoApiResponse(String),
195    /// The API response body could not be parsed.
196    #[error("Failed to parse API response")]
197    InvalidApiResponse(String),
198    /// The API responded that the inputs are incorrect. This should not happen and instead [`PostcodeError::InvalidInput`] should be returned.
199    #[error("API returned that inputs are invalid")]
200    InvalidData(String),
201    /// The API responded with 429 TOO MANY REQUESTS. You've exceeded the API limits.
202    #[error("API limits exceeded")]
203    TooManyRequests(String),
204    /// The API returned an unexpected error code.
205    #[error("API returned an error")]
206    OtherApiError(String),
207}