use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderName, HeaderValue};
use reqwest::{Method, RequestBuilder};
use serde_json::{Map, Value};
use std::fmt;
use std::marker::PhantomData;
pub type ResponseValue<T> = T;
#[derive(Debug, Clone)]
pub struct Error<E = ()> {
message: String,
_marker: PhantomData<E>,
}
impl<E> Error<E> {
fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
_marker: PhantomData,
}
}
}
impl<E> fmt::Display for Error<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl<E: fmt::Debug> std::error::Error for Error<E> {}
#[derive(Clone, Debug)]
pub struct Client {
reqwest_client: reqwest::Client,
base_url: String,
default_headers: HeaderMap,
}
impl Default for Client {
fn default() -> Self {
Self::with_default_base_url()
}
}
impl Client {
pub fn with_default_base_url() -> Self {
Self::from_base_url("https://api.cloudflare.com/client/v4")
}
pub fn from_base_url(base_url: impl Into<String>) -> Self {
Self {
reqwest_client: reqwest::Client::new(),
base_url: base_url.into(),
default_headers: HeaderMap::new(),
}
}
pub fn set_bearer_token(
&mut self,
token: impl AsRef<str>,
) -> Result<(), reqwest::header::InvalidHeaderValue> {
let value = format!("Bearer {}", token.as_ref());
self.default_headers
.insert(AUTHORIZATION, HeaderValue::from_str(&value)?);
Ok(())
}
pub fn set_api_key_auth(
&mut self,
email: impl AsRef<str>,
key: impl AsRef<str>,
) -> Result<(), reqwest::header::InvalidHeaderValue> {
self.default_headers.insert(
HeaderName::from_static("x-auth-email"),
HeaderValue::from_str(email.as_ref())?,
);
self.default_headers.insert(
HeaderName::from_static("x-auth-key"),
HeaderValue::from_str(key.as_ref())?,
);
Ok(())
}
pub fn set_service_key(
&mut self,
key: impl AsRef<str>,
) -> Result<(), reqwest::header::InvalidHeaderValue> {
self.default_headers.insert(
HeaderName::from_static("x-auth-user-service-key"),
HeaderValue::from_str(key.as_ref())?,
);
Ok(())
}
pub fn list_zones(&self) -> ZonesGetBuilder<'_> {
ZonesGetBuilder {
client: self,
account_id: None,
name: None,
status: None,
page: None,
per_page: None,
}
}
pub fn zone(&self) -> Zones0GetBuilder<'_> {
Zones0GetBuilder {
client: self,
zone_id: None,
}
}
pub fn list_dns_records(&self) -> DnsRecordsForAZoneListDnsRecordsBuilder<'_> {
DnsRecordsForAZoneListDnsRecordsBuilder {
client: self,
zone_id: None,
name: None,
page: None,
per_page: None,
}
}
pub fn create_dns_record(&self) -> DnsRecordsForAZoneCreateDnsRecordBuilder<'_> {
DnsRecordsForAZoneCreateDnsRecordBuilder {
client: self,
zone_id: None,
body: None,
}
}
pub fn delete_dns_record(&self) -> DnsRecordsForAZoneDeleteDnsRecordBuilder<'_> {
DnsRecordsForAZoneDeleteDnsRecordBuilder {
client: self,
zone_id: None,
dns_record_id: None,
body: None,
}
}
pub fn list_worker_routes(&self) -> WorkerRoutesListRoutesBuilder<'_> {
WorkerRoutesListRoutesBuilder {
client: self,
zone_id: None,
}
}
pub fn create_worker_route(&self) -> WorkerRoutesCreateRouteBuilder<'_> {
WorkerRoutesCreateRouteBuilder {
client: self,
zone_id: None,
body: None,
}
}
pub fn delete_worker_route(&self) -> WorkerRoutesDeleteRouteBuilder<'_> {
WorkerRoutesDeleteRouteBuilder {
client: self,
zone_id: None,
route_id: None,
body: None,
}
}
fn endpoint(&self, path: &str) -> String {
let base = self.base_url.trim_end_matches('/');
let suffix = path.trim_start_matches('/');
format!("{base}/{suffix}")
}
fn request(&self, method: Method, path: &str) -> RequestBuilder {
self.reqwest_client
.request(method, self.endpoint(path))
.headers(self.default_headers.clone())
}
async fn send_json(&self, request: RequestBuilder) -> Result<ResponseValue<Value>, Error<()>> {
let response = request
.send()
.await
.map_err(|e| Error::new(format!("request failed: {e}")))?;
let status = response.status();
if !status.is_success() {
let body = response
.text()
.await
.unwrap_or_else(|_| "<failed to read response body>".to_string());
return Err(Error::new(format!(
"Cloudflare API error (HTTP {}): {}",
status.as_u16(),
body
)));
}
response
.json::<Value>()
.await
.map_err(|e| Error::new(format!("invalid JSON response: {e}")))
}
}
fn encode_path(value: &str) -> String {
utf8_percent_encode(value, NON_ALPHANUMERIC).to_string()
}
#[derive(Clone, Debug)]
pub struct ZonesGetBuilder<'a> {
client: &'a Client,
account_id: Option<String>,
name: Option<String>,
status: Option<String>,
page: Option<u32>,
per_page: Option<u32>,
}
impl<'a> ZonesGetBuilder<'a> {
pub fn account_id(mut self, value: impl Into<String>) -> Self {
self.account_id = Some(value.into());
self
}
pub fn name(mut self, value: impl Into<String>) -> Self {
self.name = Some(value.into());
self
}
pub fn status(mut self, value: impl Into<String>) -> Self {
self.status = Some(value.into());
self
}
pub fn page(mut self, value: u32) -> Self {
self.page = Some(value);
self
}
pub fn per_page(mut self, value: u32) -> Self {
self.per_page = Some(value);
self
}
pub async fn send(self) -> Result<ResponseValue<Value>, Error<()>> {
let mut request = self.client.request(Method::GET, "/zones");
if let Some(account_id) = self.account_id {
request = request.query(&[("account.id", account_id)]);
}
if let Some(name) = self.name {
request = request.query(&[("name", name)]);
}
if let Some(status) = self.status {
request = request.query(&[("status", status)]);
}
if let Some(page) = self.page {
request = request.query(&[("page", page)]);
}
if let Some(per_page) = self.per_page {
request = request.query(&[("per_page", per_page)]);
}
self.client.send_json(request).await
}
}
#[derive(Clone, Debug)]
pub struct Zones0GetBuilder<'a> {
client: &'a Client,
zone_id: Option<String>,
}
impl<'a> Zones0GetBuilder<'a> {
pub fn zone_id(mut self, value: impl Into<String>) -> Self {
self.zone_id = Some(value.into());
self
}
pub async fn send(self) -> Result<ResponseValue<Value>, Error<()>> {
let zone_id = self
.zone_id
.ok_or_else(|| Error::new("zone_id is required"))?;
let path = format!("/zones/{}", encode_path(&zone_id));
self.client
.send_json(self.client.request(Method::GET, &path))
.await
}
}
#[derive(Clone, Debug)]
pub struct DnsRecordsForAZoneListDnsRecordsBuilder<'a> {
client: &'a Client,
zone_id: Option<String>,
name: Option<String>,
page: Option<u32>,
per_page: Option<u32>,
}
impl<'a> DnsRecordsForAZoneListDnsRecordsBuilder<'a> {
pub fn zone_id(mut self, value: impl Into<String>) -> Self {
self.zone_id = Some(value.into());
self
}
pub fn name(mut self, value: impl Into<String>) -> Self {
self.name = Some(value.into());
self
}
pub fn page(mut self, value: u32) -> Self {
self.page = Some(value);
self
}
pub fn per_page(mut self, value: u32) -> Self {
self.per_page = Some(value);
self
}
pub async fn send(self) -> Result<ResponseValue<Value>, Error<()>> {
let zone_id = self
.zone_id
.ok_or_else(|| Error::new("zone_id is required"))?;
let path = format!("/zones/{}/dns_records", encode_path(&zone_id));
let mut request = self.client.request(Method::GET, &path);
if let Some(name) = self.name {
request = request.query(&[("name", name)]);
}
if let Some(page) = self.page {
request = request.query(&[("page", page)]);
}
if let Some(per_page) = self.per_page {
request = request.query(&[("per_page", per_page)]);
}
self.client.send_json(request).await
}
}
#[derive(Clone, Debug)]
pub struct DnsRecordsForAZoneCreateDnsRecordBuilder<'a> {
client: &'a Client,
zone_id: Option<String>,
body: Option<Map<String, Value>>,
}
impl<'a> DnsRecordsForAZoneCreateDnsRecordBuilder<'a> {
pub fn zone_id(mut self, value: impl Into<String>) -> Self {
self.zone_id = Some(value.into());
self
}
pub fn body(mut self, body: Map<String, Value>) -> Self {
self.body = Some(body);
self
}
pub async fn send(self) -> Result<ResponseValue<Value>, Error<()>> {
let zone_id = self
.zone_id
.ok_or_else(|| Error::new("zone_id is required"))?;
let body = self.body.unwrap_or_default();
let path = format!("/zones/{}/dns_records", encode_path(&zone_id));
self.client
.send_json(self.client.request(Method::POST, &path).json(&body))
.await
}
}
#[derive(Clone, Debug)]
pub struct DnsRecordsForAZoneDeleteDnsRecordBuilder<'a> {
client: &'a Client,
zone_id: Option<String>,
dns_record_id: Option<String>,
body: Option<Map<String, Value>>,
}
impl<'a> DnsRecordsForAZoneDeleteDnsRecordBuilder<'a> {
pub fn zone_id(mut self, value: impl Into<String>) -> Self {
self.zone_id = Some(value.into());
self
}
pub fn dns_record_id(mut self, value: impl Into<String>) -> Self {
self.dns_record_id = Some(value.into());
self
}
pub fn body(mut self, body: Map<String, Value>) -> Self {
self.body = Some(body);
self
}
pub async fn send(self) -> Result<ResponseValue<Value>, Error<()>> {
let zone_id = self
.zone_id
.ok_or_else(|| Error::new("zone_id is required"))?;
let dns_record_id = self
.dns_record_id
.ok_or_else(|| Error::new("dns_record_id is required"))?;
let body = self.body.unwrap_or_default();
let path = format!(
"/zones/{}/dns_records/{}",
encode_path(&zone_id),
encode_path(&dns_record_id)
);
self.client
.send_json(self.client.request(Method::DELETE, &path).json(&body))
.await
}
}
#[derive(Clone, Debug)]
pub struct WorkerRoutesListRoutesBuilder<'a> {
client: &'a Client,
zone_id: Option<String>,
}
impl<'a> WorkerRoutesListRoutesBuilder<'a> {
pub fn zone_id(mut self, value: impl Into<String>) -> Self {
self.zone_id = Some(value.into());
self
}
pub async fn send(self) -> Result<ResponseValue<Value>, Error<()>> {
let zone_id = self
.zone_id
.ok_or_else(|| Error::new("zone_id is required"))?;
let path = format!("/zones/{}/workers/routes", encode_path(&zone_id));
self.client
.send_json(self.client.request(Method::GET, &path))
.await
}
}
#[derive(Clone, Debug)]
pub struct WorkerRoutesCreateRouteBuilder<'a> {
client: &'a Client,
zone_id: Option<String>,
body: Option<Map<String, Value>>,
}
impl<'a> WorkerRoutesCreateRouteBuilder<'a> {
pub fn zone_id(mut self, value: impl Into<String>) -> Self {
self.zone_id = Some(value.into());
self
}
pub fn body(mut self, body: Map<String, Value>) -> Self {
self.body = Some(body);
self
}
pub async fn send(self) -> Result<ResponseValue<Value>, Error<()>> {
let zone_id = self
.zone_id
.ok_or_else(|| Error::new("zone_id is required"))?;
let body = self.body.unwrap_or_default();
let path = format!("/zones/{}/workers/routes", encode_path(&zone_id));
self.client
.send_json(self.client.request(Method::POST, &path).json(&body))
.await
}
}
#[derive(Clone, Debug)]
pub struct WorkerRoutesDeleteRouteBuilder<'a> {
client: &'a Client,
zone_id: Option<String>,
route_id: Option<String>,
body: Option<Map<String, Value>>,
}
impl<'a> WorkerRoutesDeleteRouteBuilder<'a> {
pub fn zone_id(mut self, value: impl Into<String>) -> Self {
self.zone_id = Some(value.into());
self
}
pub fn route_id(mut self, value: impl Into<String>) -> Self {
self.route_id = Some(value.into());
self
}
pub fn body(mut self, body: Map<String, Value>) -> Self {
self.body = Some(body);
self
}
pub async fn send(self) -> Result<ResponseValue<Value>, Error<()>> {
let zone_id = self
.zone_id
.ok_or_else(|| Error::new("zone_id is required"))?;
let route_id = self
.route_id
.ok_or_else(|| Error::new("route_id is required"))?;
let body = self.body.unwrap_or_default();
let path = format!(
"/zones/{}/workers/routes/{}",
encode_path(&zone_id),
encode_path(&route_id)
);
self.client
.send_json(self.client.request(Method::DELETE, &path).json(&body))
.await
}
}