lmrc-cloudflare 0.3.16

Cloudflare API client library for the LMRC Stack - comprehensive DNS, zones, and cache management with automatic retry logic
Documentation
//! DNS record types and structures.

use serde::{Deserialize, Serialize};

/// DNS record type.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RecordType {
    /// IPv4 address
    A,
    /// IPv6 address
    AAAA,
    /// Canonical name
    CNAME,
    /// Mail exchange
    MX,
    /// Text record
    TXT,
    /// Service locator
    SRV,
    /// Name server
    NS,
    /// Certificate authority authorization
    CAA,
    /// Pointer record
    PTR,
    /// DNSSEC public key
    DNSKEY,
    /// Delegation signer
    DS,
    /// HTTPS service binding
    HTTPS,
    /// Location information
    LOC,
    /// Naming authority pointer
    NAPTR,
    /// S/MIME certificate association
    SMIMEA,
    /// SSH public key fingerprint
    SSHFP,
    /// Service binding
    SVCB,
    /// TLS certificate association
    TLSA,
    /// Uniform Resource Identifier
    URI,
}

impl RecordType {
    /// Convert to string representation
    pub fn as_str(&self) -> &'static str {
        match self {
            RecordType::A => "A",
            RecordType::AAAA => "AAAA",
            RecordType::CNAME => "CNAME",
            RecordType::MX => "MX",
            RecordType::TXT => "TXT",
            RecordType::SRV => "SRV",
            RecordType::NS => "NS",
            RecordType::CAA => "CAA",
            RecordType::PTR => "PTR",
            RecordType::DNSKEY => "DNSKEY",
            RecordType::DS => "DS",
            RecordType::HTTPS => "HTTPS",
            RecordType::LOC => "LOC",
            RecordType::NAPTR => "NAPTR",
            RecordType::SMIMEA => "SMIMEA",
            RecordType::SSHFP => "SSHFP",
            RecordType::SVCB => "SVCB",
            RecordType::TLSA => "TLSA",
            RecordType::URI => "URI",
        }
    }
}

impl std::fmt::Display for RecordType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

/// A DNS record in Cloudflare.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DnsRecord {
    /// Record ID
    pub id: String,

    /// Record type
    #[serde(rename = "type")]
    pub record_type: String,

    /// Record name (e.g., "example.com" or "subdomain.example.com")
    pub name: String,

    /// Record content/value
    pub content: String,

    /// Whether the record is proxied through Cloudflare
    pub proxied: bool,

    /// Time to live (1 = automatic)
    pub ttl: u32,

    /// Zone ID this record belongs to
    #[serde(skip_serializing_if = "Option::is_none")]
    pub zone_id: Option<String>,

    /// Zone name this record belongs to
    #[serde(skip_serializing_if = "Option::is_none")]
    pub zone_name: Option<String>,

    /// When the record was created
    #[serde(skip_serializing_if = "Option::is_none")]
    pub created_on: Option<String>,

    /// When the record was last modified
    #[serde(skip_serializing_if = "Option::is_none")]
    pub modified_on: Option<String>,

    /// Comment/note for the record
    #[serde(skip_serializing_if = "Option::is_none")]
    pub comment: Option<String>,

    /// Priority (for MX, SRV records)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub priority: Option<u16>,
}

impl DnsRecord {
    /// Check if this record matches the given criteria
    pub fn matches(&self, name: &str, record_type: RecordType) -> bool {
        self.name == name && self.record_type == record_type.as_str()
    }

    /// Check if the content needs updating
    pub fn needs_update(&self, desired: &DnsRecordBuilder) -> bool {
        if let Some(content) = &desired.content
            && &self.content != content
        {
            return true;
        }

        if let Some(proxied) = desired.proxied
            && self.proxied != proxied
        {
            return true;
        }

        if let Some(ttl) = desired.ttl
            && self.ttl != ttl
        {
            return true;
        }

        false
    }
}

/// Builder for creating DNS records.
#[derive(Debug, Clone, Default)]
pub struct DnsRecordBuilder {
    pub(crate) name: Option<String>,
    pub(crate) record_type: Option<RecordType>,
    pub(crate) content: Option<String>,
    pub(crate) proxied: Option<bool>,
    pub(crate) ttl: Option<u32>,
    pub(crate) comment: Option<String>,
    pub(crate) priority: Option<u16>,
}

impl DnsRecordBuilder {
    /// Create a new DNS record builder
    pub fn new() -> Self {
        Self::default()
    }

