lmrc-cloudflare 0.3.16

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

use crate::client::CloudflareClient;
use crate::error::Result;
use serde::{Deserialize, Deserializer, Serialize};

/// Deserialize null as empty vector
fn deserialize_null_default<'de, D, T>(deserializer: D) -> std::result::Result<Vec<T>, D::Error>
where
    D: Deserializer<'de>,
    T: Deserialize<'de>,
{
    let opt = Option::deserialize(deserializer)?;
    Ok(opt.unwrap_or_default())
}

/// A Cloudflare zone (domain).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Zone {
    /// Zone ID
    pub id: String,

    /// Zone name (domain)
    pub name: String,

    /// Zone status
    pub status: String,

    /// Whether zone is paused
    pub paused: bool,

    /// Zone type (full, partial, etc.)
    #[serde(rename = "type")]
    pub zone_type: String,

    /// Development mode enabled
    pub development_mode: i32,

    /// Name servers assigned to this zone
    #[serde(default, deserialize_with = "deserialize_null_default")]
    pub name_servers: Vec<String>,

    /// Original name servers
    #[serde(default, deserialize_with = "deserialize_null_default")]
    pub original_name_servers: Vec<String>,

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

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

    /// When the zone was activated
    #[serde(skip_serializing_if = "Option::is_none")]
    pub activated_on: Option<String>,
}

/// Service for managing zones.
///
/// Obtain an instance via [`CloudflareClient::zones()`].
#[derive(Clone)]
pub struct ZoneService {
    client: CloudflareClient,
}

impl ZoneService {
    /// Create a new zones service (internal use).
    pub(crate) fn new(client: CloudflareClient) -> Self {
        Self { client }
    }

    /// List all zones in the account.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # use lmrc_cloudflare::CloudflareClient;
    /// # async fn example(client: CloudflareClient) -> Result<(), lmrc_cloudflare::Error> {
    /// let zones = client.zones()
    ///     .list()
    ///     .send()
    ///     .await?;
    ///
    /// for zone in zones {
    ///     println!("{}: {}", zone.name, zone.id);
    /// }
    /// # Ok(())
    /// # }
    /// ```
    pub fn list(&self) -> ListZonesRequest {
        ListZonesRequest {
            service: self.clone(),
            name: None,
            status: None,
            page: None,
            per_page: None,
        }
    }

    /// Get a specific zone by ID.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # use lmrc_cloudflare::CloudflareClient;
    /// # async fn example(client: CloudflareClient) -> Result<(), lmrc_cloudflare::Error> {
    /// let zone = client.zones()
    ///     .get("zone_id")
    ///     .await?;
    /// # Ok(())
    /// # }
    /// ```
    pub async fn get(&self, zone_id: impl Into<String>) -> Result<Zone> {
        let zone_id = zone_id.into();
        let response = self.client.get(&format!("/zones/{}", zone_id)).await?;

        CloudflareClient::handle_response(response).await
    }

    /// Find a zone by name.
    ///
    /// Returns `None` if no zone with the given name is found.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # use lmrc_cloudflare::CloudflareClient;
    /// # async fn example(client: CloudflareClient) -> Result<(), lmrc_cloudflare::Error> {
    /// let zone = client.zones()
    ///     .find_by_name("example.com")
    ///     .await?;
    ///
    /// if let Some(zone) = zone {
    ///     println!("Zone ID: {}", zone.id);
    /// }
    /// # Ok(())
    /// # }
    /// ```
    pub async fn find_by_name(&self, name: impl Into<String>) -> Result<Option<Zone>> {
        let name = name.into();
        let zones = self.list().name(&name).send().await?;

        Ok(zones.into_iter().find(|z| z.name == name))
    }

    /// Get the zone ID for a given zone name.
    ///
    /// This is a convenience method that combines `find_by_name` with extracting the ID.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # use lmrc_cloudflare::CloudflareClient;
    /// # async fn example(client: CloudflareClient) -> Result<(), lmrc_cloudflare::Error> {
    /// let zone_id = client.zones()
    ///     .get_zone_id("example.com")
    ///     .await?;
    /// # Ok(())
    /// # }
    /// ```
    pub async fn get_zone_id(&self, name: impl Into<String>) -> Result<String> {
        let name = name.into();
        let zone = self
            .find_by_name(&name)
            .await?
            .ok_or_else(|| crate::error::Error::NotFound(format!("Zone '{}' not found", name)))?;

        Ok(zone.id)
    }
}

/// Request builder for listing zones.
pub struct ListZonesRequest {
    service: ZoneService,
    name: Option<String>,
    status: Option<String>,
    page: Option<u32>,
    per_page: Option<u32>,
}

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

    /// Filter by status.
    pub fn status(mut self, status: impl Into<String>) -> Self {
        self.status = Some(status.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
    }

    /// Send the request.
    pub async fn send(self) -> Result<Vec<Zone>> {
        let mut params = Vec::new();

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

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

        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()));
        }

        let response = self
            .service
            .client
            .get_with_params("/zones", &params)
            .await?;

        CloudflareClient::handle_response(response).await
    }
}