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}