parkright 1.4.0

Wrapper around Parkright Settlement API
Documentation
use std::sync::Arc;

use base64::{Engine, prelude::BASE64_STANDARD};
use reqwest::header::{HeaderMap, HeaderValue};

mod error;
mod settlement;
mod ticket;

pub use {error::Error, settlement::Settlement, ticket::Ticket};

const PROD_URL: &str = "https://parkright.se/";

#[allow(dead_code)]
const TEST_URL: &str = "https://test.parkright.se/";

#[derive(Clone)]
pub struct Client {
    c: reqwest::Client,
    base_url: Arc<String>,
}

impl Client {
    pub fn from_env() -> Self {
        let username =
            std::env::var("PARKRIGHT_USERNAME").expect("env var PARKRIGHT_USERNAME not set");
        let password =
            std::env::var("PARKRIGHT_PASSWORD").expect("env var PARKRIGHT_PASSWORD not set");

        Self::new(username, password)
    }

    pub fn new(username: impl AsRef<str>, password: impl AsRef<str>) -> Self {
        let auth_key =
            BASE64_STANDARD.encode(format!("{}:{}", username.as_ref(), password.as_ref()));
        let auth_header_value = format!("Basic {auth_key}");

        let mut headers = HeaderMap::new();

        headers.insert(
            reqwest::header::AUTHORIZATION,
            HeaderValue::from_str(&auth_header_value).expect("Basc auth header value"),
        );
        headers.insert(
            reqwest::header::ACCEPT,
            HeaderValue::from_static("application/json"),
        );

        let c = reqwest::ClientBuilder::new()
            .user_agent(format!("Parkando Parkright v{}", env!("CARGO_PKG_VERSION")))
            .default_headers(headers)
            .build()
            .expect("building");

        Self {
            c,
            base_url: Arc::new(String::from(PROD_URL)),
        }
    }

    async fn fetch_and_parse<T>(&self, url: &str) -> Result<T, Error>
    where
        T: serde::de::DeserializeOwned,
    {
        let res = self.c.get(url).send().await?;

        let status = res.status();
        if !status.is_success() {
            return Err(Error::NonOkReply {
                status: status.as_u16(),
            });
        }

        let bs = res.bytes().await?;

        match serde_json::from_slice::<T>(&bs) {
            Ok(res) => Ok(res),

            Err(err) => Err(Error::Deserializing { err }),
        }
    }

    pub async fn fetch_settlements(&self) -> Result<Vec<Settlement>, Error> {
        #[derive(serde::Deserialize)]
        struct Wrapper {
            #[serde(rename = "AvailableSettlements")]
            settlements: Vec<Settlement>,
        }

        let url = format!(
            "{}/api/v1/ReviewSettlementCasesOffStreet/availablesettlements?startDate=1900-01-01&endDate=2026-01-01",
            self.base_url.as_str()
        );

        let res = self.fetch_and_parse::<Wrapper>(&url).await?;
        Ok(res.settlements)
    }

    pub async fn fetch_tickets(&self, settlement_ids: &[i64]) -> Result<Vec<Ticket>, Error> {
        if settlement_ids.is_empty() {
            return Ok(vec![]);
        }

        let param_ids = settlement_ids
            .iter()
            .map(|id| id.to_string())
            .collect::<Vec<_>>()
            .join(",");

        let url = format!(
            "{}/api/v1/ReviewSettlementCasesOffStreet/report?settlementIds={}",
            self.base_url.as_str(),
            param_ids
        );

        let res = self.fetch_and_parse::<Vec<Ticket>>(&url).await?;

        Ok(res)
    }
}