    /// Set the record name
    pub fn name(mut self, name: impl Into<String>) -> Self {
        self.name = Some(name.into());
        self
    }

    /// Set the record type
    pub fn record_type(mut self, record_type: RecordType) -> Self {
        self.record_type = Some(record_type);
        self
    }

    /// Set the record content/value
    pub fn content(mut self, content: impl Into<String>) -> Self {
        self.content = Some(content.into());
        self
    }

    /// Set whether the record should be proxied through Cloudflare
    pub fn proxied(mut self, proxied: bool) -> Self {
        self.proxied = Some(proxied);
        self
    }

    /// Set the TTL (1 = automatic)
    pub fn ttl(mut self, ttl: u32) -> Self {
        self.ttl = Some(ttl);
        self
    }

    /// Set a comment for the record
    pub fn comment(mut self, comment: impl Into<String>) -> Self {
        self.comment = Some(comment.into());
        self
    }

    /// Set the priority (for MX, SRV records)
    pub fn priority(mut self, priority: u16) -> Self {
        self.priority = Some(priority);
        self
    }

    /// Build the JSON payload for API request
    pub(crate) fn build_payload(&self) -> serde_json::Value {
        let mut payload = serde_json::json!({});

        if let Some(name) = &self.name {
            payload["name"] = serde_json::Value::String(name.clone());
        }

        if let Some(record_type) = &self.record_type {
            payload["type"] = serde_json::Value::String(record_type.as_str().to_string());
        }

        if let Some(content) = &self.content {
            payload["content"] = serde_json::Value::String(content.clone());
        }

        if let Some(proxied) = self.proxied {
            payload["proxied"] = serde_json::Value::Bool(proxied);
        }

        if let Some(ttl) = self.ttl {
            payload["ttl"] = serde_json::Value::Number(ttl.into());
        }

        if let Some(comment) = &self.comment {
            payload["comment"] = serde_json::Value::String(comment.clone());
        }

        if let Some(priority) = self.priority {
            payload["priority"] = serde_json::Value::Number(priority.into());
        }

        payload
    }

    /// Validate that all required fields are present
    pub(crate) fn validate_create(&self) -> Result<(), crate::error::Error> {
        if self.name.is_none() {
            return Err(crate::error::Error::InvalidInput(
                "DNS record name is required".to_string(),
            ));
        }

        if self.record_type.is_none() {
            return Err(crate::error::Error::InvalidInput(
                "DNS record type is required".to_string(),
            ));
        }

        if self.content.is_none() {
            return Err(crate::error::Error::InvalidInput(
                "DNS record content is required".to_string(),
            ));
        }

        Ok(())
    }
}

/// Query parameters for listing DNS records.
#[derive(Debug, Clone, Default)]
pub struct ListRecordsQuery {
    pub(crate) record_type: Option<RecordType>,
    pub(crate) name: Option<String>,
    pub(crate) content: Option<String>,
    pub(crate) page: Option<u32>,
    pub(crate) per_page: Option<u32>,
}

impl ListRecordsQuery {
    /// Create a new query builder
    pub fn new() -> Self {
        Self::default()
    }

    /// Filter by record type
    pub fn record_type(mut self, record_type: RecordType) -> Self {
        self.record_type = Some(record_type);
        self
    }

    /// Filter by record name
    pub fn name(mut self, name: impl Into<String>) -> Self {
        self.name = Some(name.into());
        self
    }

    /// Filter by content
    pub fn content(mut self, content: impl Into<String>) -> Self {
        self.content = Some(content.into());
        self
    }

    /// Set page number for pagination
    pub fn page(mut self, page: u32) -> Self {
        self.page = Some(page);
        self
    }

    /// Set number of results per page
    pub fn per_page(mut self, per_page: u32) -> Self {
        self.per_page = Some(per_page);
        self
    }

    /// Build query parameters for URL
    pub(crate) fn build_params(&self) -> Vec<(&str, String)> {
        let mut params = Vec::new();

        if let Some(record_type) = &self.record_type {
            params.push(("type", record_type.as_str().to_string()));
        }

        if let Some(name) = &self.name {
            params.push(("name", name.clone()));
        }

        if let Some(content) = &self.content {
            params.push(("content", content.clone()));
        }

        if let Some(page) = self.page {
            params.push(("page", page.to_string()));
        }

        if let Some(per_page) = self.per_page {
            params.push(("per_page", per_page.to_string()));
        }

        params
    }
}