Skip to main content

pdns_client/client/
mod.rs

1//! the pdns client
2
3// ////////////////////////////////////
4// REGISTERING and RE-EXPORTS
5// ////////////////////////////////////
6
7// Not all submodules are exported
8// - submodules that contain only common public items are not exported and instead
9// only their contents are re-exported (e.g.: result, servers)
10// This is done to prevent the public API be large and avoid multiple paths to the same object which might be confusing
11//
12// - submodules that contain common and uncommon types and functions
13// have the module exported and the common types rexported (e.g.: error)
14// the common type still gets the benefit of shorter path access, and uncommon types require user to go into the submodules
15// note in this case the common types are exported via two paths - but there is no way to prevent without breaking up the modules
16
17pub mod error;
18pub use error::Error;
19
20mod result;
21pub use result::Result;
22
23mod servers;
24pub use servers::Server;
25
26// ////////////////////////////////////
27//  USE: BRING INTO SCOPE
28// ////////////////////////////////////
29
30// use items in current internal crate
31use Error::InvalidBaseUrl;
32
33// use items in external crates
34use std::fmt;
35use url::Url;
36
37// ////////////////////////////////////
38//  CONSTANTS
39// ///////////////////////////////////
40
41/// the header key whose value is the pdns api key
42const AUTH_HEADER_KEY: &str = "X-API-Key";
43
44/// The pdns api client for a single pdns webserver. It should be created with the [`Client::new`] or [`Client::new_with_client`] function.
45#[derive(Clone, Debug)]
46pub struct Client {
47    /// `base_url` is the base url for the pdns server\
48    /// api paths will be joined onto this for making requests
49    base_url: Url,
50
51    /// `client` is the internal (blocking) reqwest client
52    client: reqwest::blocking::Client,
53}
54
55impl fmt::Display for Client {
56    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
57        write!(f, "pdns client with base_url = {}", self.base_url)
58    }
59}
60
61impl Client {
62    /// creates a new [`Client`].
63    /// `api_key` is the value for the `X-API-Key` header
64    /// `base_url` should be a valid base url with no path (it will be joined with `/api/v1` which will clear its path)
65    ///
66    /// The underlying client uses the defaults of a [`reqwest::blocking::Client`]
67    /// Use [`Client::new_with_client`] instead to specify the internal client
68    ///
69    /// # Errors
70    /// - [`Error::InvalidBaseUrl`] if `base_url` is not a valid base url
71    /// - [`Error::InvalidBaseUrl`] if scheme is not `http`, `https`
72    /// - [`Error::BadApiKey`] if `api_key` is not a valid header value
73    /// - [`Error::Http`] if the internal client could not be made
74    /// - [`Error::Url`] if `base_url` could not be parsed
75    pub fn new(base_url: impl AsRef<str>, api_key: &str) -> Result<Self> {
76        let client = get_internal_reqwest_client(api_key)?;
77        let base_url = pdns_api_url(base_url)?;
78
79        Ok(Client { base_url, client })
80    }
81
82    /// creates a new [`Client`].
83    /// `client` is the underlying client to use
84    /// `base_url` should be a valid base url with no path (it will be joined with `/api/v1` which will clear its path)
85    /// It is assumed that `client` has a default header for ("X-API-Key", "api key") for authentication
86    ///
87    /// # Errors
88    /// - [`Error::Url`] if `base_url` could not be parsed
89    /// - [`Error::InvalidBaseUrl`] if `base_url` is not a valid base url
90    /// - [`Error::InvalidBaseUrl`] if scheme is not `http`, `https`
91    pub fn new_with_client(
92        base_url: impl AsRef<str>,
93        client: reqwest::blocking::Client,
94    ) -> Result<Self> {
95        let base_url = pdns_api_url(base_url)?;
96        Ok(Client { base_url, client })
97    }
98
99    /// returns the configured base url
100    #[must_use = "this is a getter"]
101    pub fn base_url(&self) -> &url::Url {
102        &self.base_url
103    }
104}
105
106/// returns the pdns API ready url
107/// it will clear the path and replace it with "/api/v1"
108///
109/// # Errors
110/// - [`Error::Url`] if `base_url` could not be parsed
111/// - [`Error::InvalidBaseUrl`] if `base_url` is not a valid base url
112/// - [`Error::InvalidBaseUrl`] if scheme is not `http`, `https`
113fn pdns_api_url(base_url: impl AsRef<str>) -> Result<Url> {
114    let base_url = Url::parse(base_url.as_ref())?;
115    let base_url = base_url.join("/api/v1/")?;
116    validate_base_url(&base_url)?;
117
118    Ok(base_url)
119}
120
121/// validates the following (returning [`InvalidBaseUrl`] if not):
122/// - url is a base url according to the url package
123/// - the scheme is one of `http`, `https`
124fn validate_base_url(base_url: &Url) -> Result<bool> {
125    if base_url.cannot_be_a_base() {
126        return Err(InvalidBaseUrl("scheme is regarded as non base url".into()));
127    }
128
129    if !matches!(base_url.scheme(), "http" | "https") {
130        return Err(InvalidBaseUrl("scheme must be one of http or https".into()));
131    }
132
133    Ok(true)
134}
135
136/// returns the internal blocking reqwest client for making HTTP requests
137/// `api_key` is specified as a default header
138///
139/// # Errors
140/// - [`Error::BadApiKey`] if `api_key` is not a valid header value
141/// - [`Error::Http`] if the internal client could not be made
142fn get_internal_reqwest_client(api_key: &str) -> Result<reqwest::blocking::Client> {
143    // make the default header (present as part of each request) for api_key
144    let mut headers = reqwest::header::HeaderMap::new();
145    let mut api_key = reqwest::header::HeaderValue::from_str(api_key)?;
146    api_key.set_sensitive(true);
147    headers.insert(AUTH_HEADER_KEY, api_key);
148
149    let client = reqwest::blocking::Client::builder()
150        .default_headers(headers)
151        .build()?;
152    Ok(client)
153}