address review: move parse_states/filter_nodes_by_state to hive-jobq-wire, rename placeholder enums, trim core-mirroring framing
This commit is contained in:
parent
962b7e60f8
commit
08efd7875e
2 changed files with 118 additions and 59 deletions
|
|
@ -354,6 +354,46 @@ fn state_index(state: State) -> usize {
|
|||
}
|
||||
}
|
||||
|
||||
/// Parses a `?states=` query value (comma-separated [`State`] names, e.g.
|
||||
/// `"Running,Pending"`) into the filter [`filter_nodes_by_state`] wants.
|
||||
///
|
||||
/// Absent or entirely-unparseable input is "no filter," never "match
|
||||
/// nothing" — an empty/garbled query reads as the unfiltered call it
|
||||
/// replaces rather than as a request for zero results. Unrecognised tokens
|
||||
/// are silently dropped rather than erroring the whole request.
|
||||
///
|
||||
/// Lives here rather than in each host's own dashboard/API layer because
|
||||
/// every host that serves [`GraphWire::wire_snapshot`] over HTTP wants the
|
||||
/// same query shape — a second, independently-typed copy is exactly the
|
||||
/// kind of drift this crate exists to prevent (see the module doc comment).
|
||||
#[must_use]
|
||||
pub fn parse_states(raw: Option<&str>) -> Option<Vec<State>> {
|
||||
let states: Vec<State> = raw?
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.filter_map(|s| serde_json::from_value(serde_json::Value::String(s.to_owned())).ok())
|
||||
.collect();
|
||||
(!states.is_empty()).then_some(states)
|
||||
}
|
||||
|
||||
/// Keeps only the nodes whose own `state` is named in `states` — a flat
|
||||
/// filter on [`GraphNode::state`], no special-casing for a root vs. a
|
||||
/// descendant (a root's own state already *is* its subtree's rolled-up
|
||||
/// answer, per the module doc comment, so filtering the root filters the
|
||||
/// whole group). `None` is a no-op, not "match nothing," mirroring
|
||||
/// [`parse_states`]'s own absent-input rule.
|
||||
#[must_use]
|
||||
pub fn filter_nodes_by_state(nodes: Vec<GraphNode>, states: Option<&[State]>) -> Vec<GraphNode> {
|
||||
let Some(states) = states else {
|
||||
return nodes;
|
||||
};
|
||||
nodes
|
||||
.into_iter()
|
||||
.filter(|n| states.contains(&n.state))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn wire_node<N: WireNode, R: WireResource>(node: &hive_jobq::Node<N, R>) -> GraphNode {
|
||||
let id = node.id.get();
|
||||
GraphNode {
|
||||
|
|
@ -400,7 +440,8 @@ mod tests {
|
|||
|
||||
use super::{
|
||||
ALL_STATES, BitFlags, GraphDep, GraphNode, GraphWire, NodePayload, State, StateSchema,
|
||||
TerminalState, TerminalStateSchema, WireId, WireNode, state_rollup,
|
||||
TerminalState, TerminalStateSchema, WireId, WireNode, filter_nodes_by_state, parse_states,
|
||||
state_rollup,
|
||||
};
|
||||
|
||||
/// The mirrors document what the wire actually says — a mirror that
|
||||
|
|
@ -630,6 +671,50 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_states_covers_absent_malformed_and_valid() {
|
||||
assert_eq!(parse_states(None), None, "no query is no filter");
|
||||
assert_eq!(
|
||||
parse_states(Some("")),
|
||||
None,
|
||||
"an empty query is no filter, not a request for zero results"
|
||||
);
|
||||
assert_eq!(
|
||||
parse_states(Some("not-a-state,also-not-one")),
|
||||
None,
|
||||
"every token failing to parse is still no filter"
|
||||
);
|
||||
assert_eq!(
|
||||
parse_states(Some(" Running , Pending ,bogus")),
|
||||
Some(vec![State::Running, State::Pending]),
|
||||
"trims whitespace, keeps the valid tokens, drops the unrecognised one"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_nodes_by_state_keeps_matching_nodes_from_a_mixed_state_tree() {
|
||||
let nodes = vec![
|
||||
node(1, None, State::Running, Vec::new()), // root: whole group still live
|
||||
node(2, Some(1), State::Done, Vec::new()), // finished step, should be hidden
|
||||
node(3, Some(1), State::Pending, Vec::new()), // not-yet-run step, should stay
|
||||
];
|
||||
|
||||
let mut kept: Vec<WireId> =
|
||||
filter_nodes_by_state(nodes.clone(), Some(&[State::Running, State::Pending]))
|
||||
.into_iter()
|
||||
.map(|n| n.id)
|
||||
.collect();
|
||||
kept.sort_unstable();
|
||||
assert_eq!(
|
||||
kept,
|
||||
vec![1, 3],
|
||||
"Done is filtered out even though its still-live parent (root) isn't"
|
||||
);
|
||||
|
||||
let unfiltered = filter_nodes_by_state(nodes, None);
|
||||
assert_eq!(unfiltered.len(), 3, "no states given is a no-op, not empty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_payload_slot_is_omitted_from_the_wire() {
|
||||
// The opaque slot costs nothing when a host has nothing to say.
|
||||
|
|
|
|||
Loading…
Reference in a new issue