1use serde_json::{json, Value};
9
10use crate::error::{Result, UnifiError};
11
12pub fn resolve(action: &str, params: &Value) -> Result<(&'static str, Value)> {
19 let prefer = match params.get("prefer") {
20 None => None,
21 Some(Value::String(value)) => Some(value.to_ascii_lowercase()),
22 Some(_) => {
23 return Err(UnifiError::HybridRouting(
24 "hybrid preference must be a string".to_string(),
25 ))
26 }
27 };
28 let has_site_id = params.get("siteId").is_some_and(|value| !value.is_null());
30 let target = match prefer.as_deref() {
31 Some("official") => official_target(action),
32 Some("internal") => internal_target(action),
33 Some(other) => {
34 return Err(UnifiError::HybridRouting(format!(
35 "unknown hybrid preference: {other}"
36 )))
37 }
38 None if has_site_id => official_target(action),
39 None => internal_target(action),
40 };
41 let Some(target) = target else {
42 return Err(UnifiError::HybridRouting(format!(
43 "unknown hybrid action: {action}"
44 )));
45 };
46 Ok((target, normalize_params(params)))
47}
48
49fn official_target(action: &str) -> Option<&'static str> {
50 match action {
51 "list_clients" => Some("official_list_clients"),
52 "list_devices" => Some("official_list_devices"),
53 "list_networks" => Some("official_list_networks"),
54 "list_wifi" => Some("official_list_wifi"),
55 "get_system_info" => Some("official_get_info"),
56 _ => None,
57 }
58}
59
60fn internal_target(action: &str) -> Option<&'static str> {
61 match action {
62 "list_clients" => Some("clients"),
63 "list_devices" => Some("devices"),
64 "list_networks" => Some("unifi_list_networks"),
65 "list_wifi" => Some("wlans"),
66 "get_system_info" => Some("sysinfo"),
67 _ => None,
68 }
69}
70
71fn normalize_params(params: &Value) -> Value {
72 let mut value = params.clone();
73 if let Some(object) = value.as_object_mut() {
74 object.remove("prefer");
75 }
76 if value.is_null() {
77 json!({})
78 } else {
79 value
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[test]
88 fn resolve_defaults_to_internal_without_a_site_id() {
89 let (target, params) = resolve("list_clients", &json!({})).unwrap();
90
91 assert_eq!(target, "clients");
92 assert_eq!(params, json!({}));
93 }
94
95 #[test]
96 fn resolve_prefers_official_when_a_site_id_is_present() {
97 let (target, _) = resolve("list_clients", &json!({ "siteId": "abc" })).unwrap();
98
99 assert_eq!(target, "official_list_clients");
100 }
101
102 #[test]
103 fn resolve_honors_an_explicit_preference_over_the_site_id_heuristic() {
104 let (target, _) = resolve(
105 "list_clients",
106 &json!({ "siteId": "abc", "prefer": "internal" }),
107 )
108 .unwrap();
109
110 assert_eq!(target, "clients");
111 }
112
113 #[test]
114 fn resolve_strips_the_prefer_field_from_the_forwarded_params() {
115 let (_, params) =
116 resolve("list_clients", &json!({ "prefer": "internal", "limit": 5 })).unwrap();
117
118 assert_eq!(params, json!({ "limit": 5 }));
119 }
120
121 #[test]
122 fn resolve_errors_on_an_unknown_preference() {
123 let err = resolve("list_clients", &json!({ "prefer": "both" })).unwrap_err();
124
125 assert!(
126 matches!(err, UnifiError::HybridRouting(msg) if msg.contains("unknown hybrid preference"))
127 );
128 }
129
130 #[test]
131 fn resolve_errors_on_an_unmapped_action() {
132 let err = resolve("not_a_real_action", &json!({})).unwrap_err();
133
134 assert!(
135 matches!(err, UnifiError::HybridRouting(msg) if msg.contains("unknown hybrid action"))
136 );
137 }
138
139 #[test]
140 fn resolve_errors_on_a_non_string_preference() {
141 let err = resolve("list_clients", &json!({ "prefer": 1 })).unwrap_err();
142
143 assert!(matches!(err, UnifiError::HybridRouting(msg) if msg.contains("must be a string")));
144 }
145
146 #[test]
147 fn resolve_treats_a_null_site_id_as_absent() {
148 let (target, _) = resolve("list_clients", &json!({ "siteId": null })).unwrap();
149
150 assert_eq!(target, "clients");
151 }
152}