pub type Field = (&'static str, &'static str);
pub const CLIENTS_LIST: &[Field] = &[
("name", "string"),
("mac", "string"),
("ip", "string"),
("type", "string"),
("ssid", "string"),
("signal", "integer"),
("uptime", "integer"),
("network", "string"),
("vlan", "integer"),
("tx_bytes", "integer"),
("rx_bytes", "integer"),
("blocked", "boolean"),
("connected_at", "string"),
];
pub const CLIENTS_SHOW: &[Field] = &[
("name", "string"),
("mac", "string"),
("ip", "string"),
("wired", "boolean"),
("uptime", "integer"),
("tx_bytes", "integer"),
("rx_bytes", "integer"),
("signal", "integer"),
("ssid", "string"),
("ap_mac", "string"),
("network", "string"),
("vlan", "integer"),
("blocked", "boolean"),
];
pub const DEVICES_LIST: &[Field] = &[
("name", "string"),
("model", "string"),
("mac", "string"),
("ip", "string"),
("state", "string"),
("firmware", "string"),
];
pub const EVENTS_LIST: &[Field] = &[
("key", "string"),
("msg", "string"),
("subsystem", "string"),
("time", "integer"),
("datetime", "string"),
];
pub const NETWORKS_LIST: &[Field] = &[
("name", "string"),
("vlan_id", "integer"),
("enabled", "boolean"),
("default", "boolean"),
];
pub const PORTS_LIST: &[Field] = &[
("device_mac", "string"),
("device_name", "string"),
("port_idx", "integer"),
("name", "string"),
("media", "string"),
("up", "boolean"),
("speed", "integer"),
("full_duplex", "boolean"),
("poe_enable", "boolean"),
("poe_power", "number"),
("port_poe", "boolean"),
("tx_bytes", "integer"),
("rx_bytes", "integer"),
];
pub const PORTS_FIND: &[Field] = &[
("device_mac", "string"),
("device_name", "string"),
("port_idx", "integer"),
("name", "string"),
("media", "string"),
("up", "boolean"),
("speed", "integer"),
("full_duplex", "boolean"),
("poe_enable", "boolean"),
("poe_power", "number"),
("port_poe", "boolean"),
("tx_bytes", "integer"),
("rx_bytes", "integer"),
("connected", "boolean"),
];
#[derive(Debug, PartialEq, Eq)]
pub struct InvalidFields {
pub unknown: Vec<String>,
pub valid: Vec<&'static str>,
}
impl std::fmt::Display for InvalidFields {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let plural = if self.unknown.len() == 1 {
"field"
} else {
"fields"
};
write!(
f,
"unknown {plural} in --fields: {}. Valid fields: {}",
self.unknown.join(", "),
self.valid.join(", ")
)
}
}
impl std::error::Error for InvalidFields {}
pub fn names(table: &[Field]) -> Vec<&'static str> {
table.iter().map(|(n, _)| *n).collect()
}
pub fn validate(spec: &str, table: &[Field]) -> Result<Vec<String>, InvalidFields> {
let valid = names(table);
let requested: Vec<&str> = spec
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.collect();
let mut unknown: Vec<String> = requested
.iter()
.filter(|r| !valid.contains(*r))
.map(|r| (*r).to_string())
.collect();
unknown.dedup();
if unknown.is_empty() {
Ok(requested.into_iter().map(str::to_string).collect())
} else {
Err(InvalidFields { unknown, valid })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_a_single_known_field() {
assert_eq!(validate("mac", CLIENTS_LIST).unwrap(), vec!["mac"]);
}
#[test]
fn accepts_several_known_fields_preserving_order() {
assert_eq!(
validate("ip,mac,ssid", CLIENTS_LIST).unwrap(),
vec!["ip", "mac", "ssid"]
);
}
#[test]
fn tolerates_whitespace_and_empty_segments() {
assert_eq!(
validate(" mac , , ip ", CLIENTS_LIST).unwrap(),
vec!["mac", "ip"]
);
}
#[test]
fn rejects_an_unknown_field() {
let err = validate("bogus", CLIENTS_LIST).unwrap_err();
assert_eq!(err.unknown, vec!["bogus"]);
assert!(err.valid.contains(&"ssid"));
}
#[test]
fn rejects_an_unknown_field_mixed_with_known_ones() {
let err = validate("mac,bogus,ip", CLIENTS_LIST).unwrap_err();
assert_eq!(err.unknown, vec!["bogus"]);
}
#[test]
fn reports_every_unknown_field() {
let err = validate("bogus,mac,nope", CLIENTS_LIST).unwrap_err();
assert_eq!(err.unknown, vec!["bogus", "nope"]);
}
#[test]
fn an_all_empty_spec_selects_nothing_rather_than_erroring() {
assert!(validate("", CLIENTS_LIST).unwrap().is_empty());
assert!(validate(" , ", CLIENTS_LIST).unwrap().is_empty());
}
#[test]
fn error_message_names_the_offender_and_the_valid_set() {
let msg = validate("bogus", CLIENTS_LIST).unwrap_err().to_string();
assert!(msg.contains("bogus"), "{msg}");
assert!(msg.contains("Valid fields:"), "{msg}");
assert!(msg.contains("ssid"), "{msg}");
assert!(msg.contains("field in --fields"), "{msg}");
}
#[test]
fn error_message_pluralises() {
let msg = validate("a,b", CLIENTS_LIST).unwrap_err().to_string();
assert!(msg.contains("fields in --fields"), "{msg}");
}
#[test]
fn every_table_has_unique_field_names() {
for table in [
CLIENTS_LIST,
CLIENTS_SHOW,
DEVICES_LIST,
EVENTS_LIST,
NETWORKS_LIST,
PORTS_LIST,
PORTS_FIND,
] {
let mut seen = names(table);
let before = seen.len();
seen.sort_unstable();
seen.dedup();
assert_eq!(before, seen.len(), "duplicate field name in table");
}
}
#[test]
fn every_field_declares_a_json_type() {
for table in [
CLIENTS_LIST,
CLIENTS_SHOW,
DEVICES_LIST,
EVENTS_LIST,
NETWORKS_LIST,
PORTS_LIST,
PORTS_FIND,
] {
for (name, ty) in table {
assert!(
["string", "integer", "boolean", "number"].contains(ty),
"field {name} has unexpected type {ty}"
);
}
}
}
#[test]
fn clients_list_can_project_everything_clients_show_reports() {
for (name, _) in CLIENTS_SHOW {
if *name == "wired" || *name == "ap_mac" {
continue; }
assert!(
names(CLIENTS_LIST).contains(name),
"clients list cannot project {name}, which clients show reports"
);
}
}
}