Skip to main content

unifi/capabilities/
official_network.rs

1//! Builds [`Capability`] entries from the official-API OpenAPI operation
2//! inventory baked into `data/unifi_official_network_v10_3_58.json`.
3
4use serde::Deserialize;
5
6use crate::api::ApiSourceFamily;
7use crate::capabilities::{AuthScope, Capability};
8
9#[derive(Debug, Deserialize)]
10struct Inventory {
11    operations: Vec<Operation>,
12}
13
14#[derive(Debug, Deserialize)]
15struct Operation {
16    method: String,
17    path: String,
18    operation_id: String,
19    summary: String,
20}
21
22/// One [`Capability`] per operation in the bundled OpenAPI inventory.
23///
24/// # Panics
25/// Panics if the bundled inventory JSON fails to parse — see
26/// [`super::all_capabilities`] for why that can only happen from a broken
27/// build, not at runtime.
28#[allow(clippy::expect_used)]
29pub fn capabilities() -> Vec<Capability> {
30    let inventory: Inventory = serde_json::from_str(include_str!(
31        "../../data/unifi_official_network_v10_3_58.json"
32    ))
33    .expect("official UniFi inventory should be valid JSON");
34    inventory
35        .operations
36        .into_iter()
37        .map(|operation| {
38            let mutating = !operation.method.eq_ignore_ascii_case("GET");
39            Capability {
40                action: action_name(&operation.operation_id),
41                title: operation.summary,
42                source: ApiSourceFamily::Official,
43                method: Some(operation.method),
44                path: Some(operation.path),
45                mutating,
46                auth_scope: if mutating {
47                    AuthScope::Admin
48                } else {
49                    AuthScope::Read
50                },
51                verification_mode: Some("contract_ok".to_string()),
52            }
53        })
54        .collect()
55}
56
57/// Maps an OpenAPI `operationId` to this crate's action-name convention:
58/// a curated override for names that read better shortened, otherwise
59/// `official_` + the operation id in `snake_case`.
60pub fn action_name(operation_id: &str) -> String {
61    let override_name = match operation_id {
62        "ConnectorDelete" => Some("official_connector_delete"),
63        "ConnectorGet" => Some("official_connector_get"),
64        "ConnectorPatch" => Some("official_connector_patch"),
65        "ConnectorPost" => Some("official_connector_post"),
66        "ConnectorPut" => Some("official_connector_put"),
67        "getSiteOverviewPage" => Some("official_list_sites"),
68        "getConnectedClientOverviewPage" => Some("official_list_clients"),
69        "getAdoptedDeviceOverviewPage" => Some("official_list_devices"),
70        "getNetworksOverviewPage" => Some("official_list_networks"),
71        "getWifiBroadcastPage" => Some("official_list_wifi"),
72        _ => None,
73    };
74    override_name
75        .map(str::to_string)
76        .unwrap_or_else(|| format!("official_{}", camel_to_snake(operation_id)))
77}
78
79fn camel_to_snake(input: &str) -> String {
80    let mut out = String::new();
81    for (idx, ch) in input.chars().enumerate() {
82        if ch.is_ascii_uppercase() {
83            if idx > 0 {
84                out.push('_');
85            }
86            out.push(ch.to_ascii_lowercase());
87        } else {
88            out.push(ch);
89        }
90    }
91    out
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn capabilities_parses_the_bundled_inventory() {
100        let caps = capabilities();
101
102        assert!(!caps.is_empty());
103        assert!(caps
104            .iter()
105            .all(|cap| cap.source == ApiSourceFamily::Official));
106    }
107
108    #[test]
109    fn action_name_uses_curated_overrides() {
110        assert_eq!(action_name("getSiteOverviewPage"), "official_list_sites");
111    }
112
113    #[test]
114    fn action_name_falls_back_to_snake_case_with_a_prefix() {
115        assert_eq!(action_name("getFooBarBaz"), "official_get_foo_bar_baz");
116    }
117
118    #[test]
119    fn camel_to_snake_does_not_prefix_a_leading_capital_with_an_underscore() {
120        assert_eq!(camel_to_snake("Connector"), "connector");
121    }
122
123    #[test]
124    fn camel_to_snake_splits_on_every_capital() {
125        assert_eq!(
126            camel_to_snake("getWifiBroadcastPage"),
127            "get_wifi_broadcast_page"
128        );
129    }
130}