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),
}