use crate::provider_support::{
CloudflareDnsRecord, CloudflareDnsSettings, CloudflareDnssec, CloudflareZone,
DomainAvailability, RegisteredDomain,
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DnsInventoryCache {
#[serde(default)]
pub cloudflare: Option<CloudflareDnsInventoryCache>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CloudflareDnsInventoryCache {
#[serde(default)]
pub account_id: Option<String>,
#[serde(default)]
pub last_updated_at: Option<DateTime<Utc>>,
#[serde(default)]
pub zones: Vec<CloudflareZone>,
#[serde(default)]
pub records_by_zone: BTreeMap<String, Vec<CloudflareDnsRecord>>,
#[serde(default)]
pub dnssec_by_zone: BTreeMap<String, CloudflareDnssec>,
#[serde(default)]
pub settings_by_zone: BTreeMap<String, CloudflareDnsSettings>,
#[serde(default)]
pub domain_availability_by_name: BTreeMap<String, DomainAvailability>,
#[serde(default)]
pub registered_domains: Vec<RegisteredDomain>,
}
impl DnsInventoryCache {
pub fn record_cloudflare_zones(
&mut self,
account_id: Option<String>,
zones: &[CloudflareZone],
) {
let cache = self.cloudflare_mut();
cache.update_context(account_id);
for zone in zones {
upsert_zone(&mut cache.zones, zone.clone());
}
sort_zones(&mut cache.zones);
}
pub fn record_cloudflare_zone(&mut self, account_id: Option<String>, zone: &CloudflareZone) {
let cache = self.cloudflare_mut();
cache.update_context(account_id);
upsert_zone(&mut cache.zones, zone.clone());
sort_zones(&mut cache.zones);
}
pub fn remove_cloudflare_zone(&mut self, zone_id: &str) {
let cache = self.cloudflare_mut();
cache.touch();
cache.zones.retain(|zone| zone.id != zone_id);
cache.records_by_zone.remove(zone_id);
cache.dnssec_by_zone.remove(zone_id);
cache.settings_by_zone.remove(zone_id);
}
pub fn record_cloudflare_records(
&mut self,
account_id: Option<String>,
zone_id: &str,
records: &[CloudflareDnsRecord],
) {
let cache = self.cloudflare_mut();
cache.update_context(account_id);
let mut next = records.to_vec();
sort_records(&mut next);
cache.records_by_zone.insert(zone_id.to_string(), next);
}
pub fn record_cloudflare_record(
&mut self,
account_id: Option<String>,
record: &CloudflareDnsRecord,
) {
let cache = self.cloudflare_mut();
cache.update_context(account_id);
let records = cache
.records_by_zone
.entry(record.zone_id.clone())
.or_default();
upsert_record(records, record.clone());
sort_records(records);
}
pub fn remove_cloudflare_record(&mut self, zone_id: &str, record_id: &str) {
let cache = self.cloudflare_mut();
cache.touch();
if let Some(records) = cache.records_by_zone.get_mut(zone_id) {
records.retain(|record| record.id != record_id);
}
}
pub fn record_cloudflare_dnssec(
&mut self,
account_id: Option<String>,
zone_id: &str,
dnssec: &CloudflareDnssec,
) {
let cache = self.cloudflare_mut();
cache.update_context(account_id);
cache
.dnssec_by_zone
.insert(zone_id.to_string(), dnssec.clone());
}
pub fn record_cloudflare_settings(
&mut self,
account_id: Option<String>,
zone_id: &str,
settings: &CloudflareDnsSettings,
) {
let cache = self.cloudflare_mut();
cache.update_context(account_id);
cache
.settings_by_zone
.insert(zone_id.to_string(), settings.clone());
}
pub fn record_cloudflare_domain_availability(
&mut self,
account_id: Option<String>,
domains: &[DomainAvailability],
) {
let cache = self.cloudflare_mut();
cache.update_context(account_id);
for domain in domains {
cache
.domain_availability_by_name
.insert(domain.name.clone(), domain.clone());
}
}
pub fn record_cloudflare_registered_domains(
&mut self,
account_id: Option<String>,
domains: &[RegisteredDomain],
) {
let cache = self.cloudflare_mut();
cache.update_context(account_id);
for domain in domains {
upsert_registered_domain(&mut cache.registered_domains, domain.clone());
}
sort_registered_domains(&mut cache.registered_domains);
}
fn cloudflare_mut(&mut self) -> &mut CloudflareDnsInventoryCache {
self.cloudflare
.get_or_insert_with(CloudflareDnsInventoryCache::default)
}
}
impl CloudflareDnsInventoryCache {
fn touch(&mut self) {
self.last_updated_at = Some(Utc::now());
}
fn update_context(&mut self, account_id: Option<String>) {
if let Some(account_id) = account_id.filter(|value| !value.trim().is_empty()) {
self.account_id = Some(account_id);
}
self.touch();
}
}
fn upsert_zone(zones: &mut Vec<CloudflareZone>, zone: CloudflareZone) {
if let Some(existing) = zones.iter_mut().find(|existing| existing.id == zone.id) {
*existing = zone;
} else {
zones.push(zone);
}
}
fn upsert_record(records: &mut Vec<CloudflareDnsRecord>, record: CloudflareDnsRecord) {
if let Some(existing) = records.iter_mut().find(|existing| existing.id == record.id) {
*existing = record;
} else {
records.push(record);
}
}
fn upsert_registered_domain(domains: &mut Vec<RegisteredDomain>, domain: RegisteredDomain) {
if let Some(existing) = domains
.iter_mut()
.find(|existing| existing.name == domain.name)
{
*existing = domain;
} else {
domains.push(domain);
}
}
fn sort_zones(zones: &mut [CloudflareZone]) {
zones.sort_by(|left, right| {
left.name
.cmp(&right.name)
.then_with(|| left.id.cmp(&right.id))
});
}
fn sort_records(records: &mut [CloudflareDnsRecord]) {
records.sort_by(|left, right| {
left.name
.cmp(&right.name)
.then_with(|| left.record_type.cmp(&right.record_type))
.then_with(|| left.id.cmp(&right.id))
});
}
fn sort_registered_domains(domains: &mut [RegisteredDomain]) {
domains.sort_by(|left, right| left.name.cmp(&right.name));
}
#[cfg(test)]
mod tests {
use super::DnsInventoryCache;
use crate::provider_support::{CloudflareDnsRecord, CloudflareZone, RegisteredDomain};
#[test]
fn record_upserts_zone_and_record_entries() {
let mut cache = DnsInventoryCache::default();
let zone = CloudflareZone {
id: "zone_1".to_string(),
name: "example.com".to_string(),
status: None,
r#type: None,
paused: None,
account: None,
owner: None,
tenant: None,
tenant_unit: None,
plan: None,
name_servers: Vec::new(),
vanity_name_servers: None,
original_dnshost: None,
original_name_servers: None,
original_registrar: None,
activated_on: None,
created_on: "2026-01-01T00:00:00Z".to_string(),
modified_on: "2026-01-01T00:00:00Z".to_string(),
development_mode: None,
cname_suffix: None,
verification_key: None,
permissions: None,
meta: Default::default(),
};
let record = CloudflareDnsRecord {
id: "rec_1".to_string(),
zone_id: "zone_1".to_string(),
zone_name: Some("example.com".to_string()),
name: "api.example.com".to_string(),
record_type: "A".to_string(),
content: Some("127.0.0.1".to_string()),
ttl: Some(1),
proxied: Some(false),
priority: None,
comment: None,
tags: None,
data: None,
settings: None,
meta: None,
proxiable: None,
created_on: None,
modified_on: None,
};
cache.record_cloudflare_zone(Some("acc_1".to_string()), &zone);
cache.record_cloudflare_record(Some("acc_1".to_string()), &record);
cache.record_cloudflare_record(Some("acc_1".to_string()), &record);
let cloudflare = cache.cloudflare.expect("cloudflare cache");
assert_eq!(cloudflare.zones.len(), 1);
assert_eq!(cloudflare.records_by_zone["zone_1"].len(), 1);
assert_eq!(cloudflare.account_id.as_deref(), Some("acc_1"));
}
#[test]
fn registered_domains_upsert_by_name() {
let mut cache = DnsInventoryCache::default();
let domain = RegisteredDomain {
name: "example.com".to_string(),
account_id: Some("acc_1".to_string()),
auto_renew: Some(true),
expires_at: None,
registered_at: None,
registration: None,
workflow: None,
extra: Default::default(),
};
cache.record_cloudflare_registered_domains(None, &[domain.clone(), domain]);
let cloudflare = cache.cloudflare.expect("cloudflare cache");
assert_eq!(cloudflare.registered_domains.len(), 1);
}
}