Skip to main content

unifly_api/integration/
types.rs

1//! Integration API response types for the UniFi Network Integration API (v10.1.84).
2//!
3//! All types match the JSON responses from `/integration/v1/` endpoints.
4//! Field names use camelCase via `#[serde(rename_all = "camelCase")]`.
5
6use std::collections::HashMap;
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use uuid::Uuid;
11
12// ── Pagination ───────────────────────────────────────────────────────
13
14/// Generic pagination wrapper returned by all list endpoints.
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub struct Page<T> {
18    pub offset: i64,
19    pub limit: i32,
20    pub count: i32,
21    pub total_count: i64,
22    pub data: Vec<T>,
23}
24
25// ── Sites ────────────────────────────────────────────────────────────
26
27/// Site overview — from `GET /v1/sites`.
28#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
29#[serde(rename_all = "camelCase")]
30pub struct SiteResponse {
31    pub id: Uuid,
32    pub name: String,
33    /// Used as the Legacy API site name (`/api/s/{internalReference}/`).
34    pub internal_reference: String,
35}
36
37// ── Devices ──────────────────────────────────────────────────────────
38
39/// Adopted device overview — from `GET /v1/sites/{siteId}/devices`.
40#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
41#[serde(rename_all = "camelCase")]
42pub struct DeviceResponse {
43    pub id: Uuid,
44    pub mac_address: String,
45    pub ip_address: Option<String>,
46    pub name: String,
47    pub model: String,
48    /// One of: `ONLINE`, `OFFLINE`, `PENDING_ADOPTION`, `UPDATING`,
49    /// `GETTING_READY`, `ADOPTING`, `DELETING`, `CONNECTION_INTERRUPTED`, `ISOLATED`.
50    pub state: String,
51    pub supported: bool,
52    pub firmware_version: Option<String>,
53    pub firmware_updatable: bool,
54    pub features: Vec<String>,
55    /// Complex nested interfaces object — kept as opaque JSON.
56    pub interfaces: Value,
57}
58
59/// Adopted device details — extends overview with additional fields.
60#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61#[serde(rename_all = "camelCase")]
62pub struct DeviceDetailsResponse {
63    pub id: Uuid,
64    pub mac_address: String,
65    pub ip_address: Option<String>,
66    pub name: String,
67    pub model: String,
68    pub state: String,
69    pub supported: bool,
70    pub firmware_version: Option<String>,
71    pub firmware_updatable: bool,
72    pub features: Vec<String>,
73    pub interfaces: Value,
74    pub serial_number: Option<String>,
75    pub short_name: Option<String>,
76    /// ISO 8601 date-time.
77    pub startup_timestamp: Option<String>,
78    /// Catch-all for additional fields not modeled above.
79    #[serde(flatten)]
80    pub extra: HashMap<String, Value>,
81}
82
83/// Latest statistics for a device — from `GET /v1/sites/{siteId}/devices/{deviceId}/statistics/latest`.
84#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85#[serde(rename_all = "camelCase")]
86pub struct DeviceStatisticsResponse {
87    pub uptime_sec: Option<i64>,
88    pub cpu_utilization_pct: Option<f64>,
89    pub memory_utilization_pct: Option<f64>,
90    pub load_average_1_min: Option<f64>,
91    pub load_average_5_min: Option<f64>,
92    pub load_average_15_min: Option<f64>,
93    /// ISO 8601 date-time.
94    pub last_heartbeat_at: Option<String>,
95    /// ISO 8601 date-time.
96    pub next_heartbeat_at: Option<String>,
97    /// Complex nested interfaces statistics.
98    pub interfaces: Value,
99    /// Uplink information.
100    pub uplink: Option<Value>,
101}
102
103// ── Clients ──────────────────────────────────────────────────────────
104
105/// Client overview — from `GET /v1/sites/{siteId}/clients`.
106#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
107#[serde(rename_all = "camelCase")]
108pub struct ClientResponse {
109    pub id: Uuid,
110    pub name: String,
111    /// One of: `WIRED`, `WIRELESS`, `VPN`, `TELEPORT`.
112    #[serde(rename = "type")]
113    pub client_type: String,
114    pub ip_address: Option<String>,
115    /// ISO 8601 date-time.
116    pub connected_at: Option<String>,
117    /// Polymorphic access object — contains a `type` discriminator field.
118    pub access: Value,
119}
120
121/// Client details — extends overview with additional fields.
122#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
123#[serde(rename_all = "camelCase")]
124pub struct ClientDetailsResponse {
125    pub id: Uuid,
126    pub name: String,
127    #[serde(rename = "type")]
128    pub client_type: String,
129    pub ip_address: Option<String>,
130    pub connected_at: Option<String>,
131    pub access: Value,
132    /// Catch-all for additional fields not modeled above.
133    #[serde(flatten)]
134    pub extra: HashMap<String, Value>,
135}
136
137// ── Networks ─────────────────────────────────────────────────────────
138
139/// Network overview — from `GET /v1/sites/{siteId}/networks`.
140///
141/// GATEWAY networks include top-level fields like `isolationEnabled`,
142/// `internetAccessEnabled`, `ipv4Configuration`, `ipv6Configuration`, etc.
143/// These are captured in `extra` via `#[serde(flatten)]`.
144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
145#[serde(rename_all = "camelCase")]
146pub struct NetworkResponse {
147    pub id: Uuid,
148    pub name: String,
149    pub enabled: bool,
150    /// One of: `USER_DEFINED`, `SYSTEM_DEFINED`, `ORCHESTRATED`.
151    pub management: String,
152    pub vlan_id: i32,
153    #[serde(default)]
154    pub default: bool,
155    pub metadata: Value,
156    /// Catch-all for management-type-specific fields (GATEWAY: ipv4Configuration,
157    /// ipv6Configuration, isolationEnabled, etc.; SWITCH: deviceId, natOutbound, etc.)
158    #[serde(flatten)]
159    pub extra: HashMap<String, Value>,
160}
161
162/// Network details — extends overview with additional fields.
163#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
164#[serde(rename_all = "camelCase")]
165pub struct NetworkDetailsResponse {
166    pub id: Uuid,
167    pub name: String,
168    pub enabled: bool,
169    pub management: String,
170    pub vlan_id: i32,
171    #[serde(default)]
172    pub default: bool,
173    pub metadata: Value,
174    pub dhcp_guarding: Option<Value>,
175    /// Catch-all for management-type-specific fields.
176    #[serde(flatten)]
177    pub extra: HashMap<String, Value>,
178}
179
180/// Create or update a network.
181#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
182#[serde(rename_all = "camelCase")]
183pub struct NetworkCreateUpdate {
184    pub name: String,
185    pub enabled: bool,
186    pub management: String,
187    pub vlan_id: i32,
188    pub dhcp_guarding: Option<Value>,
189    /// GATEWAY/SWITCH-specific fields to include in create/update requests.
190    #[serde(flatten)]
191    pub extra: HashMap<String, Value>,
192}
193
194/// References to resources using a network.
195#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub struct NetworkReferencesResponse {
198    #[serde(flatten)]
199    pub fields: HashMap<String, Value>,
200}
201
202// ── WiFi Broadcasts ──────────────────────────────────────────────────
203
204/// WiFi broadcast overview — from `GET /v1/sites/{siteId}/wifi`.
205#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
206#[serde(rename_all = "camelCase")]
207pub struct WifiBroadcastResponse {
208    pub id: Uuid,
209    pub name: String,
210    #[serde(rename = "type")]
211    pub broadcast_type: String,
212    pub enabled: bool,
213    pub security_configuration: Value,
214    pub metadata: Value,
215    pub network: Option<Value>,
216    pub broadcasting_device_filter: Option<Value>,
217    /// Catch-all for additional overview fields not modeled above.
218    #[serde(flatten)]
219    pub extra: HashMap<String, Value>,
220}
221
222/// WiFi broadcast details — extends overview with additional fields.
223#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
224#[serde(rename_all = "camelCase")]
225pub struct WifiBroadcastDetailsResponse {
226    pub id: Uuid,
227    pub name: String,
228    #[serde(rename = "type")]
229    pub broadcast_type: String,
230    pub enabled: bool,
231    pub security_configuration: Value,
232    pub metadata: Value,
233    pub network: Option<Value>,
234    pub broadcasting_device_filter: Option<Value>,
235    /// Catch-all for additional fields not modeled above.
236    #[serde(flatten)]
237    pub extra: HashMap<String, Value>,
238}
239
240/// Create or update a WiFi broadcast.
241#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
242#[serde(rename_all = "camelCase")]
243pub struct WifiBroadcastCreateUpdate {
244    pub name: String,
245    #[serde(rename = "type")]
246    pub broadcast_type: String,
247    pub enabled: bool,
248    /// All remaining type-specific fields.
249    #[serde(flatten)]
250    pub body: serde_json::Map<String, Value>,
251}
252
253// ── Firewall Traffic Filters ─────────────────────────────────────────
254
255/// Source endpoint of a firewall policy.
256#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
257#[serde(rename_all = "camelCase")]
258pub struct FirewallPolicySource {
259    pub zone_id: Option<Uuid>,
260    #[serde(default)]
261    pub traffic_filter: Option<SourceTrafficFilter>,
262}
263
264/// Destination endpoint of a firewall policy.
265#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
266#[serde(rename_all = "camelCase")]
267pub struct FirewallPolicyDestination {
268    pub zone_id: Option<Uuid>,
269    #[serde(default)]
270    pub traffic_filter: Option<DestTrafficFilter>,
271}
272
273/// Source traffic filter — discriminated by `type`.
274#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
275#[serde(tag = "type")]
276pub enum SourceTrafficFilter {
277    #[serde(rename = "NETWORK")]
278    Network {
279        #[serde(rename = "networkFilter")]
280        network_filter: NetworkFilter,
281        #[serde(
282            rename = "macAddressFilter",
283            default,
284            skip_serializing_if = "Option::is_none"
285        )]
286        mac_address_filter: Option<MacAddressFilter>,
287        #[serde(
288            rename = "portFilter",
289            default,
290            skip_serializing_if = "Option::is_none"
291        )]
292        port_filter: Option<PortFilter>,
293    },
294    #[serde(rename = "IP_ADDRESS")]
295    IpAddress {
296        #[serde(rename = "ipAddressFilter")]
297        ip_address_filter: IpAddressFilter,
298        #[serde(
299            rename = "macAddressFilter",
300            default,
301            skip_serializing_if = "Option::is_none"
302        )]
303        mac_address_filter: Option<MacAddressFilter>,
304        #[serde(
305            rename = "portFilter",
306            default,
307            skip_serializing_if = "Option::is_none"
308        )]
309        port_filter: Option<PortFilter>,
310    },
311    #[serde(rename = "MAC_ADDRESS")]
312    MacAddress {
313        #[serde(rename = "macAddressFilter")]
314        mac_address_filter: MacAddressFilter,
315        #[serde(
316            rename = "portFilter",
317            default,
318            skip_serializing_if = "Option::is_none"
319        )]
320        port_filter: Option<PortFilter>,
321    },
322    #[serde(rename = "PORT")]
323    Port {
324        #[serde(rename = "portFilter")]
325        port_filter: PortFilter,
326    },
327    #[serde(rename = "REGION")]
328    Region {
329        #[serde(rename = "regionFilter")]
330        region_filter: RegionFilter,
331        #[serde(
332            rename = "portFilter",
333            default,
334            skip_serializing_if = "Option::is_none"
335        )]
336        port_filter: Option<PortFilter>,
337    },
338    #[serde(other)]
339    Unknown,
340}
341
342/// Destination traffic filter — discriminated by `type`.
343#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
344#[serde(tag = "type")]
345pub enum DestTrafficFilter {
346    #[serde(rename = "NETWORK")]
347    Network {
348        #[serde(rename = "networkFilter")]
349        network_filter: NetworkFilter,
350        #[serde(
351            rename = "portFilter",
352            default,
353            skip_serializing_if = "Option::is_none"
354        )]
355        port_filter: Option<PortFilter>,
356    },
357    #[serde(rename = "IP_ADDRESS")]
358    IpAddress {
359        #[serde(rename = "ipAddressFilter")]
360        ip_address_filter: IpAddressFilter,
361        #[serde(
362            rename = "portFilter",
363            default,
364            skip_serializing_if = "Option::is_none"
365        )]
366        port_filter: Option<PortFilter>,
367    },
368    #[serde(rename = "PORT")]
369    Port {
370        #[serde(rename = "portFilter")]
371        port_filter: PortFilter,
372    },
373    #[serde(rename = "REGION")]
374    Region {
375        #[serde(rename = "regionFilter")]
376        region_filter: RegionFilter,
377        #[serde(
378            rename = "portFilter",
379            default,
380            skip_serializing_if = "Option::is_none"
381        )]
382        port_filter: Option<PortFilter>,
383    },
384    #[serde(rename = "APPLICATION")]
385    Application {
386        #[serde(rename = "applicationFilter")]
387        application_filter: ApplicationFilter,
388        #[serde(
389            rename = "portFilter",
390            default,
391            skip_serializing_if = "Option::is_none"
392        )]
393        port_filter: Option<PortFilter>,
394    },
395    #[serde(rename = "APPLICATION_CATEGORY")]
396    ApplicationCategory {
397        #[serde(rename = "applicationCategoryFilter")]
398        application_category_filter: ApplicationCategoryFilter,
399        #[serde(
400            rename = "portFilter",
401            default,
402            skip_serializing_if = "Option::is_none"
403        )]
404        port_filter: Option<PortFilter>,
405    },
406    #[serde(rename = "DOMAIN")]
407    Domain {
408        #[serde(rename = "domainFilter")]
409        domain_filter: DomainFilter,
410        #[serde(
411            rename = "portFilter",
412            default,
413            skip_serializing_if = "Option::is_none"
414        )]
415        port_filter: Option<PortFilter>,
416    },
417    #[serde(other)]
418    Unknown,
419}
420
421/// Network filter — match by network IDs.
422#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
423#[serde(rename_all = "camelCase")]
424pub struct NetworkFilter {
425    pub network_ids: Vec<Uuid>,
426    #[serde(default)]
427    pub match_opposite: bool,
428}
429
430/// IP address filter — polymorphic: specific addresses or traffic matching list.
431#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
432#[serde(tag = "type")]
433pub enum IpAddressFilter {
434    #[serde(rename = "IP_ADDRESSES", alias = "SPECIFIC")]
435    Specific {
436        #[serde(default)]
437        items: Vec<IpAddressItem>,
438        #[serde(default, rename = "matchOpposite")]
439        match_opposite: bool,
440    },
441    #[serde(rename = "TRAFFIC_MATCHING_LIST")]
442    TrafficMatchingList {
443        #[serde(rename = "trafficMatchingListId")]
444        traffic_matching_list_id: Uuid,
445        #[serde(default, rename = "matchOpposite")]
446        match_opposite: bool,
447    },
448    #[serde(other)]
449    Unknown,
450}
451
452/// Individual IP address match item.
453#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
454#[serde(tag = "type")]
455pub enum IpAddressItem {
456    #[serde(rename = "IP_ADDRESS")]
457    Address { value: String },
458    #[serde(rename = "RANGE")]
459    Range { start: String, stop: String },
460    #[serde(rename = "SUBNET")]
461    Subnet { value: String },
462}
463
464/// Port filter — polymorphic: explicit values or traffic matching list.
465#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
466#[serde(tag = "type")]
467pub enum PortFilter {
468    #[serde(rename = "PORTS", alias = "VALUE")]
469    Ports {
470        #[serde(default)]
471        items: Vec<PortItem>,
472        #[serde(default, rename = "matchOpposite")]
473        match_opposite: bool,
474    },
475    #[serde(rename = "TRAFFIC_MATCHING_LIST")]
476    TrafficMatchingList {
477        #[serde(rename = "trafficMatchingListId")]
478        traffic_matching_list_id: Uuid,
479        #[serde(default, rename = "matchOpposite")]
480        match_opposite: bool,
481    },
482    #[serde(other)]
483    Unknown,
484}
485
486/// Individual port match item.
487#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
488#[serde(tag = "type")]
489pub enum PortItem {
490    #[serde(rename = "PORT_NUMBER")]
491    Number {
492        #[serde(deserialize_with = "crate::integration::types::deserialize_port_value")]
493        value: String,
494    },
495    #[serde(rename = "PORT_RANGE")]
496    Range {
497        #[serde(
498            rename = "startPort",
499            deserialize_with = "crate::integration::types::deserialize_port_value"
500        )]
501        start_port: String,
502        #[serde(
503            rename = "endPort",
504            deserialize_with = "crate::integration::types::deserialize_port_value"
505        )]
506        end_port: String,
507    },
508    #[serde(other)]
509    Unknown,
510}
511
512/// Deserialize a port value that may be either a number or a string.
513fn deserialize_port_value<'de, D>(deserializer: D) -> Result<String, D::Error>
514where
515    D: serde::Deserializer<'de>,
516{
517    struct PortValueVisitor;
518    impl serde::de::Visitor<'_> for PortValueVisitor {
519        type Value = String;
520
521        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
522            formatter.write_str("a port number as string or integer")
523        }
524
525        fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<String, E> {
526            Ok(v.to_string())
527        }
528
529        fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<String, E> {
530            Ok(v.to_string())
531        }
532
533        fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<String, E> {
534            Ok(v.to_string())
535        }
536    }
537
538    deserializer.deserialize_any(PortValueVisitor)
539}
540
541/// MAC address filter.
542#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
543#[serde(rename_all = "camelCase")]
544pub struct MacAddressFilter {
545    pub mac_addresses: Vec<String>,
546}
547
548/// DPI application filter.
549#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
550#[serde(rename_all = "camelCase")]
551pub struct ApplicationFilter {
552    pub application_ids: Vec<i64>,
553}
554
555/// DPI application category filter.
556#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
557#[serde(rename_all = "camelCase")]
558pub struct ApplicationCategoryFilter {
559    pub application_category_ids: Vec<i64>,
560}
561
562/// Region filter.
563#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
564pub struct RegionFilter {
565    pub regions: Vec<String>,
566}
567
568/// Domain filter — polymorphic.
569#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
570#[serde(tag = "type")]
571pub enum DomainFilter {
572    #[serde(rename = "SPECIFIC")]
573    Specific { domains: Vec<String> },
574    #[serde(other)]
575    Unknown,
576}
577
578// ── Firewall Policies ────────────────────────────────────────────────
579
580/// Firewall policy — from `GET /v1/sites/{siteId}/firewall/policies`.
581#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
582#[serde(rename_all = "camelCase")]
583pub struct FirewallPolicyResponse {
584    pub id: Uuid,
585    pub name: String,
586    #[serde(default)]
587    pub description: Option<String>,
588    pub enabled: bool,
589    /// Polymorphic action object with `type` discriminator.
590    pub action: Value,
591    pub ip_protocol_scope: Option<Value>,
592    #[serde(default)]
593    pub logging_enabled: bool,
594    pub metadata: Option<Value>,
595    /// Typed source endpoint with traffic filter.
596    #[serde(default)]
597    pub source: Option<FirewallPolicySource>,
598    /// Typed destination endpoint with traffic filter.
599    #[serde(default)]
600    pub destination: Option<FirewallPolicyDestination>,
601    /// Catch-all for remaining fields (index, schedule, ipsec, connectionStateFilter, etc.)
602    #[serde(flatten)]
603    pub extra: HashMap<String, Value>,
604}
605
606/// Create or update a firewall policy.
607#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
608#[serde(rename_all = "camelCase")]
609pub struct FirewallPolicyCreateUpdate {
610    pub name: String,
611    pub description: Option<String>,
612    pub enabled: bool,
613    pub action: Value,
614    pub source: Value,
615    pub destination: Value,
616    pub ip_protocol_scope: Value,
617    pub logging_enabled: bool,
618    pub ipsec_filter: Option<String>,
619    pub schedule: Option<Value>,
620    pub connection_state_filter: Option<Vec<String>>,
621}
622
623/// Patch a firewall policy (partial update).
624#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
625#[serde(rename_all = "camelCase")]
626pub struct FirewallPolicyPatch {
627    #[serde(skip_serializing_if = "Option::is_none")]
628    pub enabled: Option<bool>,
629    #[serde(skip_serializing_if = "Option::is_none")]
630    pub logging_enabled: Option<bool>,
631}
632
633/// Ordered firewall policy IDs — for reordering policies.
634#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
635#[serde(rename_all = "camelCase")]
636pub struct FirewallPolicyOrdering {
637    pub before_system_defined: Vec<Uuid>,
638    pub after_system_defined: Vec<Uuid>,
639}
640
641// ── Firewall Zones ───────────────────────────────────────────────────
642
643/// Firewall zone — from `GET /v1/sites/{siteId}/firewall/zones`.
644#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
645#[serde(rename_all = "camelCase")]
646pub struct FirewallZoneResponse {
647    pub id: Uuid,
648    pub name: String,
649    pub network_ids: Vec<Uuid>,
650    pub metadata: Value,
651}
652
653/// Create or update a firewall zone.
654#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
655#[serde(rename_all = "camelCase")]
656pub struct FirewallZoneCreateUpdate {
657    pub name: String,
658    pub network_ids: Vec<Uuid>,
659}
660
661// ── ACL Rules ────────────────────────────────────────────────────────
662
663/// ACL rule — from `GET /v1/sites/{siteId}/acl/rules`.
664#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
665#[serde(rename_all = "camelCase")]
666pub struct AclRuleResponse {
667    pub id: Uuid,
668    pub name: String,
669    /// One of: `IP`, `MAC`.
670    #[serde(rename = "type")]
671    pub rule_type: String,
672    /// One of: `ALLOW`, `BLOCK`.
673    pub action: String,
674    pub enabled: bool,
675    pub index: i32,
676    pub description: Option<String>,
677    pub source_filter: Option<Value>,
678    pub destination_filter: Option<Value>,
679    pub enforcing_device_filter: Option<Value>,
680    pub metadata: Value,
681}
682
683/// Create or update an ACL rule.
684#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
685#[serde(rename_all = "camelCase")]
686pub struct AclRuleCreateUpdate {
687    pub name: String,
688    #[serde(rename = "type")]
689    pub rule_type: String,
690    pub action: String,
691    pub enabled: bool,
692    pub description: Option<String>,
693    pub source_filter: Option<Value>,
694    pub destination_filter: Option<Value>,
695    pub enforcing_device_filter: Option<Value>,
696}
697
698/// ACL rule ordering — for reordering rules.
699#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
700#[serde(rename_all = "camelCase")]
701pub struct AclRuleOrdering {
702    pub ordered_acl_rule_ids: Vec<Uuid>,
703}
704
705// ── DNS Policies ─────────────────────────────────────────────────────
706
707/// DNS policy — from `GET /v1/sites/{siteId}/dns/policies`.
708#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
709#[serde(rename_all = "camelCase")]
710pub struct DnsPolicyResponse {
711    pub id: Uuid,
712    #[serde(rename = "type")]
713    pub policy_type: String,
714    pub enabled: bool,
715    pub domain: Option<String>,
716    pub metadata: Value,
717    /// Type-specific fields vary by policy type.
718    #[serde(flatten)]
719    pub extra: HashMap<String, Value>,
720}
721
722/// Create or update a DNS policy.
723#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
724#[serde(rename_all = "camelCase")]
725pub struct DnsPolicyCreateUpdate {
726    #[serde(rename = "type")]
727    pub policy_type: String,
728    pub enabled: bool,
729    /// Type-specific fields vary by policy type.
730    #[serde(flatten)]
731    pub fields: serde_json::Map<String, Value>,
732}
733
734// ── Traffic Matching Lists ───────────────────────────────────────────
735
736/// Traffic matching list — from `GET /v1/sites/{siteId}/traffic-matching-lists`.
737#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
738#[serde(rename_all = "camelCase")]
739pub struct TrafficMatchingListResponse {
740    pub id: Uuid,
741    pub name: String,
742    /// One of: `IPV4`, `IPV6`, `PORT`.
743    #[serde(rename = "type")]
744    pub list_type: String,
745    #[serde(flatten)]
746    pub extra: HashMap<String, Value>,
747}
748
749/// Create or update a traffic matching list.
750#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
751#[serde(rename_all = "camelCase")]
752pub struct TrafficMatchingListCreateUpdate {
753    pub name: String,
754    #[serde(rename = "type")]
755    pub list_type: String,
756    #[serde(flatten)]
757    pub fields: serde_json::Map<String, Value>,
758}
759
760// ── Hotspot Vouchers ─────────────────────────────────────────────────
761
762/// Hotspot voucher — from `GET /v1/sites/{siteId}/hotspot/vouchers`.
763#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
764#[serde(rename_all = "camelCase")]
765pub struct VoucherResponse {
766    pub id: Uuid,
767    pub code: String,
768    pub name: String,
769    /// ISO 8601 date-time.
770    pub created_at: String,
771    /// ISO 8601 date-time.
772    pub activated_at: Option<String>,
773    /// ISO 8601 date-time.
774    pub expires_at: Option<String>,
775    pub expired: bool,
776    pub time_limit_minutes: i64,
777    pub authorized_guest_count: i64,
778    pub authorized_guest_limit: Option<i64>,
779    pub data_usage_limit_m_bytes: Option<i64>,
780    pub rx_rate_limit_kbps: Option<i64>,
781    pub tx_rate_limit_kbps: Option<i64>,
782}
783
784/// Create hotspot voucher(s).
785#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
786#[serde(rename_all = "camelCase")]
787pub struct VoucherCreateRequest {
788    pub name: String,
789    /// Number of vouchers to create (defaults to 1).
790    pub count: Option<i32>,
791    pub time_limit_minutes: i64,
792    pub authorized_guest_limit: Option<i64>,
793    pub data_usage_limit_m_bytes: Option<i64>,
794    pub rx_rate_limit_kbps: Option<i64>,
795    pub tx_rate_limit_kbps: Option<i64>,
796}
797
798/// Bulk voucher deletion results.
799#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
800#[serde(rename_all = "camelCase")]
801pub struct VoucherDeletionResults {
802    #[serde(flatten)]
803    pub fields: HashMap<String, Value>,
804}
805
806// ── Device Actions ───────────────────────────────────────────────────
807
808/// Device action request body.
809///
810/// Valid actions: `RESTART`, `ADOPT`, `LOCATE_ON`, `LOCATE_OFF`.
811#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
812#[serde(rename_all = "camelCase")]
813pub struct DeviceActionRequest {
814    pub action: String,
815}
816
817/// Device adoption request body.
818#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
819#[serde(rename_all = "camelCase")]
820pub struct DeviceAdoptionRequest {
821    pub mac_address: String,
822    pub ignore_device_limit: bool,
823}
824
825// ── Client Actions ───────────────────────────────────────────────────
826
827/// Client action request body.
828///
829/// Valid actions: `BLOCK`, `UNBLOCK`, `RECONNECT`.
830#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
831#[serde(rename_all = "camelCase")]
832pub struct ClientActionRequest {
833    pub action: String,
834}
835
836/// Client action response.
837#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
838#[serde(rename_all = "camelCase")]
839pub struct ClientActionResponse {
840    pub action: String,
841    pub id: Uuid,
842    #[serde(flatten)]
843    pub extra: HashMap<String, Value>,
844}
845
846// ── Port Actions ─────────────────────────────────────────────────────
847
848/// Port action request body.
849///
850/// Valid actions: `POWER_CYCLE`.
851#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
852#[serde(rename_all = "camelCase")]
853pub struct PortActionRequest {
854    pub action: String,
855}
856
857// ── Application Info ─────────────────────────────────────────────────
858
859/// Application info — from `GET /v1/info`.
860#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
861#[serde(rename_all = "camelCase")]
862pub struct ApplicationInfoResponse {
863    #[serde(flatten)]
864    pub fields: HashMap<String, Value>,
865}
866
867// ── Error ────────────────────────────────────────────────────────────
868
869/// Error response returned by the Integration API.
870#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
871#[serde(rename_all = "camelCase")]
872pub struct ErrorResponse {
873    pub message: Option<String>,
874    #[serde(flatten)]
875    pub extra: HashMap<String, Value>,
876}
877
878// ── Read-only / Opaque Types ─────────────────────────────────────────
879//
880// These endpoints return complex or under-documented structures.
881// We capture them as open-ended maps until we need typed access.
882
883/// DPI category — from `GET /v1/sites/{siteId}/dpi/categories`.
884#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
885#[serde(rename_all = "camelCase")]
886pub struct DpiCategoryResponse {
887    #[serde(flatten)]
888    pub fields: HashMap<String, Value>,
889}
890
891/// DPI application — from `GET /v1/sites/{siteId}/dpi/applications`.
892#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
893#[serde(rename_all = "camelCase")]
894pub struct DpiApplicationResponse {
895    #[serde(flatten)]
896    pub fields: HashMap<String, Value>,
897}
898
899/// VPN server — from `GET /v1/sites/{siteId}/vpn/servers`.
900#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
901#[serde(rename_all = "camelCase")]
902pub struct VpnServerResponse {
903    #[serde(flatten)]
904    pub fields: HashMap<String, Value>,
905}
906
907/// VPN tunnel — from `GET /v1/sites/{siteId}/vpn/tunnels`.
908#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
909#[serde(rename_all = "camelCase")]
910pub struct VpnTunnelResponse {
911    #[serde(flatten)]
912    pub fields: HashMap<String, Value>,
913}
914
915/// WAN configuration — from `GET /v1/sites/{siteId}/wan`.
916#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
917#[serde(rename_all = "camelCase")]
918pub struct WanResponse {
919    #[serde(flatten)]
920    pub fields: HashMap<String, Value>,
921}
922
923/// RADIUS profile — from `GET /v1/sites/{siteId}/radius/profiles`.
924#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
925#[serde(rename_all = "camelCase")]
926pub struct RadiusProfileResponse {
927    #[serde(flatten)]
928    pub fields: HashMap<String, Value>,
929}
930
931/// Country metadata — from `GET /v1/countries`.
932#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
933#[serde(rename_all = "camelCase")]
934pub struct CountryResponse {
935    #[serde(flatten)]
936    pub fields: HashMap<String, Value>,
937}
938
939/// Device tag — from `GET /v1/sites/{siteId}/devices/tags`.
940#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
941#[serde(rename_all = "camelCase")]
942pub struct DeviceTagResponse {
943    #[serde(flatten)]
944    pub fields: HashMap<String, Value>,
945}
946
947/// Pending device — from `GET /v1/sites/{siteId}/devices/pending`.
948#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
949#[serde(rename_all = "camelCase")]
950pub struct PendingDeviceResponse {
951    #[serde(flatten)]
952    pub fields: HashMap<String, Value>,
953}