unifly-api 0.9.0

Async Rust client, reactive data layer, and domain model for UniFi controller APIs
Documentation
use crate::core_error::CoreError;
use crate::model::DnsPolicyType;

pub(in super::super) fn dns_policy_type_name(policy_type: DnsPolicyType) -> &'static str {
    match policy_type {
        DnsPolicyType::ARecord => "A",
        DnsPolicyType::AaaaRecord => "AAAA",
        DnsPolicyType::CnameRecord => "CNAME",
        DnsPolicyType::MxRecord => "MX",
        DnsPolicyType::TxtRecord => "TXT",
        DnsPolicyType::SrvRecord => "SRV",
        DnsPolicyType::ForwardDomain => "FORWARD_DOMAIN",
    }
}

fn dns_policy_type_from_name(policy_type: &str) -> DnsPolicyType {
    match policy_type {
        "A" => DnsPolicyType::ARecord,
        "AAAA" => DnsPolicyType::AaaaRecord,
        "CNAME" => DnsPolicyType::CnameRecord,
        "MX" => DnsPolicyType::MxRecord,
        "TXT" => DnsPolicyType::TxtRecord,
        "SRV" => DnsPolicyType::SrvRecord,
        _ => DnsPolicyType::ForwardDomain,
    }
}

fn validation_failed(message: impl Into<String>) -> CoreError {
    CoreError::ValidationFailed {
        message: message.into(),
    }
}

fn dns_domain_value(
    domain: Option<&str>,
    domains: Option<&[String]>,
    fallback: Option<&str>,
) -> Option<String> {
    domain
        .map(str::to_owned)
        .or_else(|| domains.and_then(|values| values.first().cloned()))
        .or_else(|| {
            fallback
                .filter(|value| !value.is_empty())
                .map(str::to_owned)
        })
}

fn insert_string_field(
    fields: &mut serde_json::Map<String, serde_json::Value>,
    key: &str,
    value: Option<String>,
) {
    if let Some(value) = value {
        fields.insert(key.into(), serde_json::Value::String(value));
    }
}

fn insert_u16_field(
    fields: &mut serde_json::Map<String, serde_json::Value>,
    key: &str,
    value: Option<u16>,
) {
    if let Some(value) = value {
        fields.insert(
            key.into(),
            serde_json::Value::Number(serde_json::Number::from(value)),
        );
    }
}

fn insert_u32_field(
    fields: &mut serde_json::Map<String, serde_json::Value>,
    key: &str,
    value: Option<u32>,
) {
    if let Some(value) = value {
        fields.insert(
            key.into(),
            serde_json::Value::Number(serde_json::Number::from(value)),
        );
    }
}

fn ensure_dns_required_string(
    fields: &serde_json::Map<String, serde_json::Value>,
    key: &str,
    policy_type: DnsPolicyType,
) -> Result<(), CoreError> {
    if fields
        .get(key)
        .and_then(serde_json::Value::as_str)
        .is_some()
    {
        Ok(())
    } else {
        Err(validation_failed(format!(
            "{policy_type:?} DNS policy requires `{key}`"
        )))
    }
}

fn ensure_dns_required_number(
    fields: &serde_json::Map<String, serde_json::Value>,
    key: &str,
    policy_type: DnsPolicyType,
) -> Result<(), CoreError> {
    if fields
        .get(key)
        .and_then(serde_json::Value::as_u64)
        .is_some()
    {
        Ok(())
    } else {
        Err(validation_failed(format!(
            "{policy_type:?} DNS policy requires `{key}`"
        )))
    }
}

