unifly-api 0.9.0

Async Rust client, reactive data layer, and domain model for UniFi controller APIs
Documentation
use std::collections::HashMap;

use serde_json::Value;

use crate::integration_types;
use crate::model::common::DataSource;
use crate::model::dns::{DnsPolicy, DnsPolicyType};
use crate::model::entity_id::EntityId;

use super::helpers::origin_from_metadata;

fn dns_value_from_extra(policy_type: DnsPolicyType, extra: &HashMap<String, Value>) -> String {
    match policy_type {
        DnsPolicyType::ARecord => extra
            .get("ipv4Address")
            .and_then(Value::as_str)
            .unwrap_or_default()
            .to_owned(),
        DnsPolicyType::AaaaRecord => extra
            .get("ipv6Address")
            .and_then(Value::as_str)
            .unwrap_or_default()
            .to_owned(),
        DnsPolicyType::CnameRecord => extra
            .get("targetDomain")
            .and_then(Value::as_str)
            .unwrap_or_default()
            .to_owned(),
        DnsPolicyType::MxRecord => extra
            .get("mailServerDomain")
            .and_then(Value::as_str)
            .unwrap_or_default()
            .to_owned(),
        DnsPolicyType::TxtRecord => extra
            .get("text")
            .and_then(Value::as_str)
            .unwrap_or_default()
            .to_owned(),
        DnsPolicyType::SrvRecord => {
            let server = extra
                .get("serverDomain")
                .and_then(Value::as_str)
                .unwrap_or("");
            let service = extra.get("service").and_then(Value::as_str).unwrap_or("");
            let protocol = extra.get("protocol").and_then(Value::as_str).unwrap_or("");
            let port = extra.get("port").and_then(Value::as_u64);
            let priority = extra.get("priority").and_then(Value::as_u64);
            let weight = extra.get("weight").and_then(Value::as_u64);

            let mut parts = Vec::new();
            if !server.is_empty() {
                parts.push(server.to_owned());
            }
            if !service.is_empty() || !protocol.is_empty() {
                parts.push(format!("service={service}{protocol}"));
            }
            if let Some(port) = port {
                parts.push(format!("port={port}"));
            }
            if let Some(priority) = priority {
                parts.push(format!("priority={priority}"));
            }
            if let Some(weight) = weight {
                parts.push(format!("weight={weight}"));
            }
            parts.join(" ")
        }
        DnsPolicyType::ForwardDomain => extra
            .get("ipAddress")
            .and_then(Value::as_str)
            .unwrap_or_default()
            .to_owned(),
    }
}

impl From<integration_types::DnsPolicyResponse> for DnsPolicy {
    fn from(d: integration_types::DnsPolicyResponse) -> Self {
        let policy_type = match d.policy_type.as_str() {
            "A" => DnsPolicyType::ARecord,
            "AAAA" => DnsPolicyType::AaaaRecord,
            "CNAME" => DnsPolicyType::CnameRecord,
            "MX" => DnsPolicyType::MxRecord,
            "TXT" => DnsPolicyType::TxtRecord,
            "SRV" => DnsPolicyType::SrvRecord,
            _ => DnsPolicyType::ForwardDomain,
        };

        DnsPolicy {
            id: EntityId::Uuid(d.id),
            policy_type,
            domain: d.domain.unwrap_or_default(),
            value: dns_value_from_extra(policy_type, &d.extra),
            #[allow(clippy::as_conversions, clippy::cast_possible_truncation)]
            ttl_seconds: d
                .extra
                .get("ttlSeconds")
                .and_then(serde_json::Value::as_u64)
                .map(|t| t as u32),
            origin: origin_from_metadata(&d.metadata),
            source: DataSource::IntegrationApi,
        }
    }
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;

    use super::*;
    use serde_json::json;

    #[test]
    fn integration_dns_policy_uses_type_specific_fields() {
        let response = integration_types::DnsPolicyResponse {
            id: uuid::Uuid::nil(),
            policy_type: "A".into(),
            enabled: true,
            domain: Some("example.com".into()),
            metadata: json!({"origin": "USER"}),
            extra: HashMap::from([
                ("ipv4Address".into(), json!("192.168.1.10")),
                ("ttlSeconds".into(), json!(600)),
            ]),
        };

        let dns = DnsPolicy::from(response);
        assert_eq!(dns.value, "192.168.1.10");
        assert_eq!(dns.ttl_seconds, Some(600));
    }
}