use crate::dnsimple::accounts::Accounts;
use crate::dnsimple::certificates::Certificates;
use crate::dnsimple::contacts::Contacts;
use crate::dnsimple::domains::Domains;
use crate::dnsimple::identity::Identity;
use crate::dnsimple::oauth::OAuth;
use crate::dnsimple::registrar::Registrar;
use crate::dnsimple::services::Services;
use crate::dnsimple::templates::Templates;
use crate::dnsimple::tlds::Tlds;
use crate::dnsimple::vanity_name_servers::VanityNameServers;
use crate::dnsimple::webhooks::Webhooks;
use crate::dnsimple::zones::Zones;
use crate::errors::DNSimpleError;
use serde;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
pub mod accounts;
pub mod certificates;
pub mod contacts;
pub mod domains;
pub mod domains_dnssec;
pub mod domains_email_forwards;
pub mod domains_push;
pub mod domains_research;
pub mod domains_signer_records;
pub mod identity;
pub mod oauth;
pub mod registrar;
pub mod registrar_auto_renewal;
pub mod registrar_name_servers;
pub mod registrar_registrant_changes;
pub mod registrar_transfer_lock;
pub mod registrar_whois_privacy;
pub mod services;
pub mod templates;
pub mod tlds;
pub mod vanity_name_servers;
pub mod webhooks;
pub mod zones;
pub mod zones_records;
const VERSION: &str = env!("CARGO_PKG_VERSION");
const DEFAULT_USER_AGENT: &str = "dnsimple-rust/";
const API_VERSION: &str = "v2";
const DEFAULT_BASE_URL: &str = "https://api.dnsimple.com";
const DEFAULT_SANDBOX_URL: &str = "https://api.sandbox.dnsimple.com";
pub struct Client {
base_url: String,
user_agent: String,
auth_token: String,
client: reqwest::Client,
}
pub trait Endpoint {
type Output: DeserializeOwned;
}
#[derive(Debug)]
pub struct DNSimpleResponse<T> {
pub rate_limit: String,
pub rate_limit_remaining: String,
pub rate_limit_reset: String,
pub status: u16,
pub data: Option<T>,
pub pagination: Option<Pagination>,
pub body: Option<Value>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Pagination {
pub current_page: u64,
pub per_page: u64,
pub total_entries: u64,
pub total_pages: u64,
}
pub struct RequestOptions {
pub filters: Option<Filters>,
pub sort: Option<Sort>,
pub paginate: Option<Paginate>,
}
pub struct DNSimpleEmptyResponse {
pub rate_limit: String,
pub rate_limit_remaining: String,
pub rate_limit_reset: String,
pub status: u16,
}
#[derive(Debug)]
pub struct Filters {
pub filters: HashMap<String, String>,
}
impl Filters {
pub fn new(filters: HashMap<String, String>) -> Filters {
Filters { filters }
}
}
#[derive(Debug)]
pub struct Sort {
pub sort_by: String,
}
impl Sort {
pub fn new(sort_by: String) -> Sort {
Sort { sort_by }
}
}
pub struct Paginate {
pub per_page: u32,
pub page: u32,
}
pub fn new_client(sandbox: bool, token: String) -> Result<Client, DNSimpleError> {
let mut url = DEFAULT_BASE_URL;
if sandbox {
url = DEFAULT_SANDBOX_URL;
}
let client = reqwest::Client::builder()
.build()
.map_err(DNSimpleError::from_reqwest)?;
Ok(Client {
base_url: String::from(url),
user_agent: DEFAULT_USER_AGENT.to_owned() + VERSION,
auth_token: token,
client,
})
}
impl Client {
pub fn accounts(&self) -> Accounts<'_> {
Accounts { client: self }
}
pub fn contacts(&self) -> Contacts<'_> {
Contacts { client: self }
}
pub fn certificates(&self) -> Certificates<'_> {
Certificates { client: self }
}
pub fn domains(&self) -> Domains<'_> {
Domains { client: self }
}
pub fn identity(&self) -> Identity<'_> {
Identity { client: self }
}
pub fn oauth(&self) -> OAuth<'_> {
OAuth { client: self }
}
pub fn registrar(&self) -> Registrar<'_> {
Registrar { client: self }
}
pub fn services(&self) -> Services<'_> {
Services { client: self }
}
pub fn templates(&self) -> Templates<'_> {
Templates { client: self }
}
pub fn tlds(&self) -> Tlds<'_> {
Tlds { client: self }
}
pub fn vanity_name_servers(&self) -> VanityNameServers<'_> {
VanityNameServers { client: self }
}
pub fn webhooks(&self) -> Webhooks<'_> {
Webhooks { client: self }
}
pub fn zones(&self) -> Zones<'_> {
Zones { client: self }
}
pub fn set_base_url(&mut self, url: &str) {
self.base_url = String::from(url);
}
pub fn set_user_agent(&mut self, custom_user_agent: &str) {
let custom_user_agent = custom_user_agent.trim();
let default_user_agent = DEFAULT_USER_AGENT.to_owned() + VERSION;
self.user_agent = if custom_user_agent.is_empty() {
default_user_agent
} else {
format!("{} {}", custom_user_agent, default_user_agent)
};
}
pub fn versioned_url(&self) -> String {
let mut url = String::from(&self.base_url);
url.push('/');
url.push_str(API_VERSION);
url
}
pub async fn get<E: Endpoint>(
&self,
path: &str,
options: Option<RequestOptions>,
) -> Result<DNSimpleResponse<E::Output>, DNSimpleError> {
let request = self.build_get_request(path, options);
self.call::<E>(request).await
}
pub async fn post<E: Endpoint>(
&self,
path: &str,
data: Value,
) -> Result<DNSimpleResponse<<E as Endpoint>::Output>, DNSimpleError> {
let request = self.build_post_request(path);
self.call_with_payload::<E>(request, data).await
}
pub async fn empty_post(&self, path: &str) -> Result<DNSimpleEmptyResponse, DNSimpleError> {
let request = self.build_post_request(path);
self.call_empty(request).await
}
pub async fn put<E: Endpoint>(
&self,
path: &str,
data: Value,
) -> Result<DNSimpleResponse<<E as Endpoint>::Output>, DNSimpleError> {
let request = self.build_put_request(path);
self.call_with_payload::<E>(request, data).await
}
pub async fn empty_put(&self, path: &str) -> Result<DNSimpleEmptyResponse, DNSimpleError> {
let request = self.build_put_request(path);
self.call_empty(request).await
}
pub async fn patch<E: Endpoint>(
&self,
path: &str,
data: Value,
) -> Result<DNSimpleResponse<<E as Endpoint>::Output>, DNSimpleError> {
let request = self.build_patch_request(path);
self.call_with_payload::<E>(request, data).await
}
pub async fn delete(&self, path: &str) -> Result<DNSimpleEmptyResponse, DNSimpleError> {
let request = self.build_delete_request(path);
self.call_empty(request).await
}
pub async fn delete_with_response<E: Endpoint>(
&self,
path: &str,
) -> Result<DNSimpleResponse<E::Output>, DNSimpleError> {
let request = self.build_delete_request(path);
self.call::<E>(request).await
}
async fn call_with_payload<E: Endpoint>(
&self,
request: reqwest::RequestBuilder,
data: Value,
) -> Result<DNSimpleResponse<E::Output>, DNSimpleError> {
self.process_response::<E>(request.json(&data).send().await)
.await
}
async fn call<E: Endpoint>(
&self,
request: reqwest::RequestBuilder,
) -> Result<DNSimpleResponse<E::Output>, DNSimpleError> {
self.process_response::<E>(request.send().await).await
}
async fn process_response<E: Endpoint>(
&self,
result: Result<reqwest::Response, reqwest::Error>,
) -> Result<DNSimpleResponse<E::Output>, DNSimpleError> {
match result {
Ok(response) => {
let status = response.status().as_u16();
match response.status().is_success() {
true => Self::build_dnsimple_response::<E>(response).await,
false => {
let body = response.json::<Value>().await.ok();
Err(DNSimpleError::parse_response(status, body))
}
}
}
Err(error) => Err(DNSimpleError::from_reqwest(error)),
}
}
pub(crate) async fn process_direct_response<T: DeserializeOwned>(
&self,
result: Result<reqwest::Response, reqwest::Error>,
) -> Result<T, DNSimpleError> {
match result {
Ok(response) => {
let status = response.status().as_u16();
match response.status().is_success() {
true => response
.json::<T>()
.await
.map_err(DNSimpleError::from_reqwest),
false => {
let body = response.json::<Value>().await.ok();
Err(DNSimpleError::parse_response(status, body))
}
}
}
Err(error) => Err(DNSimpleError::from_reqwest(error)),
}
}
async fn call_empty(
&self,
request: reqwest::RequestBuilder,
) -> Result<DNSimpleEmptyResponse, DNSimpleError> {
match request.send().await {
Ok(response) => {
let status = response.status().as_u16();
match response.status().is_success() {
true => Self::build_empty_dnsimple_response(response).await,
false => {
let body = response.json::<Value>().await.ok();
Err(DNSimpleError::parse_response(status, body))
}
}
}
Err(error) => Err(DNSimpleError::from_reqwest(error)),
}
}
async fn build_dnsimple_response<E: Endpoint>(
resp: reqwest::Response,
) -> Result<DNSimpleResponse<E::Output>, DNSimpleError> {
let rate_limit = Self::extract_rate_limit_limit_header(&resp)?;
let rate_limit_remaining = Self::extract_rate_limit_remaining_header(&resp)?;
let rate_limit_reset = Self::extract_rate_limit_reset_header(&resp)?;
let status = resp.status().as_u16();
if status == 204 {
return Ok(DNSimpleResponse {
rate_limit,
rate_limit_remaining,
rate_limit_reset,
status,
data: None,
pagination: None,
body: None,
});
}
let json = resp
.json::<Value>()
.await
.map_err(|e| DNSimpleError::Deserialization(e.to_string()))?;
let data = json
.get("data")
.cloned()
.map(serde_json::from_value)
.transpose()
.map_err(|e| DNSimpleError::Deserialization(e.to_string()))?;
let pagination = json
.get("pagination")
.cloned()
.map(serde_json::from_value)
.transpose()
.map_err(|e| DNSimpleError::Deserialization(e.to_string()))?;
let body = Some(json);
Ok(DNSimpleResponse {
rate_limit,
rate_limit_remaining,
rate_limit_reset,
status,
data,
pagination,
body,
})
}
fn extract_rate_limit_reset_header(resp: &reqwest::Response) -> Result<String, DNSimpleError> {
match resp.headers().get("X-RateLimit-Reset") {
Some(header) => header.to_str().map(|s| s.to_string()).map_err(|_| {
DNSimpleError::Deserialization(String::from(
"Cannot parse the X-RateLimit-Reset header",
))
}),
None => Err(DNSimpleError::Deserialization(String::from(
"Cannot parse the X-RateLimit-Reset header",
))),
}
}
fn extract_rate_limit_remaining_header(
resp: &reqwest::Response,
) -> Result<String, DNSimpleError> {
match resp.headers().get("X-RateLimit-Remaining") {
Some(header) => header.to_str().map(|s| s.to_string()).map_err(|_| {
DNSimpleError::Deserialization(String::from(
"Cannot parse the X-RateLimit-Remaining header",
))
}),
None => Err(DNSimpleError::Deserialization(String::from(
"Cannot parse the X-RateLimit-Remaining header",
))),
}
}
fn extract_rate_limit_limit_header(resp: &reqwest::Response) -> Result<String, DNSimpleError> {
match resp.headers().get("X-RateLimit-Limit") {
Some(header) => header.to_str().map(|s| s.to_string()).map_err(|_| {
DNSimpleError::Deserialization(String::from(
"Cannot parse the X-RateLimit-Limit header",
))
}),
None => Err(DNSimpleError::Deserialization(String::from(
"Cannot parse the X-RateLimit-Limit header",
))),
}
}
async fn build_empty_dnsimple_response(
response: reqwest::Response,
) -> Result<DNSimpleEmptyResponse, DNSimpleError> {
Ok(DNSimpleEmptyResponse {
rate_limit: Self::extract_rate_limit_limit_header(&response)?,
rate_limit_remaining: Self::extract_rate_limit_remaining_header(&response)?,
rate_limit_reset: Self::extract_rate_limit_reset_header(&response)?,
status: response.status().as_u16(),
})
}
fn build_get_request(
&self,
path: &str,
options: Option<RequestOptions>,
) -> reqwest::RequestBuilder {
let mut query_params: Vec<(String, String)> = Vec::new();
if let Some(options) = options {
if let Some(pagination) = options.paginate {
query_params.push(("page".to_string(), pagination.page.to_string()));
query_params.push(("per_page".to_string(), pagination.per_page.to_string()));
}
if let Some(filters) = options.filters {
for (key, value) in filters.filters {
query_params.push((key, value));
}
}
if let Some(sort) = options.sort {
query_params.push(("sort".to_string(), sort.sort_by));
}
}
let request = self
.client
.get(self.url(path))
.header("User-Agent", &self.user_agent)
.header("Accept", "application/json")
.query(&query_params);
self.add_headers_to_request(request)
}
pub fn build_post_request(&self, path: &str) -> reqwest::RequestBuilder {
let request = self
.client
.post(self.url(path))
.header("User-Agent", &self.user_agent)
.header("Accept", "application/json");
self.add_headers_to_request(request)
}
pub fn build_put_request(&self, path: &str) -> reqwest::RequestBuilder {
let request = self
.client
.put(self.url(path))
.header("User-Agent", &self.user_agent)
.header("Accept", "application/json");
self.add_headers_to_request(request)
}
pub fn build_patch_request(&self, path: &str) -> reqwest::RequestBuilder {
let request = self
.client
.patch(self.url(path))
.header("User-Agent", &self.user_agent)
.header("Accept", "application/json");
self.add_headers_to_request(request)
}
fn build_delete_request(&self, path: &str) -> reqwest::RequestBuilder {
let request = self
.client
.delete(self.url(path))
.header("User-Agent", &self.user_agent)
.header("Accept", "application/json");
self.add_headers_to_request(request)
}
fn add_headers_to_request(&self, request: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
let auth_token = format!("Bearer {}", self.auth_token);
request.header("Authorization", auth_token.as_str())
}
pub fn url(&self, path: &str) -> String {
let mut url = self.versioned_url();
url.push_str(path);
url
}
}
#[cfg(test)]
mod tests {
use crate::dnsimple::{DEFAULT_SANDBOX_URL, DEFAULT_USER_AGENT, VERSION, new_client};
use crate::errors::DNSimpleError;
#[test]
fn creates_a_client() -> Result<(), DNSimpleError> {
let token = "some-auth-token";
let client = new_client(true, String::from(token))?;
assert_eq!(client.base_url, DEFAULT_SANDBOX_URL);
assert_eq!(client.user_agent, DEFAULT_USER_AGENT.to_owned() + VERSION);
assert_eq!(client.auth_token, token);
Ok(())
}
#[test]
fn can_change_the_base_url() -> Result<(), DNSimpleError> {
let mut client = new_client(true, String::from("token"))?;
client.set_base_url("https://example.com");
assert_eq!(client.versioned_url(), "https://example.com/v2");
Ok(())
}
#[test]
fn can_set_a_custom_user_agent() -> Result<(), DNSimpleError> {
let mut client = new_client(true, String::from("token"))?;
client.set_user_agent("my-app/1.0");
assert_eq!(
client.user_agent,
format!("my-app/1.0 {}{}", DEFAULT_USER_AGENT, VERSION)
);
Ok(())
}
#[test]
fn default_user_agent_is_unchanged_when_not_customized() -> Result<(), DNSimpleError> {
let client = new_client(true, String::from("token"))?;
assert_eq!(client.user_agent, DEFAULT_USER_AGENT.to_owned() + VERSION);
Ok(())
}
#[test]
fn empty_custom_user_agent_uses_the_default_user_agent() -> Result<(), DNSimpleError> {
let mut client = new_client(true, String::from("token"))?;
client.set_user_agent(" ");
assert_eq!(client.user_agent, DEFAULT_USER_AGENT.to_owned() + VERSION);
Ok(())
}
}