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::{json, Value};
use std::collections::HashMap;
use ureq::{Error, Request, Response};
pub mod accounts;
pub mod certificates;
pub mod contacts;
pub mod domains;
pub mod domains_collaborators;
pub mod domains_dnssec;
pub mod domains_email_forwards;
pub mod domains_push;
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_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 = "0.2.0";
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,
pub _agent: ureq::Agent,
}
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) -> Client {
let mut url = DEFAULT_BASE_URL;
if sandbox {
url = DEFAULT_SANDBOX_URL;
}
Client {
base_url: String::from(url),
user_agent: DEFAULT_USER_AGENT.to_owned() + VERSION,
auth_token: token,
_agent: ureq::Agent::new(),
}
}
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 versioned_url(&self) -> String {
let mut url = String::from(&self.base_url);
url.push('/');
url.push_str(API_VERSION);
url
}
pub fn get<E: Endpoint>(
&self,
path: &str,
options: Option<RequestOptions>,
) -> Result<DNSimpleResponse<E::Output>, DNSimpleError> {
self.call::<E>(self.build_get_request(&path, options))
}
pub fn post<E: Endpoint>(
&self,
path: &str,
data: Value,
) -> Result<DNSimpleResponse<<E as Endpoint>::Output>, DNSimpleError> {
self.call_with_payload::<E>(self.build_post_request(&path), data)
}
pub fn empty_post(&self, path: &str) -> Result<DNSimpleEmptyResponse, DNSimpleError> {
self.call_empty(self.build_post_request(&path))
}
pub fn put<E: Endpoint>(
&self,
path: &str,
data: Value,
) -> Result<DNSimpleResponse<<E as Endpoint>::Output>, DNSimpleError> {
self.call_with_payload::<E>(self.build_put_request(&path), data)
}
pub fn empty_put(&self, path: &str) -> Result<DNSimpleEmptyResponse, DNSimpleError> {
self.call_empty(self.build_put_request(&path))
}
pub fn patch<E: Endpoint>(
&self,
path: &str,
data: Value,
) -> Result<DNSimpleResponse<<E as Endpoint>::Output>, DNSimpleError> {
self.call_with_payload::<E>(self.build_patch_request(&path), data)
}
pub fn delete(&self, path: &str) -> Result<DNSimpleEmptyResponse, DNSimpleError> {
self.call_empty(self.build_delete_request(&path))
}
pub fn delete_with_response<E: Endpoint>(
&self,
path: &str,
) -> Result<DNSimpleResponse<E::Output>, DNSimpleError> {
self.call::<E>(self.build_delete_request(&path))
}
fn call_with_payload<E: Endpoint>(
&self,
request: Request,
data: Value,
) -> Result<DNSimpleResponse<E::Output>, DNSimpleError> {
self.process_response::<E>(request.send_json(data))
}
fn call<E: Endpoint>(
&self,
request: Request,
) -> Result<DNSimpleResponse<E::Output>, DNSimpleError> {
self.process_response::<E>(request.call())
}
fn process_response<E: Endpoint>(
&self,
result: Result<Response, Error>,
) -> Result<DNSimpleResponse<E::Output>, DNSimpleError> {
match result {
Ok(response) => Self::build_dnsimple_response::<E>(response),
Err(Error::Status(code, response)) => {
Err(DNSimpleError::parse_response(code, response))
}
Err(Error::Transport(transport)) => Err(DNSimpleError::parse_transport(transport)),
}
}
fn call_empty(&self, request: Request) -> Result<DNSimpleEmptyResponse, DNSimpleError> {
match request.call() {
Ok(response) => Self::build_empty_dnsimple_response(response),
Err(Error::Status(code, response)) => {
Err(DNSimpleError::parse_response(code, response))
}
Err(Error::Transport(transport)) => Err(DNSimpleError::parse_transport(transport)),
}
}
fn build_dnsimple_response<E: Endpoint>(
resp: 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();
let json = resp
.into_json::<Value>()
.map_err(|e| DNSimpleError::Deserialization(e.to_string()))?;
let data = serde_json::from_value(json!(json.get("data")))
.map_err(|e| DNSimpleError::Deserialization(e.to_string()))?;
let pagination = serde_json::from_value(json!(json.get("pagination")))
.map_err(|e| DNSimpleError::Deserialization(e.to_string()))?;
let body = serde_json::from_value(json)
.map_err(|e| DNSimpleError::Deserialization(e.to_string()))?;
Ok(DNSimpleResponse {
rate_limit,
rate_limit_remaining,
rate_limit_reset,
status,
data,
pagination,
body,
})
}
fn extract_rate_limit_reset_header(resp: &Response) -> Result<String, DNSimpleError> {
match resp.header("X-RateLimit-Reset") {
Some(header) => Ok(header.to_string()),
None => Err(DNSimpleError::Deserialization(String::from(
"Cannot parse the X-RateLimit-Reset header",
))),
}
}
fn extract_rate_limit_remaining_header(resp: &Response) -> Result<String, DNSimpleError> {
match resp.header("X-RateLimit-Remaining") {
Some(header) => Ok(header.to_string()),
None => Err(DNSimpleError::Deserialization(String::from(
"Cannot parse the X-RateLimit-Remaining header",
))),
}
}
fn extract_rate_limit_limit_header(resp: &Response) -> Result<String, DNSimpleError> {
match resp.header("X-RateLimit-Limit") {
Some(header) => Ok(header.to_string()),
None => Err(DNSimpleError::Deserialization(String::from(
"Cannot parse the X-RateLimit-Limit header",
))),
}
}
fn build_empty_dnsimple_response(
response: 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(),
})
}
fn build_get_request(&self, path: &&str, options: Option<RequestOptions>) -> Request {
let mut request = self
._agent
.get(&*self.url(path))
.set("User-Agent", &self.user_agent)
.set("Accept", "application/json");
if let Some(options) = options {
if let Some(pagination) = options.paginate {
request = request.query("page", &*pagination.page.to_string());
request = request.query("per_page", &*pagination.per_page.to_string())
}
if let Some(filters) = options.filters {
for (key, value) in filters.filters {
request = request.query(&*key, &*value);
}
}
if let Some(sort) = options.sort {
request = request.query("sort", &*sort.sort_by);
}
}
self.add_headers_to_request(request)
}
pub fn build_post_request(&self, path: &&str) -> Request {
let request = self
._agent
.post(&self.url(path))
.set("User-Agent", &self.user_agent)
.set("Accept", "application/json");
self.add_headers_to_request(request)
}
pub fn build_put_request(&self, path: &&str) -> Request {
let request = self
._agent
.put(&self.url(path))
.set("User-Agent", &self.user_agent)
.set("Accept", "application/json");
self.add_headers_to_request(request)
}
pub fn build_patch_request(&self, path: &&str) -> Request {
let request = self
._agent
.request("PATCH", &self.url(path))
.set("User-Agent", &self.user_agent)
.set("Accept", "application/json");
self.add_headers_to_request(request)
}
fn build_delete_request(&self, path: &&str) -> Request {
let request = self
._agent
.delete(&self.url(path))
.set("User-Agent", &self.user_agent)
.set("Accept", "application/json");
self.add_headers_to_request(request)
}
fn add_headers_to_request(&self, request: Request) -> Request {
let auth_token = &format!("Bearer {}", self.auth_token);
request.set("Authorization", auth_token.as_str())
}
fn url(&self, path: &str) -> String {
let mut url = self.versioned_url();
url.push_str(path);
url
}
}
#[cfg(test)]
mod tests {
use crate::dnsimple::{new_client, DEFAULT_SANDBOX_URL, DEFAULT_USER_AGENT, VERSION};
#[test]
fn creates_a_client() {
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);
}
#[test]
fn can_change_the_base_url() {
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");
}
}