use internals::{call_api, IntoInternal, PostcodeApiFullResponse, PostcodeApiSimpleResponse};
use regex::Regex;
use reqwest::{Client, StatusCode};
use thiserror::Error;
mod internals;
pub struct PostcodeClient {
api_token: String,
client: Client,
}
#[derive(Debug, Clone)]
pub struct Address {
pub street: String,
pub house_number: u32,
pub postcode: String,
pub city: String,
}
#[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,
}
#[derive(Debug, Clone)]
pub struct Coordinates {
pub lat: f32,
pub lon: f32,
}
#[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 {
pub fn new(api_token: &str) -> Self {
let client = Client::new();
Self {
api_token: api_token.to_string(),
client,
}
}
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))
}
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}"
)))
}
}
}
#[derive(Debug, Error)]
pub enum PostcodeError {
#[error("Invalid input")]
InvalidInput(String),
#[error("Did not get response from API")]
NoApiResponse(String),
#[error("Failed to parse API response")]
InvalidApiResponse(String),
#[error("API returned that inputs are invalid")]
InvalidData(String),
#[error("API limits exceeded")]
TooManyRequests(String),
#[error("API returned an error")]
OtherApiError(String),
}