use std::net::IpAddr;
use reqwest::StatusCode;
use serde::Deserialize;
use serde_json::Value as JsonValue;
use url::Url;
use crate::domain::{Domain, Root};
use crate::record::{self, Content, Record, Type};
use crate::{ApiError, ClientBuilderError, ClientError, Payload};
pub struct NoApikey;
pub struct Apikey(String);
pub struct NoSecretapikey;
pub struct Secretapikey(String);
pub struct ClientBuilder<A, S> {
endpoint: Option<Url>,
apikey: A,
secretapikey: S,
}
impl ClientBuilder<NoApikey, NoSecretapikey> {
fn new() -> Self {
Self {
endpoint: None,
apikey: NoApikey,
secretapikey: NoSecretapikey,
}
}
}
impl<A, S> ClientBuilder<A, S> {
#[must_use]
pub fn endpoint(mut self, endpoint: Url) -> Self {
self.endpoint = Some(endpoint);
self
}
#[must_use]
pub fn endpoint_if_some(mut self, endpoint: Option<Url>) -> Self {
if let Some(endpoint) = endpoint {
self.endpoint = Some(endpoint);
}
self
}
}
impl<S> ClientBuilder<NoApikey, S> {
pub fn apikey(self, apikey: String) -> ClientBuilder<Apikey, S> {
ClientBuilder {
endpoint: self.endpoint,
apikey: Apikey(apikey),
secretapikey: self.secretapikey,
}
}
}
impl<A> ClientBuilder<A, NoSecretapikey> {
pub fn secretapikey(self, secretapikey: String) -> ClientBuilder<A, Secretapikey> {
ClientBuilder {
endpoint: self.endpoint,
apikey: self.apikey,
secretapikey: Secretapikey(secretapikey),
}
}
}
impl ClientBuilder<Apikey, Secretapikey> {
pub fn build(self) -> Result<Client, ClientBuilderError> {
let endpoint = match self.endpoint {
Some(endpoint) => endpoint,
None => "https://api.porkbun.com/api/json/v3/".parse()?,
};
let Apikey(apikey) = self.apikey;
let Secretapikey(secretapikey) = self.secretapikey;
Ok(Client::new(endpoint, apikey, secretapikey))
}
}
pub struct Client {
endpoint: Url,
apikey: String,
secretapikey: String,
#[allow(clippy::struct_field_names, reason = "it's the underlying client")]
client: reqwest::blocking::Client,
}
#[allow(
clippy::missing_errors_doc,
reason = "All the functions can fail with a ClientError"
)]
impl Client {
fn new(endpoint: Url, apikey: String, secretapikey: String) -> Self {
Self {
endpoint,
apikey,
secretapikey,
client: reqwest::blocking::Client::new(),
}
}
#[must_use]
pub fn builder() -> ClientBuilder<NoApikey, NoSecretapikey> {
ClientBuilder::new()
}
fn build_url(&self, path: &[&str]) -> Result<Url, url::ParseError> {
path.iter()
.filter(|p| !p.is_empty())
.try_fold(self.endpoint.clone(), |acc, p| acc.join(&format!("{p}/")))
}
fn send_request<T: for<'de> Deserialize<'de>>(
&self,
url: Url,
payload: Payload,
) -> Result<T, ClientError> {
let resp = self
.client
.post(url)
.json(&JsonValue::from(payload))
.send()?;
if resp.status() != StatusCode::OK {
return Err(ClientError::Porkbun(ApiError::from_blocking_response(resp)));
}
Ok(resp.json()?)
}
fn payload(&self) -> Payload {
Payload::new(&self.apikey, &self.secretapikey)
}
pub fn test_auth(&self) -> Result<IpAddr, ClientError> {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Response {
your_ip: IpAddr,
}
let url = self.build_url(&["ping"])?;
let payload = self.payload();
Ok(self.send_request::<Response>(url, payload)?.your_ip)
}
pub fn create_dns(
&self,
domain: &Domain,
content: &Content,
ttl: Option<i64>,
prio: Option<i64>,
) -> Result<i64, ClientError> {
#[derive(Deserialize)]
struct Response {
#[serde(deserialize_with = "record::deserialize_string_or_t")]
id: i64,
}
let url = self.build_url(&["dns", "create", domain.root()])?;
let payload = self
.payload()
.add("type", content.type_as_str())
.add("content", content.value_to_string())
.add_if_some("name", domain.prefix())
.add_if_some("ttl", ttl)
.add_if_some("prio", prio);
Ok(self.send_request::<Response>(url, payload)?.id)
}
pub fn edit_dns(
&self,
domain: &Domain,
id: i64,
content: &Content,
ttl: Option<i64>,
prio: Option<i64>,
) -> Result<(), ClientError> {
let url = self.build_url(&["dns", "edit", domain.root(), &id.to_string()])?;
let payload = self
.payload()
.add("type", content.type_as_str())
.add("content", content.value_to_string())
.add_if_some("name", domain.prefix())
.add_if_some("ttl", ttl)
.add_if_some("prio", prio);
self.send_request(url, payload)
}
pub fn edit_dns_by_name_type(
&self,
domain: &Domain,
content: &Content,
ttl: Option<i64>,
prio: Option<i64>,
) -> Result<(), ClientError> {
let url = self.build_url(&[
"dns",
"editByNameType",
domain.root(),
content.type_as_str(),
domain.prefix().unwrap_or(""),
])?;
let payload = self
.payload()
.add("content", content.value_to_string())
.add_if_some("ttl", ttl)
.add_if_some("prio", prio);
self.send_request(url, payload)
}
pub fn delete_dns(&self, root: &Root, id: i64) -> Result<(), ClientError> {
let url = self.build_url(&["dns", "delete", root, &id.to_string()])?;
let payload = self.payload();
self.send_request(url, payload)
}
pub fn delete_dns_by_name_type(
&self,
domain: &Domain,
type_: &Type,
) -> Result<(), ClientError> {
let url = self.build_url(&[
"dns",
"deleteByNameType",
domain.root(),
type_.as_str(),
domain.prefix().unwrap_or(""),
])?;
let payload = self.payload();
self.send_request(url, payload)
}
pub fn retrieve_dns(&self, root: &Root, id: Option<i64>) -> Result<Vec<Record>, ClientError> {
#[derive(Deserialize)]
struct Response {
records: Vec<Record>,
}
let url = self.build_url(&[
"dns",
"retrieve",
root,
&id.map_or_else(String::new, |id| id.to_string()),
])?;
let payload = self.payload();
Ok(self.send_request::<Response>(url, payload)?.records)
}
pub fn retrieve_dns_by_name_type(
&self,
domain: &Domain,
type_: &Type,
) -> Result<Vec<Record>, ClientError> {
#[derive(Deserialize)]
struct Response {
records: Vec<Record>,
}
let url = self.build_url(&[
"dns",
"retrieveByNameType",
domain.root(),
type_.as_str(),
domain.prefix().unwrap_or(""),
])?;
let payload = self.payload();
Ok(self.send_request::<Response>(url, payload)?.records)
}
}