use std::time::Duration;
use reqwest::header::{HeaderMap, HeaderValue, ACCEPT};
use serde::de::DeserializeOwned;
use crate::error::{error_from_response, Error, Result};
use crate::models::{FeatureCollection, GeocodingResponse};
use crate::places::PlacesClient;
pub const DEFAULT_BASE_URL: &str = "https://api.latlng.work";
pub const DEFAULT_AUTOSUGGEST_BASE_URL: &str = "https://suggest.latlng.work";
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
#[derive(Clone, Debug)]
pub struct LatlngClient {
http: reqwest::Client,
base_url: String,
autosuggest_base_url: String,
}
impl LatlngClient {
pub fn anonymous() -> Result<Self> {
Self::builder().build()
}
pub fn with_api_key(api_key: impl Into<String>) -> Result<Self> {
Self::builder().api_key(api_key).build()
}
pub fn builder() -> ClientBuilder {
ClientBuilder::default()
}
pub fn places(&self) -> PlacesClient<'_> {
PlacesClient::new(self)
}
pub async fn geocode(&self, query: impl AsRef<str>) -> Result<GeocodingResponse> {
self.geocode_with(GeocodeRequest::new(query.as_ref())).await
}
pub async fn geocode_with(&self, request: GeocodeRequest<'_>) -> Result<GeocodingResponse> {
let mut params = vec![("q", request.query.to_string())];
push_param(&mut params, "limit", request.limit);
push_param(&mut params, "lang", request.lang);
push_param(&mut params, "lat", request.lat);
push_param(&mut params, "lon", request.lon);
let response: FeatureCollection = self.get_json("/api", ¶ms).await?;
Ok(response.into())
}
pub async fn reverse(&self, lat: f64, lon: f64) -> Result<GeocodingResponse> {
self.reverse_with(ReverseRequest::new(lat, lon)).await
}
pub async fn reverse_with(&self, request: ReverseRequest<'_>) -> Result<GeocodingResponse> {
let mut params = vec![
("lat", request.lat.to_string()),
("lon", request.lon.to_string()),
];
push_param(&mut params, "limit", request.limit);
push_param(&mut params, "lang", request.lang);
let response: FeatureCollection = self.get_json("/reverse", ¶ms).await?;
Ok(response.into())
}
pub async fn health(&self) -> Result<bool> {
let response = self
.http
.get(self.url("/health"))
.send()
.await
.map_err(Error::Request)?;
Ok(response.status().is_success())
}
pub(crate) async fn get_json<T>(&self, path: &str, params: &[(&str, String)]) -> Result<T>
where
T: DeserializeOwned,
{
let response = self
.http
.get(self.url(path))
.query(params)
.send()
.await
.map_err(Error::Request)?;
if response.status().is_success() {
response.json::<T>().await.map_err(Error::Request)
} else {
Err(error_from_response(response).await)
}
}
pub(crate) async fn get_autosuggest_json<T>(
&self,
path: &str,
params: &[(&str, String)],
) -> Result<T>
where
T: DeserializeOwned,
{
let response = self
.http
.get(format!("{}{}", self.autosuggest_base_url, path))
.query(params)
.send()
.await
.map_err(Error::Request)?;
if response.status().is_success() {
response.json::<T>().await.map_err(Error::Request)
} else {
Err(error_from_response(response).await)
}
}
fn url(&self, path: &str) -> String {
format!("{}{}", self.base_url, path)
}
}
#[derive(Debug, Clone)]
pub struct ClientBuilder {
api_key: Option<String>,
base_url: String,
autosuggest_base_url: String,
timeout: Duration,
}
impl Default for ClientBuilder {
fn default() -> Self {
Self {
api_key: None,
base_url: DEFAULT_BASE_URL.to_string(),
autosuggest_base_url: DEFAULT_AUTOSUGGEST_BASE_URL.to_string(),
timeout: DEFAULT_TIMEOUT,
}
}
}
impl ClientBuilder {
pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
self.api_key = Some(api_key.into());
self
}
pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
let base_url = base_url.into().trim_end_matches('/').to_string();
self.base_url = base_url.clone();
self.autosuggest_base_url = base_url;
self
}
pub fn autosuggest_base_url(mut self, base_url: impl Into<String>) -> Self {
self.autosuggest_base_url = base_url.into().trim_end_matches('/').to_string();
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn build(self) -> Result<LatlngClient> {
let mut headers = HeaderMap::new();
headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
if let Some(api_key) = self.api_key.filter(|key| !key.trim().is_empty()) {
headers.insert(
"x-api-key",
HeaderValue::from_str(&api_key).map_err(Error::InvalidApiKey)?,
);
}
let http = reqwest::Client::builder()
.default_headers(headers)
.timeout(self.timeout)
.build()
.map_err(Error::ClientBuild)?;
Ok(LatlngClient {
http,
base_url: self.base_url.trim_end_matches('/').to_string(),
autosuggest_base_url: self.autosuggest_base_url.trim_end_matches('/').to_string(),
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct GeocodeRequest<'a> {
query: &'a str,
limit: Option<u32>,
lang: Option<&'a str>,
lat: Option<f64>,
lon: Option<f64>,
}
impl<'a> GeocodeRequest<'a> {
pub fn new(query: &'a str) -> Self {
Self {
query,
limit: None,
lang: None,
lat: None,
lon: None,
}
}
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
pub fn lang(mut self, lang: &'a str) -> Self {
self.lang = Some(lang);
self
}
pub fn bias(mut self, lat: f64, lon: f64) -> Self {
self.lat = Some(lat);
self.lon = Some(lon);
self
}
}
#[derive(Debug, Clone, Copy)]
pub struct ReverseRequest<'a> {
lat: f64,
lon: f64,
limit: Option<u32>,
lang: Option<&'a str>,
}
impl<'a> ReverseRequest<'a> {
pub fn new(lat: f64, lon: f64) -> Self {
Self {
lat,
lon,
limit: None,
lang: None,
}
}
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
pub fn lang(mut self, lang: &'a str) -> Self {
self.lang = Some(lang);
self
}
}
fn push_param<'a, T>(params: &mut Vec<(&'a str, String)>, key: &'a str, value: Option<T>)
where
T: ToString,
{
if let Some(value) = value {
params.push((key, value.to_string()));
}
}