1use serde_json::{Value, json};
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() { json!({}) } else { value }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn resolve_defaults_to_internal_without_a_site_id() {
85 let (target, params) = resolve("list_clients", &json!({})).unwrap();
86
87 assert_eq!(target, "clients");
88 assert_eq!(params, json!({}));
89 }
90
91 #[test]
92 fn resolve_prefers_official_when_a_site_id_is_present() {
93 let (target, _) = resolve("list_clients", &json!({ "siteId": "abc" })).unwrap();
94
95 assert_eq!(target, "official_list_clients");
96 }
97
98 #[test]
99 fn resolve_honors_an_explicit_preference_over_the_site_id_heuristic() {
100 let (target, _) = resolve(
101 "list_clients",
102 &json!({ "siteId": "abc", "prefer": "internal" }),
103 )
104 .unwrap();
105
106 assert_eq!(target, "clients");
107 }
108
109 #[test]
110 fn resolve_strips_the_prefer_field_from_the_forwarded_params() {
111 let (_, params) =
112 resolve("list_clients", &json!({ "prefer": "internal", "limit": 5 })).unwrap();
113
114 assert_eq!(params, json!({ "limit": 5 }));
115 }
116
117 #[test]
118 fn resolve_errors_on_an_unknown_preference() {
119 let err = resolve("list_clients", &json!({ "prefer": "both" })).unwrap_err();
120
121 assert!(
122 matches!(err, UnifiError::HybridRouting(msg) if msg.contains("unknown hybrid preference"))
123 );
124 }
125
126 #[test]
127 fn resolve_errors_on_an_unmapped_action() {
128 let err = resolve("not_a_real_action", &json!({})).unwrap_err();
129
130 assert!(
131 matches!(err, UnifiError::HybridRouting(msg) if msg.contains("unknown hybrid action"))
132 );
133 }
134
135 #[test]
136 fn resolve_errors_on_a_non_string_preference() {
137 let err = resolve("list_clients", &json!({ "prefer": 1 })).unwrap_err();
138
139 assert!(matches!(err, UnifiError::HybridRouting(msg) if msg.contains("must be a string")));
140 }
141
142 #[test]
143 fn resolve_treats_a_null_site_id_as_absent() {
144 let (target, _) = resolve("list_clients", &json!({ "siteId": null })).unwrap();
145
146 assert_eq!(target, "clients");
147 }
148}