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}