unifly-api 0.9.0

Async Rust client, reactive data layer, and domain model for UniFi controller APIs
Documentation
use serde_json::Value;

use crate::integration_types;
use crate::model::common::DataSource;
use crate::model::entity_id::EntityId;
use crate::model::hotspot::Voucher;
use crate::model::supporting::TrafficMatchingList;

use super::helpers::parse_iso;

fn traffic_matching_item_to_string(item: &Value) -> Option<String> {
    match item {
        Value::String(value) => Some(value.clone()),
        Value::Object(map) => {
            if let Some(value) = map
                .get("value")
                .and_then(Value::as_str)
                .map(str::to_owned)
                .or_else(|| {
                    map.get("value")
                        .and_then(Value::as_i64)
                        .map(|value| value.to_string())
                })
            {
                return Some(value);
            }

            let start = map.get("start").or_else(|| map.get("startPort"));
            let stop = map.get("stop").or_else(|| map.get("endPort"));
            match (start, stop) {
                (Some(start), Some(stop)) => {
                    let start = start
                        .as_str()
                        .map(str::to_owned)
                        .or_else(|| start.as_i64().map(|value| value.to_string()));
                    let stop = stop
                        .as_str()
                        .map(str::to_owned)
                        .or_else(|| stop.as_i64().map(|value| value.to_string()));
                    match (start, stop) {
                        (Some(start), Some(stop)) => Some(format!("{start}-{stop}")),
                        _ => None,
                    }
                }
                _ => None,
            }
        }
        _ => None,
    }
}

impl From<integration_types::TrafficMatchingListResponse> for TrafficMatchingList {
    fn from(t: integration_types::TrafficMatchingListResponse) -> Self {
        let items = t
            .extra
            .get("items")
            .and_then(|v| v.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(traffic_matching_item_to_string)
                    .collect()
            })
            .unwrap_or_default();

        TrafficMatchingList {
            id: EntityId::Uuid(t.id),
            name: t.name,
            list_type: t.list_type,
            items,
            origin: None,
        }
    }
}

impl From<integration_types::VoucherResponse> for Voucher {
    fn from(v: integration_types::VoucherResponse) -> Self {
        #[allow(
            clippy::as_conversions,
            clippy::cast_possible_truncation,
            clippy::cast_sign_loss
        )]
        Voucher {
            id: EntityId::Uuid(v.id),
            code: v.code,
            name: Some(v.name),
            created_at: parse_iso(&v.created_at),
            activated_at: v.activated_at.as_deref().and_then(parse_iso),
            expires_at: v.expires_at.as_deref().and_then(parse_iso),
            expired: v.expired,
            time_limit_minutes: Some(v.time_limit_minutes as u32),
            data_usage_limit_mb: v.data_usage_limit_m_bytes.map(|b| b as u64),
            authorized_guest_limit: v.authorized_guest_limit.map(|l| l as u32),
            authorized_guest_count: Some(v.authorized_guest_count as u32),
            rx_rate_limit_kbps: v.rx_rate_limit_kbps.map(|r| r as u64),
            tx_rate_limit_kbps: v.tx_rate_limit_kbps.map(|r| r as u64),
            source: DataSource::IntegrationApi,
        }
    }
}

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

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

    #[test]
    fn integration_traffic_matching_list_formats_structured_items() {
        let response = integration_types::TrafficMatchingListResponse {
            id: uuid::Uuid::nil(),
            name: "Ports".into(),
            list_type: "PORT".into(),
            extra: HashMap::from([(
                "items".into(),
                json!([
                    {"type": "PORT_NUMBER", "value": 443},
                    {"type": "PORT_RANGE", "start": 1000, "stop": 2000}
                ]),
            )]),
        };

        let list = TrafficMatchingList::from(response);
        assert_eq!(list.items, vec!["443".to_owned(), "1000-2000".to_owned()]);
    }
}