fn validate_dns_policy_fields(
    policy_type: DnsPolicyType,
    fields: &serde_json::Map<String, serde_json::Value>,
) -> Result<(), CoreError> {
    ensure_dns_required_string(fields, "domain", policy_type)?;

    match policy_type {
        DnsPolicyType::ARecord => {
            ensure_dns_required_string(fields, "ipv4Address", policy_type)?;
            ensure_dns_required_number(fields, "ttlSeconds", policy_type)?;
        }
        DnsPolicyType::AaaaRecord => {
            ensure_dns_required_string(fields, "ipv6Address", policy_type)?;
            ensure_dns_required_number(fields, "ttlSeconds", policy_type)?;
        }
        DnsPolicyType::CnameRecord => {
            ensure_dns_required_string(fields, "targetDomain", policy_type)?;
            ensure_dns_required_number(fields, "ttlSeconds", policy_type)?;
        }
        DnsPolicyType::MxRecord => {
            ensure_dns_required_string(fields, "mailServerDomain", policy_type)?;
            ensure_dns_required_number(fields, "priority", policy_type)?;
        }
        DnsPolicyType::TxtRecord => {
            ensure_dns_required_string(fields, "text", policy_type)?;
        }
        DnsPolicyType::SrvRecord => {
            for key in ["serverDomain", "service", "protocol"] {
                ensure_dns_required_string(fields, key, policy_type)?;
            }
            for key in ["port", "priority", "weight"] {
                ensure_dns_required_number(fields, key, policy_type)?;
            }
        }
        DnsPolicyType::ForwardDomain => {
            ensure_dns_required_string(fields, "ipAddress", policy_type)?;
        }
    }

    Ok(())
}

pub(in super::super) fn build_create_dns_policy_fields(
    req: &crate::command::CreateDnsPolicyRequest,
) -> Result<serde_json::Map<String, serde_json::Value>, CoreError> {
    let mut fields = serde_json::Map::new();
    let domain = dns_domain_value(
        req.domain.as_deref(),
        req.domains.as_deref(),
        Some(req.name.as_str()),
    )
    .ok_or_else(|| validation_failed("DNS policy requires `domain`"))?;
    fields.insert("domain".into(), serde_json::Value::String(domain));

    match req.policy_type {
        DnsPolicyType::ARecord => {
            insert_string_field(
                &mut fields,
                "ipv4Address",
                req.ipv4_address.clone().or_else(|| req.value.clone()),
            );
            insert_u32_field(&mut fields, "ttlSeconds", req.ttl_seconds);
        }
        DnsPolicyType::AaaaRecord => {
            insert_string_field(
                &mut fields,
                "ipv6Address",
                req.ipv6_address.clone().or_else(|| req.value.clone()),
            );
            insert_u32_field(&mut fields, "ttlSeconds", req.ttl_seconds);
        }
        DnsPolicyType::CnameRecord => {
            insert_string_field(
                &mut fields,
                "targetDomain",
                req.target_domain.clone().or_else(|| req.value.clone()),
            );
            insert_u32_field(&mut fields, "ttlSeconds", req.ttl_seconds);
        }
        DnsPolicyType::MxRecord => {
            insert_string_field(
                &mut fields,
                "mailServerDomain",
                req.mail_server_domain.clone().or_else(|| req.value.clone()),
            );
            insert_u16_field(&mut fields, "priority", req.priority);
        }
        DnsPolicyType::TxtRecord => {
            insert_string_field(
                &mut fields,
                "text",
                req.text.clone().or_else(|| req.value.clone()),
            );
        }
        DnsPolicyType::SrvRecord => {
            insert_string_field(
                &mut fields,
                "serverDomain",
                req.server_domain.clone().or_else(|| req.value.clone()),
            );
            insert_string_field(&mut fields, "service", req.service.clone());
            insert_string_field(&mut fields, "protocol", req.protocol.clone());
            insert_u16_field(&mut fields, "port", req.port);
            insert_u16_field(&mut fields, "priority", req.priority);
            insert_u16_field(&mut fields, "weight", req.weight);
        }
        DnsPolicyType::ForwardDomain => {
            insert_string_field(
                &mut fields,
                "ipAddress",
                req.ip_address
                    .clone()
                    .or_else(|| req.upstream.clone())
                    .or_else(|| req.value.clone()),
            );
        }
    }

    validate_dns_policy_fields(req.policy_type, &fields)?;
    Ok(fields)
}

