unifi/capabilities/
official_network.rs1use 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#[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
57pub 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}