unifly_api/command/requests/
ports.rs1use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Default, Serialize, Deserialize)]
14#[serde(deny_unknown_fields)]
15pub struct ApplyPortsRequest {
16 pub ports: Vec<ApplyPortEntry>,
20}
21
22#[derive(Debug, Clone, Default, Serialize, Deserialize)]
29#[serde(deny_unknown_fields)]
30pub struct ApplyPortEntry {
31 pub index: u32,
33
34 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub name: Option<String>,
37
38 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub mode: Option<String>,
41
42 #[serde(
46 default,
47 skip_serializing_if = "Option::is_none",
48 alias = "native_vlan"
49 )]
50 pub native_network_id: Option<String>,
51
52 #[serde(
56 default,
57 skip_serializing_if = "Option::is_none",
58 alias = "tagged_vlans"
59 )]
60 pub tagged_network_ids: Option<Vec<String>>,
61
62 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub tagged_all: Option<bool>,
67
68 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub poe: Option<String>,
73
74 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub speed: Option<String>,
78
79 #[serde(default, skip_serializing_if = "is_false")]
83 pub reset: bool,
84}
85
86#[allow(clippy::trivially_copy_pass_by_ref)]
87fn is_false(b: &bool) -> bool {
88 !*b
89}
90
91#[cfg(test)]
92mod tests {
93 use super::{ApplyPortEntry, ApplyPortsRequest};
94
95 #[test]
96 fn deserializes_minimal_entry() {
97 let req: ApplyPortsRequest = serde_json::from_value(serde_json::json!({
98 "ports": [
99 { "index": 9, "name": "mac-mini" }
100 ]
101 }))
102 .expect("deserialize");
103 assert_eq!(req.ports.len(), 1);
104 assert_eq!(req.ports[0].index, 9);
105 assert_eq!(req.ports[0].name.as_deref(), Some("mac-mini"));
106 assert!(req.ports[0].mode.is_none());
107 assert!(!req.ports[0].reset);
108 }
109
110 #[test]
111 fn deserializes_full_entry_via_aliases() {
112 let req: ApplyPortsRequest = serde_json::from_value(serde_json::json!({
113 "ports": [
114 {
115 "index": 1,
116 "name": "uplink",
117 "mode": "trunk",
118 "native_vlan": "infra",
119 "tagged_vlans": ["personal", "iot"],
120 "tagged_all": false,
121 "poe": "auto",
122 "speed": "auto"
123 }
124 ]
125 }))
126 .expect("deserialize");
127 let p = &req.ports[0];
128 assert_eq!(p.native_network_id.as_deref(), Some("infra"));
129 assert_eq!(
130 p.tagged_network_ids.as_deref(),
131 Some(&["personal".into(), "iot".into()][..])
132 );
133 assert_eq!(p.tagged_all, Some(false));
134 }
135
136 #[test]
137 fn empty_tagged_array_round_trips_as_clear_intent() {
138 let req: ApplyPortsRequest = serde_json::from_value(serde_json::json!({
141 "ports": [{ "index": 5, "tagged_vlans": [] }]
142 }))
143 .expect("deserialize");
144 assert_eq!(req.ports[0].tagged_network_ids.as_deref(), Some(&[][..]));
145 }
146
147 #[test]
148 fn reset_entry_serializes_compactly() {
149 let req = ApplyPortsRequest {
150 ports: vec![ApplyPortEntry {
151 index: 5,
152 name: None,
153 mode: None,
154 native_network_id: None,
155 tagged_network_ids: None,
156 tagged_all: None,
157 poe: None,
158 speed: None,
159 reset: true,
160 }],
161 };
162 let value = serde_json::to_value(&req).expect("serialize");
163 assert_eq!(
164 value,
165 serde_json::json!({ "ports": [{ "index": 5, "reset": true }] })
166 );
167 }
168
169 #[test]
170 fn unknown_field_is_rejected() {
171 let result: Result<ApplyPortsRequest, _> = serde_json::from_value(serde_json::json!({
172 "ports": [{ "index": 1, "vlan": "infra" }] }));
174 let err = result.expect_err("expected unknown-field rejection");
175 assert!(err.to_string().contains("unknown field"));
176 }
177}