Skip to main content

unifly_api/controller/query/
reference.rs

1use crate::core_error::CoreError;
2use crate::model::{
3    Country, DpiApplication, DpiCategory, EntityId, RadiusProfile, VpnServer, VpnTunnel,
4    WanInterface,
5};
6
7use super::super::{
8    Controller, integration_client_context, integration_site_context, require_uuid,
9};
10
11impl Controller {
12    pub async fn list_vpn_servers(&self) -> Result<Vec<VpnServer>, CoreError> {
13        let (client, site_id) = integration_site_context(self, "list_vpn_servers").await?;
14        let raw = client
15            .paginate_all(200, |offset, limit| {
16                client.list_vpn_servers(&site_id, offset, limit)
17            })
18            .await?;
19        Ok(raw
20            .into_iter()
21            .map(|server| parse_vpn_server(&server.fields))
22            .collect())
23    }
24
25    pub async fn list_vpn_tunnels(&self) -> Result<Vec<VpnTunnel>, CoreError> {
26        let (client, site_id) = integration_site_context(self, "list_vpn_tunnels").await?;
27        let raw = client
28            .paginate_all(200, |offset, limit| {
29                client.list_vpn_tunnels(&site_id, offset, limit)
30            })
31            .await?;
32        Ok(raw
33            .into_iter()
34            .map(|tunnel| parse_vpn_tunnel(&tunnel.fields))
35            .collect())
36    }
37
38    pub async fn list_wans(&self) -> Result<Vec<WanInterface>, CoreError> {
39        let (client, site_id) = integration_site_context(self, "list_wans").await?;
40        let raw = client
41            .paginate_all(200, |offset, limit| {
42                client.list_wans(&site_id, offset, limit)
43            })
44            .await?;
45        Ok(raw
46            .into_iter()
47            .map(|wan| {
48                let parse_ip = |key: &str| -> Option<std::net::IpAddr> {
49                    wan.fields
50                        .get(key)
51                        .and_then(|value| value.as_str())
52                        .and_then(|value| value.parse().ok())
53                };
54                let dns = wan
55                    .fields
56                    .get("dns")
57                    .and_then(|value| value.as_array())
58                    .map(|values| {
59                        values
60                            .iter()
61                            .filter_map(|value| value.as_str().and_then(|value| value.parse().ok()))
62                            .collect()
63                    })
64                    .unwrap_or_default();
65                WanInterface {
66                    id: parse_integration_entity_id(&wan.fields),
67                    name: wan
68                        .fields
69                        .get("name")
70                        .and_then(|value| value.as_str())
71                        .map(String::from),
72                    ip: parse_ip("ipAddress").or_else(|| parse_ip("ip")),
73                    gateway: parse_ip("gateway"),
74                    dns,
75                }
76            })
77            .collect())
78    }
79
80    pub async fn list_dpi_categories(&self) -> Result<Vec<DpiCategory>, CoreError> {
81        let (client, site_id) = integration_site_context(self, "list_dpi_categories").await?;
82        let raw = client
83            .paginate_all(200, |offset, limit| {
84                client.list_dpi_categories(&site_id, offset, limit)
85            })
86            .await?;
87        Ok(raw
88            .into_iter()
89            .map(|category| {
90                #[allow(clippy::as_conversions, clippy::cast_possible_truncation)]
91                let id = category
92                    .fields
93                    .get("id")
94                    .and_then(serde_json::Value::as_u64)
95                    .unwrap_or(0) as u32;
96                DpiCategory {
97                    id,
98                    name: category
99                        .fields
100                        .get("name")
101                        .and_then(|value| value.as_str())
102                        .unwrap_or("Unknown")
103                        .to_owned(),
104                    tx_bytes: category
105                        .fields
106                        .get("txBytes")
107                        .and_then(serde_json::Value::as_u64)
108                        .unwrap_or(0),
109                    rx_bytes: category
110                        .fields
111                        .get("rxBytes")
112                        .and_then(serde_json::Value::as_u64)
113                        .unwrap_or(0),
114                    apps: Vec::new(),
115                }
116            })
117            .collect())
118    }
119
120    pub async fn list_dpi_applications(&self) -> Result<Vec<DpiApplication>, CoreError> {
121        let (client, site_id) = integration_site_context(self, "list_dpi_applications").await?;
122        let raw = client
123            .paginate_all(200, |offset, limit| {
124                client.list_dpi_applications(&site_id, offset, limit)
125            })
126            .await?;
127        Ok(raw
128            .into_iter()
129            .map(|application| {
130                #[allow(clippy::as_conversions, clippy::cast_possible_truncation)]
131                let id = application
132                    .fields
133                    .get("id")
134                    .and_then(serde_json::Value::as_u64)
135                    .unwrap_or(0) as u32;
136                DpiApplication {
137                    id,
138                    name: application
139                        .fields
140                        .get("name")
141                        .and_then(|value| value.as_str())
142                        .unwrap_or("Unknown")
143                        .to_owned(),
144                    #[allow(clippy::as_conversions, clippy::cast_possible_truncation)]
145                    category_id: application
146                        .fields
147                        .get("categoryId")
148                        .and_then(serde_json::Value::as_u64)
149                        .unwrap_or(0) as u32,
150                    tx_bytes: application
151                        .fields
152                        .get("txBytes")
153                        .and_then(serde_json::Value::as_u64)
154                        .unwrap_or(0),
155                    rx_bytes: application
156                        .fields
157                        .get("rxBytes")
158                        .and_then(serde_json::Value::as_u64)
159                        .unwrap_or(0),
160                }
161            })
162            .collect())
163    }
164
165    pub async fn list_radius_profiles(&self) -> Result<Vec<RadiusProfile>, CoreError> {
166        let (client, site_id) = integration_site_context(self, "list_radius_profiles").await?;
167        let raw = client
168            .paginate_all(200, |offset, limit| {
169                client.list_radius_profiles(&site_id, offset, limit)
170            })
171            .await?;
172        Ok(raw
173            .into_iter()
174            .map(|profile| RadiusProfile {
175                id: parse_integration_entity_id(&profile.fields),
176                name: profile
177                    .fields
178                    .get("name")
179                    .and_then(|value| value.as_str())
180                    .unwrap_or("Unknown")
181                    .to_owned(),
182            })
183            .collect())
184    }
185
186    pub async fn list_countries(&self) -> Result<Vec<Country>, CoreError> {
187        let client = integration_client_context(self, "list_countries").await?;
188        let raw = client
189            .paginate_all(200, |offset, limit| client.list_countries(offset, limit))
190            .await?;
191        Ok(raw
192            .into_iter()
193            .map(|country| Country {
194                code: country
195                    .fields
196                    .get("code")
197                    .and_then(|value| value.as_str())
198                    .unwrap_or("")
199                    .to_owned(),
200                name: country
201                    .fields
202                    .get("name")
203                    .and_then(|value| value.as_str())
204                    .unwrap_or("Unknown")
205                    .to_owned(),
206            })
207            .collect())
208    }
209
210    pub async fn get_network_references(
211        &self,
212        network_id: &EntityId,
213    ) -> Result<serde_json::Value, CoreError> {
214        let (client, site_id) = integration_site_context(self, "get_network_references").await?;
215        let uuid = require_uuid(network_id)?;
216        let refs = client.get_network_references(&site_id, &uuid).await?;
217        Ok(serde_json::to_value(refs).unwrap_or_default())
218    }
219}
220
221fn parse_integration_entity_id(
222    fields: &std::collections::HashMap<String, serde_json::Value>,
223) -> EntityId {
224    fields
225        .get("id")
226        .and_then(|value| value.as_str())
227        .and_then(|value| uuid::Uuid::parse_str(value).ok())
228        .map_or_else(|| EntityId::Legacy("unknown".into()), EntityId::Uuid)
229}
230
231fn parse_vpn_server(fields: &std::collections::HashMap<String, serde_json::Value>) -> VpnServer {
232    VpnServer {
233        id: parse_integration_entity_id(fields),
234        name: field_string(fields, &["name"]),
235        server_type: field_string(fields, &["type", "serverType"])
236            .unwrap_or_else(|| "UNKNOWN".into()),
237        enabled: field_bool(fields, &["enabled"]),
238        subnet: field_string(fields, &["subnet", "addressRange"]),
239        port: field_u16(fields, &["port"]),
240        wan_ip: field_string(fields, &["wanIp", "wanIP", "wanAddress"]),
241        connected_clients: field_u32(
242            fields,
243            &["connectedClients", "connectedClientCount", "numClients"],
244        ),
245        protocol: field_string(fields, &["protocol", "transportProtocol"]),
246        extra: collect_extra(fields),
247    }
248}
249
250fn parse_vpn_tunnel(fields: &std::collections::HashMap<String, serde_json::Value>) -> VpnTunnel {
251    let local_subnets = field_string_list(fields, &["localNetworks", "localSubnets"]);
252    let remote_subnets = field_string_list(fields, &["remoteNetworks", "remoteSubnets"]);
253
254    VpnTunnel {
255        id: parse_integration_entity_id(fields),
256        name: field_string(fields, &["name"]),
257        tunnel_type: field_string(fields, &["type", "tunnelType"])
258            .unwrap_or_else(|| "UNKNOWN".into()),
259        enabled: field_bool(fields, &["enabled"]),
260        peer_address: field_string(
261            fields,
262            &["peerIp", "peerAddress", "remoteAddress", "remoteHost"],
263        ),
264        local_subnets,
265        remote_subnets,
266        has_psk: fields
267            .get("psk")
268            .or_else(|| fields.get("preSharedKey"))
269            .is_some_and(|value| !value.is_null()),
270        ike_version: field_string(fields, &["ikeVersion", "ike"]),
271        extra: collect_extra(fields),
272    }
273}
274
275fn field_string(
276    fields: &std::collections::HashMap<String, serde_json::Value>,
277    keys: &[&str],
278) -> Option<String> {
279    keys.iter().find_map(|key| {
280        fields.get(*key).and_then(|value| match value {
281            serde_json::Value::String(value) => Some(value.clone()),
282            serde_json::Value::Number(value) => Some(value.to_string()),
283            _ => None,
284        })
285    })
286}
287
288fn field_bool(
289    fields: &std::collections::HashMap<String, serde_json::Value>,
290    keys: &[&str],
291) -> Option<bool> {
292    keys.iter()
293        .find_map(|key| fields.get(*key).and_then(serde_json::Value::as_bool))
294}
295
296fn field_u16(
297    fields: &std::collections::HashMap<String, serde_json::Value>,
298    keys: &[&str],
299) -> Option<u16> {
300    keys.iter().find_map(|key| {
301        fields
302            .get(*key)
303            .and_then(serde_json::Value::as_u64)
304            .and_then(|value| u16::try_from(value).ok())
305    })
306}
307
308fn field_u32(
309    fields: &std::collections::HashMap<String, serde_json::Value>,
310    keys: &[&str],
311) -> Option<u32> {
312    keys.iter().find_map(|key| {
313        fields
314            .get(*key)
315            .and_then(serde_json::Value::as_u64)
316            .and_then(|value| u32::try_from(value).ok())
317    })
318}
319
320fn field_string_list(
321    fields: &std::collections::HashMap<String, serde_json::Value>,
322    keys: &[&str],
323) -> Vec<String> {
324    keys.iter()
325        .filter_map(|key| fields.get(*key))
326        .flat_map(|value| match value {
327            serde_json::Value::String(value) => vec![value.clone()],
328            serde_json::Value::Array(values) => values
329                .iter()
330                .filter_map(|value| match value {
331                    serde_json::Value::String(value) => Some(value.clone()),
332                    serde_json::Value::Number(value) => Some(value.to_string()),
333                    _ => None,
334                })
335                .collect(),
336            _ => Vec::new(),
337        })
338        .collect()
339}
340
341fn collect_extra(
342    fields: &std::collections::HashMap<String, serde_json::Value>,
343) -> serde_json::Map<String, serde_json::Value> {
344    fields
345        .iter()
346        .map(|(key, value)| (key.clone(), value.clone()))
347        .collect()
348}
349
350#[cfg(test)]
351mod tests {
352    use super::{
353        field_string_list, parse_integration_entity_id, parse_vpn_server, parse_vpn_tunnel,
354    };
355    use crate::EntityId;
356
357    #[test]
358    fn parse_integration_entity_id_prefers_valid_uuid() {
359        let fields: std::collections::HashMap<String, serde_json::Value> =
360            serde_json::from_value(serde_json::json!({
361                "id": "11111111-1111-1111-1111-111111111111"
362            }))
363            .expect("hash map");
364
365        let id = parse_integration_entity_id(&fields);
366        assert_eq!(id.to_string(), "11111111-1111-1111-1111-111111111111");
367        assert!(id.as_uuid().is_some());
368    }
369
370    #[test]
371    fn parse_integration_entity_id_falls_back_to_legacy_unknown() {
372        let fields: std::collections::HashMap<String, serde_json::Value> =
373            serde_json::from_value(serde_json::json!({})).expect("hash map");
374        let id = parse_integration_entity_id(&fields);
375
376        assert_eq!(id, EntityId::Legacy("unknown".into()));
377    }
378
379    #[test]
380    fn parse_vpn_server_uses_fallback_fields() {
381        let fields: std::collections::HashMap<String, serde_json::Value> =
382            serde_json::from_value(serde_json::json!({
383                "id": "11111111-1111-1111-1111-111111111111",
384                "name": "Home WG",
385                "serverType": "WIREGUARD",
386                "enabled": true,
387                "addressRange": "10.8.0.0/24",
388                "port": 51820,
389                "wanAddress": "198.51.100.10",
390                "numClients": 4,
391                "transportProtocol": "UDP"
392            }))
393            .expect("hash map");
394
395        let server = parse_vpn_server(&fields);
396
397        assert_eq!(server.name.as_deref(), Some("Home WG"));
398        assert_eq!(server.server_type, "WIREGUARD");
399        assert_eq!(server.subnet.as_deref(), Some("10.8.0.0/24"));
400        assert_eq!(server.port, Some(51_820));
401        assert_eq!(server.wan_ip.as_deref(), Some("198.51.100.10"));
402        assert_eq!(server.connected_clients, Some(4));
403        assert_eq!(server.protocol.as_deref(), Some("UDP"));
404        assert_eq!(server.extra.len(), fields.len());
405    }
406
407    #[test]
408    fn parse_vpn_tunnel_combines_subnet_aliases() {
409        let fields: std::collections::HashMap<String, serde_json::Value> =
410            serde_json::from_value(serde_json::json!({
411                "id": "22222222-2222-2222-2222-222222222222",
412                "name": "Branch Tunnel",
413                "tunnelType": "IPSEC",
414                "enabled": true,
415                "remoteAddress": "203.0.113.20",
416                "localNetworks": ["10.0.0.0/24"],
417                "localSubnets": ["10.0.1.0/24"],
418                "remoteNetworks": ["10.10.0.0/24"],
419                "remoteSubnets": ["10.10.1.0/24"],
420                "preSharedKey": "redacted",
421                "ike": 2
422            }))
423            .expect("hash map");
424
425        let tunnel = parse_vpn_tunnel(&fields);
426
427        assert_eq!(tunnel.name.as_deref(), Some("Branch Tunnel"));
428        assert_eq!(tunnel.tunnel_type, "IPSEC");
429        assert_eq!(tunnel.peer_address.as_deref(), Some("203.0.113.20"));
430        assert_eq!(
431            tunnel.local_subnets,
432            vec!["10.0.0.0/24".to_string(), "10.0.1.0/24".to_string()]
433        );
434        assert_eq!(
435            tunnel.remote_subnets,
436            vec!["10.10.0.0/24".to_string(), "10.10.1.0/24".to_string()]
437        );
438        assert!(tunnel.has_psk);
439        assert_eq!(tunnel.ike_version.as_deref(), Some("2"));
440        assert_eq!(tunnel.extra.len(), fields.len());
441    }
442
443    #[test]
444    fn field_string_list_accepts_scalar_and_array_values() {
445        let fields: std::collections::HashMap<String, serde_json::Value> =
446            serde_json::from_value(serde_json::json!({
447                "localNetworks": "10.0.0.0/24",
448                "localSubnets": ["10.0.1.0/24", 42]
449            }))
450            .expect("hash map");
451
452        let values = field_string_list(&fields, &["localNetworks", "localSubnets"]);
453
454        assert_eq!(
455            values,
456            vec![
457                "10.0.0.0/24".to_string(),
458                "10.0.1.0/24".to_string(),
459                "42".to_string()
460            ]
461        );
462    }
463}