pub(in super::super) fn build_update_dns_policy_fields(
    existing: &crate::integration_types::DnsPolicyResponse,
    update: &crate::command::UpdateDnsPolicyRequest,
) -> Result<serde_json::Map<String, serde_json::Value>, CoreError> {
    let policy_type = dns_policy_type_from_name(&existing.policy_type);
    let mut fields: serde_json::Map<String, serde_json::Value> =
        existing.extra.clone().into_iter().collect();

    if let Some(domain) = dns_domain_value(
        update.domain.as_deref(),
        update.domains.as_deref(),
        existing.domain.as_deref(),
    ) {
        fields.insert("domain".into(), serde_json::Value::String(domain));
    }

    match policy_type {
        DnsPolicyType::ARecord => {
            insert_string_field(
                &mut fields,
                "ipv4Address",
                update.ipv4_address.clone().or_else(|| update.value.clone()),
            );
            insert_u32_field(&mut fields, "ttlSeconds", update.ttl_seconds);
        }
        DnsPolicyType::AaaaRecord => {
            insert_string_field(
                &mut fields,
                "ipv6Address",
                update.ipv6_address.clone().or_else(|| update.value.clone()),
            );
            insert_u32_field(&mut fields, "ttlSeconds", update.ttl_seconds);
        }
        DnsPolicyType::CnameRecord => {
            insert_string_field(
                &mut fields,
                "targetDomain",
                update
                    .target_domain
                    .clone()
                    .or_else(|| update.value.clone()),
            );
            insert_u32_field(&mut fields, "ttlSeconds", update.ttl_seconds);
        }
        DnsPolicyType::MxRecord => {
            insert_string_field(
                &mut fields,
                "mailServerDomain",
                update
                    .mail_server_domain
                    .clone()
                    .or_else(|| update.value.clone()),
            );
            insert_u16_field(&mut fields, "priority", update.priority);
        }
        DnsPolicyType::TxtRecord => {
            insert_string_field(
                &mut fields,
                "text",
                update.text.clone().or_else(|| update.value.clone()),
            );
        }
        DnsPolicyType::SrvRecord => {
            insert_string_field(
                &mut fields,
                "serverDomain",
                update
                    .server_domain
                    .clone()
                    .or_else(|| update.value.clone()),
            );
            insert_string_field(&mut fields, "service", update.service.clone());
            insert_string_field(&mut fields, "protocol", update.protocol.clone());
            insert_u16_field(&mut fields, "port", update.port);
            insert_u16_field(&mut fields, "priority", update.priority);
            insert_u16_field(&mut fields, "weight", update.weight);
        }
        DnsPolicyType::ForwardDomain => {
            insert_string_field(
                &mut fields,
                "ipAddress",
                update
                    .ip_address
                    .clone()
                    .or_else(|| update.upstream.clone())
                    .or_else(|| update.value.clone()),
            );
        }
    }

    validate_dns_policy_fields(policy_type, &fields)?;
    Ok(fields)
}

#[cfg(test)]
mod tests {
    use super::build_create_dns_policy_fields;
    use crate::command::CreateDnsPolicyRequest;
    use crate::model::DnsPolicyType;
    use serde_json::json;

    #[test]
    fn dns_create_fields_use_type_specific_schema_keys() {
        let fields = build_create_dns_policy_fields(&CreateDnsPolicyRequest {
            name: "example.com".into(),
            policy_type: DnsPolicyType::ARecord,
            enabled: true,
            domain: Some("example.com".into()),
            domains: None,
            upstream: None,
            value: Some("192.168.1.10".into()),
            ttl_seconds: Some(600),
            priority: None,
            ipv4_address: None,
            ipv6_address: None,
            target_domain: None,
            mail_server_domain: None,
            text: None,
            ip_address: None,
            server_domain: None,
            service: None,
            protocol: None,
            port: None,
            weight: None,
        })
        .expect("valid DNS fields");

        assert_eq!(fields.get("domain"), Some(&json!("example.com")));
        assert_eq!(fields.get("ipv4Address"), Some(&json!("192.168.1.10")));
        assert_eq!(fields.get("ttlSeconds"), Some(&json!(600)));
        assert!(fields.get("value").is_none());
        assert!(fields.get("ttl").is_none());
    }
}