use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ApplyPortsRequest {
pub ports: Vec<ApplyPortEntry>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ApplyPortEntry {
pub index: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mode: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "native_vlan"
)]
pub native_network_id: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "tagged_vlans"
)]
pub tagged_network_ids: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tagged_all: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub poe: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub speed: Option<String>,
#[serde(default, skip_serializing_if = "is_false")]
pub reset: bool,
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_false(b: &bool) -> bool {
!*b
}
#[cfg(test)]
mod tests {
use super::{ApplyPortEntry, ApplyPortsRequest};
#[test]
fn deserializes_minimal_entry() {
let req: ApplyPortsRequest = serde_json::from_value(serde_json::json!({
"ports": [
{ "index": 9, "name": "mac-mini" }
]
}))
.expect("deserialize");
assert_eq!(req.ports.len(), 1);
assert_eq!(req.ports[0].index, 9);
assert_eq!(req.ports[0].name.as_deref(), Some("mac-mini"));
assert!(req.ports[0].mode.is_none());
assert!(!req.ports[0].reset);
}
#[test]
fn deserializes_full_entry_via_aliases() {
let req: ApplyPortsRequest = serde_json::from_value(serde_json::json!({
"ports": [
{
"index": 1,
"name": "uplink",
"mode": "trunk",
"native_vlan": "infra",
"tagged_vlans": ["personal", "iot"],
"tagged_all": false,
"poe": "auto",
"speed": "auto"
}
]
}))
.expect("deserialize");
let p = &req.ports[0];
assert_eq!(p.native_network_id.as_deref(), Some("infra"));
assert_eq!(
p.tagged_network_ids.as_deref(),
Some(&["personal".into(), "iot".into()][..])
);
assert_eq!(p.tagged_all, Some(false));
}
#[test]
fn empty_tagged_array_round_trips_as_clear_intent() {
let req: ApplyPortsRequest = serde_json::from_value(serde_json::json!({
"ports": [{ "index": 5, "tagged_vlans": [] }]
}))
.expect("deserialize");
assert_eq!(req.ports[0].tagged_network_ids.as_deref(), Some(&[][..]));
}
#[test]
fn reset_entry_serializes_compactly() {
let req = ApplyPortsRequest {
ports: vec![ApplyPortEntry {
index: 5,
name: None,
mode: None,
native_network_id: None,
tagged_network_ids: None,
tagged_all: None,
poe: None,
speed: None,
reset: true,
}],
};
let value = serde_json::to_value(&req).expect("serialize");
assert_eq!(
value,
serde_json::json!({ "ports": [{ "index": 5, "reset": true }] })
);
}
#[test]
fn unknown_field_is_rejected() {
let result: Result<ApplyPortsRequest, _> = serde_json::from_value(serde_json::json!({
"ports": [{ "index": 1, "vlan": "infra" }] }));
let err = result.expect_err("expected unknown-field rejection");
assert!(err.to_string().contains("unknown field"));
}
}