unifi/actions/
official.rs1use reqwest::Method;
5use serde_json::{json, Value};
6
7use crate::api::{official::OfficialNetworkApi, path, ApiSourceFamily};
8use crate::capabilities::Capability;
9use crate::error::{Result, UnifiError};
10use crate::UnifiClient;
11
12const CONNECTOR_PREFIXES: &[&str] = &["/proxy/network/integration/", "/proxy/protect/integration/"];
13
14pub async fn execute(
21 client: &UnifiClient,
22 capability: &Capability,
23 params: &Value,
24) -> Result<Value> {
25 if capability.source != ApiSourceFamily::Official {
26 return Err(UnifiError::InvalidRequest {
27 context: capability.action.clone(),
28 message: "not an official API action".to_string(),
29 });
30 }
31 let path_template = capability
32 .path
33 .as_deref()
34 .ok_or_else(|| UnifiError::InvalidRequest {
35 context: capability.action.clone(),
36 message: "official action has no path configured".to_string(),
37 })?;
38 let method = capability
39 .method
40 .as_deref()
41 .unwrap_or("GET")
42 .parse::<Method>()
43 .map_err(|_| UnifiError::InvalidRequest {
44 context: capability.action.clone(),
45 message: format!("invalid HTTP method {:?}", capability.method),
46 })?;
47 let mut effective_params = params.clone();
48 normalize_official_request(capability.action.as_str(), &mut effective_params);
49 let path = path::substitute_path(path_template, &effective_params, CONNECTOR_PREFIXES)?;
50 let api = OfficialNetworkApi::new(&client.url);
51 let full_path = api.path(&path);
52 client
53 .request_json(
54 capability.action.as_str(),
55 method,
56 &full_path,
57 effective_params.get("query"),
58 effective_params.get("body"),
59 )
60 .await
61}
62
63#[allow(clippy::expect_used)]
71fn normalize_official_request(action: &str, params: &mut Value) {
72 if action != "official_get_firewall_policy_ordering" {
73 return;
74 }
75 let Some(zone_id) = params
76 .get("query")
77 .and_then(|query| query.get("firewallZoneId"))
78 .cloned()
79 else {
80 return;
81 };
82 if !params.is_object() {
83 *params = json!({});
84 }
85 let object = params
87 .as_object_mut()
88 .expect("params was just made an object");
89 let query = object.entry("query").or_insert_with(|| json!({}));
90 if !query.is_object() {
91 return;
92 }
93 let query = query
94 .as_object_mut()
95 .expect("query.is_object() was just checked");
96 query
97 .entry("sourceFirewallZoneId".to_string())
98 .or_insert_with(|| zone_id.clone());
99 query
100 .entry("destinationFirewallZoneId".to_string())
101 .or_insert(zone_id);
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn normalize_official_request_fills_both_zone_ids_from_query() {
110 let mut params = json!({ "query": { "firewallZoneId": "zone-1" } });
111
112 normalize_official_request("official_get_firewall_policy_ordering", &mut params);
113
114 assert_eq!(
115 params,
116 json!({
117 "query": {
118 "firewallZoneId": "zone-1",
119 "sourceFirewallZoneId": "zone-1",
120 "destinationFirewallZoneId": "zone-1",
121 }
122 })
123 );
124 }
125
126 #[test]
127 fn normalize_official_request_ignores_other_actions() {
128 let mut params = json!({ "query": { "firewallZoneId": "zone-1" } });
129
130 normalize_official_request("official_list_devices", &mut params);
131
132 assert_eq!(params, json!({ "query": { "firewallZoneId": "zone-1" } }));
133 }
134
135 #[test]
136 fn normalize_official_request_is_a_no_op_without_a_zone_id() {
137 let mut params = json!({});
138
139 normalize_official_request("official_get_firewall_policy_ordering", &mut params);
140
141 assert_eq!(params, json!({}));
142 }
143}