pdns-cli 0.0.2

Rust client library and CLI for the PowerDNS Authoritative Server API
Documentation
//! the pdns client

// ////////////////////////////////////
// REGISTERING and RE-EXPORTS
// ////////////////////////////////////

// Not all submodules are exported
// - submodules that contain only common public items are not exported and instead
// only their contents are re-exported (e.g.: result, servers)
// This is done to prevent the public API be large and avoid multiple paths to the same object which might be confusing
//
// - submodules that contain common and uncommon types and functions
// have the module exported and the common types rexported (e.g.: error)
// the common type still gets the benefit of shorter path access, and uncommon types require user to go into the submodules
// note in this case the common types are exported via two paths - but there is no way to prevent without breaking up the modules

pub mod error;
pub use error::Error;

mod result;
pub use result::Result;

mod servers;
pub use servers::Server;

// ////////////////////////////////////
//  USE: BRING INTO SCOPE
// ////////////////////////////////////

// use items in current internal crate
use Error::InvalidBaseUrl;

// use items in external crates
use std::fmt;
use url::Url;

// ////////////////////////////////////
//  CONSTANTS
// ///////////////////////////////////

/// the header key whose value is the pdns api key
const AUTH_HEADER_KEY: &str = "X-API-Key";

/// The pdns api client for a single pdns webserver. It should be created with the [`Client::new`] or [`Client::new_with_client`] function.
#[derive(Clone, Debug)]
pub struct Client {
    /// `base_url` is the base url for the pdns server\
    /// api paths will be joined onto this for making requests
    base_url: Url,

    /// `client` is the internal (blocking) reqwest client
    client: reqwest::blocking::Client,
}

impl fmt::Display for Client {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "pdns client with base_url = {}", self.base_url)
    }
}

impl Client {
    /// creates a new [`Client`].
    /// `api_key` is the value for the `X-API-Key` header
    /// `base_url` should be a valid base url with no path (it will be joined with `/api/v1` which will clear its path)
    ///
    /// The underlying client uses the defaults of a [`reqwest::blocking::Client`]
    /// Use [`Client::new_with_client`] instead to specify the internal client
    ///
    /// # Errors
    /// - [`Error::InvalidBaseUrl`] if `base_url` is not a valid base url
    /// - [`Error::InvalidBaseUrl`] if scheme is not `http`, `https`
    /// - [`Error::BadApiKey`] if `api_key` is not a valid header value
    /// - [`Error::Http`] if the internal client could not be made
    /// - [`Error::Url`] if `base_url` could not be parsed
    pub fn new(base_url: impl AsRef<str>, api_key: &str) -> Result<Self> {
        let client = get_internal_reqwest_client(api_key)?;
        let base_url = pdns_api_url(base_url)?;

        Ok(Client { base_url, client })
    }

    /// creates a new [`Client`].
    /// `client` is the underlying client to use
    /// `base_url` should be a valid base url with no path (it will be joined with `/api/v1` which will clear its path)
    /// It is assumed that `client` has a default header for ("X-API-Key", "api key") for authentication
    ///
    /// # Errors
    /// - [`Error::Url`] if `base_url` could not be parsed
    /// - [`Error::InvalidBaseUrl`] if `base_url` is not a valid base url
    /// - [`Error::InvalidBaseUrl`] if scheme is not `http`, `https`
    pub fn new_with_client(
        base_url: impl AsRef<str>,
        client: reqwest::blocking::Client,
    ) -> Result<Self> {
        let base_url = pdns_api_url(base_url)?;
        Ok(Client { base_url, client })
    }

    /// returns the configured base url
    #[must_use = "this is a getter"]
    pub fn base_url(&self) -> &url::Url {
        &self.base_url
    }
}

/// returns the pdns API ready url
/// it will clear the path and replace it with "/api/v1"
///
/// # Errors
/// - [`Error::Url`] if `base_url` could not be parsed
/// - [`Error::InvalidBaseUrl`] if `base_url` is not a valid base url
/// - [`Error::InvalidBaseUrl`] if scheme is not `http`, `https`
fn pdns_api_url(base_url: impl AsRef<str>) -> Result<Url> {
    let base_url = Url::parse(base_url.as_ref())?;
    let base_url = base_url.join("/api/v1/")?;
    validate_base_url(&base_url)?;

    Ok(base_url)
}

/// validates the following (returning [`InvalidBaseUrl`] if not):
/// - url is a base url according to the url package
/// - the scheme is one of `http`, `https`
fn validate_base_url(base_url: &Url) -> Result<bool> {
    if base_url.cannot_be_a_base() {
        return Err(InvalidBaseUrl("scheme is regarded as non base url".into()));
    }

    if !matches!(base_url.scheme(), "http" | "https") {
        return Err(InvalidBaseUrl("scheme must be one of http or https".into()));
    }

    Ok(true)
}

/// returns the internal blocking reqwest client for making HTTP requests
/// `api_key` is specified as a default header
///
/// # Errors
/// - [`Error::BadApiKey`] if `api_key` is not a valid header value
/// - [`Error::Http`] if the internal client could not be made
fn get_internal_reqwest_client(api_key: &str) -> Result<reqwest::blocking::Client> {
    // make the default header (present as part of each request) for api_key
    let mut headers = reqwest::header::HeaderMap::new();
    let mut api_key = reqwest::header::HeaderValue::from_str(api_key)?;
    api_key.set_sensitive(true);
    headers.insert(AUTH_HEADER_KEY, api_key);

    let client = reqwest::blocking::Client::builder()
        .default_headers(headers)
        .build()?;
    Ok(client)
}