use crate::client::CloudflareClient;
use crate::error::Result;
use crate::types::Change;
pub mod types;
pub use types::{DnsRecord, DnsRecordBuilder, ListRecordsQuery, RecordType};
#[derive(Clone)]
pub struct DnsService {
client: CloudflareClient,
}
impl DnsService {
pub(crate) fn new(client: CloudflareClient) -> Self {
Self { client }
}
pub fn list_records(&self, zone_id: impl Into<String>) -> ListRecordsRequest {
ListRecordsRequest {
service: self.clone(),
zone_id: zone_id.into(),
query: ListRecordsQuery::new(),
}
}
pub async fn get_record(
&self,
zone_id: impl Into<String>,
record_id: impl Into<String>,
) -> Result<DnsRecord> {
let zone_id = zone_id.into();
let record_id = record_id.into();
let response = self
.client
.get(&format!("/zones/{}/dns_records/{}", zone_id, record_id))
.await?;
CloudflareClient::handle_response(response).await
}
pub fn create_record(&self, zone_id: impl Into<String>) -> CreateRecordRequest {
CreateRecordRequest {
service: self.clone(),
zone_id: zone_id.into(),
builder: DnsRecordBuilder::new(),
}
}
pub fn update_record(
&self,
zone_id: impl Into<String>,
record_id: impl Into<String>,
) -> UpdateRecordRequest {
UpdateRecordRequest {
service: self.clone(),
zone_id: zone_id.into(),
record_id: record_id.into(),
builder: DnsRecordBuilder::new(),
}
}
pub async fn delete_record(
&self,
zone_id: impl Into<String>,
record_id: impl Into<String>,
) -> Result<()> {
let zone_id = zone_id.into();
let record_id = record_id.into();
let response = self
.client
.delete(&format!("/zones/{}/dns_records/{}", zone_id, record_id))
.await?;
let _: serde_json::Value = CloudflareClient::handle_response(response).await?;
Ok(())
}
pub async fn find_record(
&self,
zone_id: impl Into<String>,
name: impl Into<String>,
record_type: RecordType,
) -> Result<Option<DnsRecord>> {
let name = name.into();
let records = self
.list_records(zone_id)
.name(&name)
.record_type(record_type)
.send()
.await?;
Ok(records.into_iter().find(|r| r.matches(&name, record_type)))
}
pub fn sync_records(&self, zone_id: impl Into<String>) -> SyncRecordsRequest {
SyncRecordsRequest {
service: self.clone(),
zone_id: zone_id.into(),
records: Vec::new(),
dry_run: false,
}
}
}
pub struct ListRecordsRequest {
service: DnsService,
zone_id: String,
query: ListRecordsQuery,
}
impl ListRecordsRequest {
pub fn record_type(mut self, record_type: RecordType) -> Self {
self.query = self.query.record_type(record_type);
self
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.query = self.query.name(name);
self
}
pub fn content(mut self, content: impl Into<String>) -> Self {
self.query = self.query.content(content);
self
}
pub fn page(mut self, page: u32) -> Self {
self.query = self.query.page(page);
self
}
pub fn per_page(mut self, per_page: u32) -> Self {
self.query = self.query.per_page(per_page);
self
}
pub async fn send(self) -> Result<Vec<DnsRecord>> {
let params = self.query.build_params();
let response = self
.service
.client
.get_with_params(&format!("/zones/{}/dns_records", self.zone_id), ¶ms)
.await?;
CloudflareClient::handle_response(response).await
}
}
pub struct CreateRecordRequest {
service: DnsService,
zone_id: String,
builder: DnsRecordBuilder,
}
impl CreateRecordRequest {
pub fn name(mut self, name: impl Into<String>) -> Self {
self.builder = self.builder.name(name);
self
}
pub fn record_type(mut self, record_type: RecordType) -> Self {
self.builder = self.builder.record_type(record_type);
self
}
pub fn content(mut self, content: impl Into<String>) -> Self {
self.builder = self.builder.content(content);
self
}
pub fn proxied(mut self, proxied: bool) -> Self {
self.builder = self.builder.proxied(proxied);
self
}
pub fn ttl(mut self, ttl: u32) -> Self {
self.builder = self.builder.ttl(ttl);
self
}
pub fn comment(mut self, comment: impl Into<String>) -> Self {
self.builder = self.builder.comment(comment);
self
}
pub fn priority(mut self, priority: u16) -> Self {
self.builder = self.builder.priority(priority);
self
}
pub async fn send(self) -> Result<DnsRecord> {
self.builder.validate_create()?;
let payload = self.builder.build_payload();
let response = self
.service
.client
.post(&format!("/zones/{}/dns_records", self.zone_id), &payload)
.await?;
CloudflareClient::handle_response(response).await
}
}
pub struct UpdateRecordRequest {
service: DnsService,
zone_id: String,
record_id: String,
builder: DnsRecordBuilder,
}
impl UpdateRecordRequest {
pub fn name(mut self, name: impl Into<String>) -> Self {
self.builder = self.builder.name(name);
self
}
pub fn record_type(mut self, record_type: RecordType) -> Self {
self.builder = self.builder.record_type(record_type);
self
}
pub fn content(mut self, content: impl Into<String>) -> Self {
self.builder = self.builder.content(content);
self
}
pub fn proxied(mut self, proxied: bool) -> Self {
self.builder = self.builder.proxied(proxied);
self
}
pub fn ttl(mut self, ttl: u32) -> Self {
self.builder = self.builder.ttl(ttl);
self
}
pub fn comment(mut self, comment: impl Into<String>) -> Self {
self.builder = self.builder.comment(comment);
self
}
pub fn priority(mut self, priority: u16) -> Self {
self.builder = self.builder.priority(priority);
self
}
pub async fn send(self) -> Result<DnsRecord> {
let payload = self.builder.build_payload();
let response = self
.service
.client
.put(
&format!("/zones/{}/dns_records/{}", self.zone_id, self.record_id),
&payload,
)
.await?;
CloudflareClient::handle_response(response).await
}
}
pub struct SyncRecordsRequest {
service: DnsService,
zone_id: String,
records: Vec<DnsRecordBuilder>,
dry_run: bool,
}
impl SyncRecordsRequest {
pub fn records(mut self, records: Vec<DnsRecordBuilder>) -> Self {
self.records = records;
self
}
pub fn add_record(mut self, record: DnsRecordBuilder) -> Self {
self.records.push(record);
self
}
pub fn dry_run(mut self, dry_run: bool) -> Self {
self.dry_run = dry_run;
self
}
pub async fn send(self) -> Result<Vec<Change<DnsRecord>>> {
let mut changes = Vec::new();
let existing_records = self.service.list_records(&self.zone_id).send().await?;
for desired in &self.records {
desired.validate_create()?;
let name = desired.name.as_ref().unwrap();
let record_type = desired.record_type.unwrap();
let content = desired.content.as_ref().unwrap();
let existing = existing_records
.iter()
.find(|r| r.matches(name, record_type));
match existing {
Some(existing_record) => {
if existing_record.needs_update(desired) {
let change = Change::update(
existing_record.clone(),
existing_record.clone(), format!(
"Update {} record: {} -> {}",
record_type.as_str(),
name,
content
),
);
changes.push(change);
if !self.dry_run {
self.service
.update_record(&self.zone_id, &existing_record.id)
.content(content)
.proxied(desired.proxied.unwrap_or(existing_record.proxied))
.ttl(desired.ttl.unwrap_or(existing_record.ttl))
.send()
.await?;
}
} else {
let change = Change::no_change(
existing_record.clone(),
format!("{} record already correct: {}", record_type.as_str(), name),
);
changes.push(change);
}
}
None => {
let change = Change::create(
DnsRecord {
id: String::new(),
record_type: record_type.as_str().to_string(),
name: name.clone(),
content: content.clone(),
proxied: desired.proxied.unwrap_or(true),
ttl: desired.ttl.unwrap_or(1),
zone_id: Some(self.zone_id.clone()),
zone_name: None,
created_on: None,
modified_on: None,
comment: desired.comment.clone(),
priority: desired.priority,
},
format!(
"Create {} record: {} -> {}",
record_type.as_str(),
name,
content
),
);
changes.push(change);
if !self.dry_run {
self.service
.create_record(&self.zone_id)
.name(name)
.record_type(record_type)
.content(content)
.proxied(desired.proxied.unwrap_or(true))
.ttl(desired.ttl.unwrap_or(1))
.send()
.await?;
}
}
}
}
Ok(changes)
}
}