dashboard: filter /dashboard/stream by ?kinds= allow-list (#408)
This commit is contained in:
parent
f540584553
commit
d143abb63d
2 changed files with 193 additions and 1 deletions
|
|
@ -218,3 +218,160 @@ pub enum DashboardEvent {
|
|||
queue: Vec<QueueEntry>,
|
||||
},
|
||||
}
|
||||
|
||||
impl DashboardEvent {
|
||||
/// Snake-case identifier matching this variant's serde `tag`
|
||||
/// (e.g. `Sent` → `"sent"`, `ContainerStateChanged` →
|
||||
/// `"container_state_changed"`). Lets `/dashboard/stream`'s
|
||||
/// `?kinds=` filter (#408) decide whether to forward a frame
|
||||
/// without paying the JSON-serialise cost first.
|
||||
///
|
||||
/// Keep in sync with `#[serde(rename_all = "snake_case", tag =
|
||||
/// "kind")]` on `DashboardEvent` — if a new variant lands above,
|
||||
/// add it here too. `cargo test` covers this via the
|
||||
/// `kind_tag_matches_serde_kind_field` round-trip test.
|
||||
#[must_use]
|
||||
pub fn kind_tag(&self) -> &'static str {
|
||||
match self {
|
||||
DashboardEvent::Sent { .. } => "sent",
|
||||
DashboardEvent::Delivered { .. } => "delivered",
|
||||
DashboardEvent::ApprovalAdded { .. } => "approval_added",
|
||||
DashboardEvent::ApprovalResolved { .. } => "approval_resolved",
|
||||
DashboardEvent::QuestionAdded { .. } => "question_added",
|
||||
DashboardEvent::QuestionResolved { .. } => "question_resolved",
|
||||
DashboardEvent::TransientSet { .. } => "transient_set",
|
||||
DashboardEvent::TransientCleared { .. } => "transient_cleared",
|
||||
DashboardEvent::ContainerStateChanged { .. } => "container_state_changed",
|
||||
DashboardEvent::ContainerRemoved { .. } => "container_removed",
|
||||
DashboardEvent::TombstonesChanged { .. } => "tombstones_changed",
|
||||
DashboardEvent::MetaInputsChanged { .. } => "meta_inputs_changed",
|
||||
DashboardEvent::MetaUpdateRunning { .. } => "meta_update_running",
|
||||
DashboardEvent::RebuildQueueChanged { .. } => "rebuild_queue_changed",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Round-trip representative variants through serde and confirm
|
||||
/// the `kind` JSON field matches `kind_tag()`. The exhaustive
|
||||
/// `match` in `kind_tag` already provides compile-time variant
|
||||
/// coverage — this test is the value-side guard against
|
||||
/// typos in the snake_case strings vs serde's `rename_all`
|
||||
/// output. `ContainerStateChanged` is omitted from the sample
|
||||
/// list only because `ContainerView` has no `Default` impl and
|
||||
/// constructing one inline here is more boilerplate than the
|
||||
/// test is worth; the variant is still covered by the
|
||||
/// `kind_tag` match arm.
|
||||
#[test]
|
||||
fn kind_tag_matches_serde_kind_field() {
|
||||
let samples: Vec<DashboardEvent> = vec![
|
||||
DashboardEvent::Sent {
|
||||
seq: 1,
|
||||
id: 1,
|
||||
from: "a".into(),
|
||||
to: "b".into(),
|
||||
body: String::new(),
|
||||
at: 0,
|
||||
in_reply_to: None,
|
||||
file_refs: Vec::new(),
|
||||
},
|
||||
DashboardEvent::Delivered {
|
||||
seq: 1,
|
||||
id: 1,
|
||||
from: "a".into(),
|
||||
to: "b".into(),
|
||||
body: String::new(),
|
||||
at: 0,
|
||||
in_reply_to: None,
|
||||
file_refs: Vec::new(),
|
||||
},
|
||||
DashboardEvent::ApprovalAdded {
|
||||
seq: 1,
|
||||
id: 1,
|
||||
agent: "x".into(),
|
||||
approval_kind: "apply_commit",
|
||||
sha_short: None,
|
||||
diff: None,
|
||||
description: None,
|
||||
},
|
||||
DashboardEvent::ApprovalResolved {
|
||||
seq: 1,
|
||||
id: 1,
|
||||
agent: "x".into(),
|
||||
approval_kind: "apply_commit",
|
||||
sha_short: None,
|
||||
status: "approved",
|
||||
resolved_at: 0,
|
||||
note: None,
|
||||
description: None,
|
||||
},
|
||||
DashboardEvent::QuestionAdded {
|
||||
seq: 1,
|
||||
id: 1,
|
||||
asker: "a".into(),
|
||||
question: String::new(),
|
||||
options: Vec::new(),
|
||||
multi: false,
|
||||
asked_at: 0,
|
||||
deadline_at: None,
|
||||
target: None,
|
||||
question_refs: Vec::new(),
|
||||
},
|
||||
DashboardEvent::QuestionResolved {
|
||||
seq: 1,
|
||||
id: 1,
|
||||
answer: String::new(),
|
||||
answerer: "a".into(),
|
||||
answered_at: 0,
|
||||
cancelled: false,
|
||||
target: None,
|
||||
answer_refs: Vec::new(),
|
||||
},
|
||||
DashboardEvent::TransientSet {
|
||||
seq: 1,
|
||||
name: "x".into(),
|
||||
transient_kind: "rebuilding",
|
||||
since_unix: 0,
|
||||
},
|
||||
DashboardEvent::TransientCleared {
|
||||
seq: 1,
|
||||
name: "x".into(),
|
||||
},
|
||||
DashboardEvent::ContainerRemoved {
|
||||
seq: 1,
|
||||
name: "x".into(),
|
||||
},
|
||||
DashboardEvent::TombstonesChanged {
|
||||
seq: 1,
|
||||
tombstones: Vec::new(),
|
||||
},
|
||||
DashboardEvent::MetaInputsChanged {
|
||||
seq: 1,
|
||||
inputs: Vec::new(),
|
||||
},
|
||||
DashboardEvent::MetaUpdateRunning {
|
||||
seq: 1,
|
||||
running: false,
|
||||
},
|
||||
DashboardEvent::RebuildQueueChanged {
|
||||
seq: 1,
|
||||
queue: Vec::new(),
|
||||
},
|
||||
];
|
||||
for ev in samples {
|
||||
let v: serde_json::Value = serde_json::to_value(&ev).expect("serialise");
|
||||
let serde_kind = v
|
||||
.get("kind")
|
||||
.and_then(|k| k.as_str())
|
||||
.expect("kind field present");
|
||||
assert_eq!(
|
||||
ev.kind_tag(),
|
||||
serde_kind,
|
||||
"kind_tag() drift on {ev:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue