use chrono::{DateTime, FixedOffset};
use tower_service::Service;
use std::{
borrow::Cow,
future::Future,
pin::Pin,
task::{Context, Poll},
};
use crate::{body::Body, client::Client, Error, Result};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Params {
pub lat: f64,
pub lon: f64,
pub alt: Option<i32>,
pub last_response: Option<Response>,
}
impl Params {
pub fn new(lat: f64, lon: f64, alt: impl Into<Option<i32>>) -> Result<Self> {
Self::new_with_last_response(lat, lon, alt, None)
}
pub fn new_with_last_response(
lat: f64,
lon: f64,
alt: impl Into<Option<i32>>,
last_response: impl Into<Option<Response>>,
) -> Result<Self> {
if !lat.is_finite() || lat.abs() > 90.0 {
return Err(Error::Params("Invalid lat value."));
}
if !lon.is_finite() || lon.abs() > 180.0 {
return Err(Error::Params("Invalid lon value."));
}
let alt = alt.into();
if let Some(alt) = alt {
if !(-500..=9000).contains(&alt) {
return Err(Error::Params("Invalid alt value."));
}
}
Ok(Self {
lat: (lat * 10000.0).trunc() / 10000.0,
lon: (lon * 10000.0).trunc() / 10000.0,
alt,
last_response: last_response.into(),
})
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Response {
expires_at: DateTime<FixedOffset>,
last_modified: Box<str>,
pub(crate) raw_body: Box<str>,
}
impl Response {
pub(crate) fn new(
expires_at: DateTime<FixedOffset>,
last_modified: Box<str>,
raw_body: Box<str>,
) -> Self {
Self {
expires_at,
last_modified,
raw_body,
}
}
pub fn expires_at(&self) -> &DateTime<FixedOffset> {
&self.expires_at
}
pub fn last_modified(&self) -> &str {
&self.last_modified
}
pub fn body(&self) -> Result<Body<'_>> {
serde_json::from_str::<Body>(&self.raw_body).map_err(Into::into)
}
}
#[derive(Debug, Clone)]
pub struct Monsoon {
client: Client,
}
impl Monsoon {
pub fn new(user_agent: impl Into<Cow<'static, str>>) -> Result<Self> {
let client = Client::new(user_agent.into())?;
Ok(Self { client })
}
pub async fn get(&self, lat: f64, lon: f64) -> Result<Response> {
self.get_with_params(Params::new(lat, lon, None)?).await
}
pub async fn get_with_altitude(&self, lat: f64, lon: f64, alt: i32) -> Result<Response> {
self.get_with_params(Params::new(lat, lon, alt)?).await
}
pub async fn get_with_params(&self, params: Params) -> Result<Response> {
self.client.get(params).await
}
}
impl Service<Params> for Monsoon {
type Response = Response;
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response>> + Send>>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, params: Params) -> Self::Future {
let clone = self.clone();
Box::pin(async move { clone.get_with_params(params).await })
}
}
#[cfg(test)]
mod tests {
mod params {
use crate::Params;
#[test]
fn validates_lat_value() {
for lat in [f64::INFINITY, f64::NEG_INFINITY, -91.0, 91.0] {
assert!(Params::new(lat, 100.0, None).is_err());
}
for lat in [-90.0, 90.0, 42.0] {
assert!(Params::new(lat, 100.0, None).is_ok());
}
}
#[test]
fn validates_lon_value() {
for lat in [f64::INFINITY, f64::NEG_INFINITY, -181.0, 181.0] {
assert!(Params::new(50.0, lat, None).is_err());
}
for lat in [-180.0, 180.0, 42.0] {
assert!(Params::new(50.0, lat, None).is_ok());
}
}
#[test]
fn validates_alt_value() {
for alt in [-501, 9001] {
assert!(Params::new(50.0, 42.0, alt).is_err());
}
for alt in [-500, 9000, 42] {
assert!(Params::new(50.0, 42.0, alt).is_ok());
}
}
#[test]
fn truncates_values() {
let params = Params::new(14.1234567, 12.7654321, None).unwrap();
assert_eq!(params.lat, 14.1234);
assert_eq!(params.lon, 12.7654);
}
}
}