lmrc-cloudflare 0.3.16

Cloudflare API client library for the LMRC Stack - comprehensive DNS, zones, and cache management with automatic retry logic
Documentation
//! # Cloudflare Adapter
//!
//! Adapter implementation that wraps the `lmrc-cloudflare` client and implements
//! the `DnsProvider` port trait from `lmrc-ports`.
//!
//! This adapter allows the Cloudflare DNS client to be used interchangeably with
//! other DNS providers in the LMRC Stack hexagonal architecture.

use async_trait::async_trait;
use crate::{CloudflareClient, Error as CloudflareError};
use lmrc_ports::{
    DnsProvider, DnsRecord, DnsRecordRequest, DnsRecordType, PortError, PortResult,
};

/// Cloudflare adapter implementing the DnsProvider port
pub struct CloudflareAdapter {
    client: CloudflareClient,
}

impl CloudflareAdapter {
    /// Create a new Cloudflare adapter with the given client
    pub fn new(client: CloudflareClient) -> Self {
        Self { client }
    }

    /// Create a new Cloudflare adapter from environment variables
    ///
    /// Reads the `CLOUDFLARE_API_TOKEN` environment variable.
    ///
    /// # Errors
    ///
    /// Returns `PortError::AuthenticationFailed` if the API token is missing or invalid
    pub fn from_env() -> PortResult<Self> {
        let api_token = std::env::var("CLOUDFLARE_API_TOKEN").map_err(|_| {
            PortError::AuthenticationFailed(
                "CLOUDFLARE_API_TOKEN environment variable not set".to_string(),
            )
        })?;

        let client = CloudflareClient::new(&api_token)
            .map_err(|e| PortError::AuthenticationFailed(e.to_string()))?;

        Ok(Self::new(client))
    }

    /// Create a new Cloudflare adapter with an API token
    pub fn with_token(api_token: String) -> PortResult<Self> {
        let client = CloudflareClient::new(&api_token)
            .map_err(|e| PortError::AuthenticationFailed(e.to_string()))?;
        Ok(Self::new(client))
    }
}

/// Convert DnsRecordType to Cloudflare RecordType
fn convert_record_type(record_type: &DnsRecordType) -> crate::dns::RecordType {
    match record_type {
        DnsRecordType::A => crate::dns::RecordType::A,
        DnsRecordType::AAAA => crate::dns::RecordType::AAAA,
        DnsRecordType::CNAME => crate::dns::RecordType::CNAME,
        DnsRecordType::MX => crate::dns::RecordType::MX,
        DnsRecordType::TXT => crate::dns::RecordType::TXT,
        DnsRecordType::NS => crate::dns::RecordType::NS,
        DnsRecordType::SRV => crate::dns::RecordType::SRV,
        DnsRecordType::CAA => crate::dns::RecordType::CAA,
    }
}

/// Convert Cloudflare RecordType string to DnsRecordType
fn convert_record_type_from_string(record_type: &str) -> DnsRecordType {
    match record_type.to_uppercase().as_str() {
        "A" => DnsRecordType::A,
        "AAAA" => DnsRecordType::AAAA,
        "CNAME" => DnsRecordType::CNAME,
        "MX" => DnsRecordType::MX,
        "TXT" => DnsRecordType::TXT,
        "NS" => DnsRecordType::NS,
        "SRV" => DnsRecordType::SRV,
        "CAA" => DnsRecordType::CAA,
        _ => DnsRecordType::A, // Default fallback
    }
}

/// Convert Cloudflare DnsRecord to port DnsRecord
fn convert_dns_record(record: crate::dns::DnsRecord, zone_id: &str) -> DnsRecord {
    DnsRecord {
        id: record.id,
        zone_id: zone_id.to_string(),
        record_type: convert_record_type_from_string(&record.record_type),
        name: record.name,
        content: record.content,
        ttl: record.ttl,
        proxied: record.proxied,
        priority: record.priority,
    }
}

/// Convert Cloudflare errors to PortError
fn convert_error(err: CloudflareError) -> PortError {
    match err {
        CloudflareError::Unauthorized(msg) => PortError::AuthenticationFailed(msg),
        CloudflareError::NotFound(message) => PortError::NotFound {
            resource_type: "DNS Record".to_string(),
            resource_id: message,
        },
        CloudflareError::RateLimited { retry_after } => PortError::RateLimitExceeded {
            retry_after_seconds: retry_after,
        },
        CloudflareError::InvalidInput(message) => PortError::InvalidConfiguration(message),
        CloudflareError::Http(e) => PortError::NetworkError(e.to_string()),
        _ => PortError::ProviderError(err.to_string()),
    }
}

#[async_trait]
impl DnsProvider for CloudflareAdapter {
    async fn create_record(
        &self,
        zone_id: &str,
        request: DnsRecordRequest,
    ) -> PortResult<DnsRecord> {
        let mut builder = self
            .client
            .dns()
            .create_record(zone_id)
            .name(&request.name)
            .record_type(convert_record_type(&request.record_type))
            .content(&request.content)
            .proxied(request.proxied);

        if let Some(ttl) = request.ttl {
            builder = builder.ttl(ttl);
        }

        if let Some(priority) = request.priority {
            builder = builder.priority(priority);
        }

        let record = builder.send().await.map_err(convert_error)?;

        Ok(convert_dns_record(record, zone_id))
    }

    async fn list_records(&self, zone_id: &str) -> PortResult<Vec<DnsRecord>> {
        let records = self
            .client
            .dns()
            .list_records(zone_id)
            .send()
            .await
            .map_err(convert_error)?;

        Ok(records
            .into_iter()
            .map(|r| convert_dns_record(r, zone_id))
            .collect())
    }

    async fn get_record(&self, zone_id: &str, record_id: &str) -> PortResult<DnsRecord> {
        let record = self
            .client
            .dns()
            .get_record(zone_id, record_id)
            .await
            .map_err(convert_error)?;

        Ok(convert_dns_record(record, zone_id))
    }

    async fn update_record(
        &self,
        zone_id: &str,
        record_id: &str,
        request: DnsRecordRequest,
    ) -> PortResult<DnsRecord> {
        let mut builder = self
            .client
            .dns()
            .update_record(zone_id, record_id)
            .name(&request.name)
            .record_type(convert_record_type(&request.record_type))
            .content(&request.content)
            .proxied(request.proxied);

        if let Some(ttl) = request.ttl {
            builder = builder.ttl(ttl);
        }

        if let Some(priority) = request.priority {
            builder = builder.priority(priority);
        }

        let record = builder.send().await.map_err(convert_error)?;

        Ok(convert_dns_record(record, zone_id))
    }

    async fn delete_record(&self, zone_id: &str, record_id: &str) -> PortResult<()> {
        self.client
            .dns()
            .delete_record(zone_id, record_id)
            .await
            .map_err(convert_error)?;

        Ok(())
    }

    async fn find_record_by_name(
        &self,
        zone_id: &str,
        name: &str,
    ) -> PortResult<Option<DnsRecord>> {
        let records = self
            .client
            .dns()
            .list_records(zone_id)
            .name(name)
            .send()
            .await
            .map_err(convert_error)?;

        Ok(records
            .into_iter()
            .next()
            .map(|r| convert_dns_record(r, zone_id)))
    }
}