watchbird_client 0.1.0

A self-hosted and API-driven uptime monitor
Documentation
use std::fmt::Debug;

use reqwest::{Method, RequestBuilder};
use thiserror::Error;
use url::Url;

mod alerts;
mod watches;

pub type Result<T> = std::result::Result<T, Error>;

#[derive(Debug, Error)]
pub enum Error {
    #[error("reqwest: {0}")]
    Reqwest(#[from] reqwest::Error),
    #[error("serde: {0}")]
    Serde(#[from] serde_json::Error),
    #[error("object owner id does not match. Expected {expected}, was {was}")]
    OwnerRefMismatch { expected: String, was: String },
    #[error("Invalid URL: {0}")]
    UrlParse(#[from] url::ParseError),
}

#[derive(Clone)]
pub struct ApiClient {
    host: Url,
    api_url: Url,
    api_token: String,
    owner_ref: String,
    client: reqwest::Client,
}

impl ApiClient {
    pub fn new(host: Url, api_token: String, owner_ref: String) -> Result<ApiClient> {
        Ok(ApiClient {
            host: host.clone(),
            api_url: host.join("api/v1alpha1/")?,
            api_token,
            owner_ref,
            client: reqwest::Client::new(),
        })
    }

    pub fn api_url(&self) -> &Url {
        &self.api_url
    }
    pub fn owner_ref(&self) -> &str {
        &self.owner_ref
    }

    fn request(&self, method: Method, path: &str) -> RequestBuilder {
        // panic-safety: internal method, path is controlled
        #[allow(clippy::expect_used)]
        self.client
            .request(
                method,
                self.api_url
                    .join(path)
                    .expect("api resource path should be valid"),
            )
            .bearer_auth(&self.api_token)
    }

    pub async fn check_health(&self) -> Result<()> {
        // panic-safety: path is controlled and the base url is checked in new()
        #[allow(clippy::expect_used)]
        let _ = self
            .client
            .get(
                self.host
                    .join("health")
                    .expect("health url path should be valid"),
            )
            .send()
            .await?
            .error_for_status()?;
        Ok(())
    }
}

impl Debug for ApiClient {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let Self {
            host,
            api_url,
            api_token: _api_token,
            owner_ref,
            client,
        } = self;
        f.debug_struct("ApiClient")
            .field("host", host)
            .field("api_url", api_url)
            .field("api_token", &&"XXXXXXXX")
            .field("owner_ref", owner_ref)
            .field("client", client)
            .finish()
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use std::str::FromStr;

    use url::Url;

    #[tokio::test]
    pub async fn parses_to_api_url() -> anyhow::Result<()> {
        let host = Url::from_str("http://somehost:1234")?;
        let client = ApiClient::new(host, "invalid".to_string(), "invalid".to_string())?;
        assert_eq!(
            client.api_url().to_string(),
            "http://somehost:1234/api/v1alpha1/"
        );
        Ok(())
    }

    #[tokio::test]
    pub async fn parses_to_api_url_trailing_slash() -> anyhow::Result<()> {
        let host = Url::from_str("http://somehost:1234/")?;
        let client = ApiClient::new(host, "invalid".to_string(), "invalid".to_string())?;
        assert_eq!(
            client.api_url().to_string(),
            "http://somehost:1234/api/v1alpha1/"
        );
        Ok(())
    }

    #[tokio::test]
    pub async fn parses_to_api_url_subpath() -> anyhow::Result<()> {
        let host = Url::from_str("http://somehost:1234/subpath/")?;
        let client = ApiClient::new(host, "invalid".to_string(), "invalid".to_string())?;
        assert_eq!(
            client.api_url().to_string(),
            "http://somehost:1234/subpath/api/v1alpha1/"
        );
        Ok(())
    }
}