From 556adb429c82fb6c03e1c78ea7209668d87253da Mon Sep 17 00:00:00 2001 From: Hare Date: Mon, 20 Jul 2026 17:16:40 +0900 Subject: [PATCH] ticket: use list state tokens in query --- crates/ticket/src/lib.rs | 65 +++++++++++++++++++++++++++++++----- crates/ticket/src/tool.rs | 35 ++++++++++++------- crates/yoi/src/ticket_cli.rs | 18 +++++----- 3 files changed, 89 insertions(+), 29 deletions(-) diff --git a/crates/ticket/src/lib.rs b/crates/ticket/src/lib.rs index a68dd4d4..49ffcab5 100644 --- a/crates/ticket/src/lib.rs +++ b/crates/ticket/src/lib.rs @@ -510,6 +510,53 @@ impl NewTicket { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum TicketListState { + Planning, + Ready, + Queued, + InProgress, + Done, + Closed, +} + +impl TicketListState { + pub fn parse(value: &str) -> Option { + match value { + "planning" => Some(Self::Planning), + "ready" => Some(Self::Ready), + "queued" => Some(Self::Queued), + "inprogress" => Some(Self::InProgress), + "done" => Some(Self::Done), + "closed" => Some(Self::Closed), + _ => None, + } + } + + pub fn as_str(self) -> &'static str { + match self { + Self::Planning => "planning", + Self::Ready => "ready", + Self::Queued => "queued", + Self::InProgress => "inprogress", + Self::Done => "done", + Self::Closed => "closed", + } + } + + fn matches_workflow_state(self, state: TicketWorkflowState) -> bool { + match self { + Self::Planning => state == TicketWorkflowState::Planning, + Self::Ready => state == TicketWorkflowState::Ready, + Self::Queued => state == TicketWorkflowState::Queued, + Self::InProgress => state == TicketWorkflowState::InProgress, + Self::Done => state == TicketWorkflowState::Done, + Self::Closed => state == TicketWorkflowState::Closed, + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum TicketStateSelector { @@ -517,8 +564,8 @@ pub enum TicketStateSelector { Active, /// Every workflow state, including closed. All, - /// An explicit set of workflow states. - States(BTreeSet), + /// An explicit set of list-query state tokens. + States(BTreeSet), } impl Default for TicketStateSelector { @@ -551,11 +598,11 @@ impl TicketListQuery { } } - pub fn state(state: TicketWorkflowState) -> Self { + pub fn state(state: TicketListState) -> Self { Self::states([state]) } - pub fn states(states: impl IntoIterator) -> Self { + pub fn states(states: impl IntoIterator) -> Self { Self { state: TicketStateSelector::States(states.into_iter().collect()), } @@ -565,7 +612,9 @@ impl TicketListQuery { match &self.state { TicketStateSelector::Active => state != TicketWorkflowState::Closed, TicketStateSelector::All => true, - TicketStateSelector::States(states) => states.contains(&state), + TicketStateSelector::States(states) => states + .iter() + .any(|query_state| query_state.matches_workflow_state(state)), } } @@ -4060,15 +4109,15 @@ state: planning assert!(all_ids.contains(&closed.id.as_str())); let ready_only = backend - .list(TicketListQuery::state(TicketWorkflowState::Ready)) + .list(TicketListQuery::state(TicketListState::Ready)) .unwrap(); assert_eq!(ready_only.len(), 1); assert_eq!(ready_only[0].id, ready.id); let planning_or_closed = backend .list(TicketListQuery::states([ - TicketWorkflowState::Planning, - TicketWorkflowState::Closed, + TicketListState::Planning, + TicketListState::Closed, ])) .unwrap(); let explicit_ids = planning_or_closed diff --git a/crates/ticket/src/tool.rs b/crates/ticket/src/tool.rs index 33dea587..0d4f2a8d 100644 --- a/crates/ticket/src/tool.rs +++ b/crates/ticket/src/tool.rs @@ -16,8 +16,8 @@ use crate::{ NewTicket, NewTicketEvent, NewTicketRelation, OrchestrationPlanKind, OrchestrationPlanRecord, Result as TicketResult, Ticket, TicketBackend, TicketDoctorDiagnostic, TicketDoctorReport, TicketDoctorSeverity, TicketError, TicketEventKind, TicketIdOrSlug, TicketIntakeSummary, - TicketRef, TicketRelation, TicketRelationKind, TicketRelationView, TicketReview, - TicketReviewResult, TicketStateChange, TicketSummary, TicketWorkflowState, + TicketListState, TicketRef, TicketRelation, TicketRelationKind, TicketRelationView, + TicketReview, TicketReviewResult, TicketStateChange, TicketSummary, TicketWorkflowState, }; const DEFAULT_LIST_LIMIT: usize = 50; @@ -373,6 +373,17 @@ impl TicketWorkflowStateParam { Self::Closed => TicketWorkflowState::Closed, } } + + fn into_list_state(self) -> TicketListState { + match self { + Self::Planning => TicketListState::Planning, + Self::Ready => TicketListState::Ready, + Self::Queued => TicketListState::Queued, + Self::Inprogress => TicketListState::InProgress, + Self::Done => TicketListState::Done, + Self::Closed => TicketListState::Closed, + } + } } #[derive(Debug, Clone, Copy, Deserialize, schemars::JsonSchema)] @@ -389,14 +400,14 @@ enum TicketListStateParam { } impl TicketListStateParam { - fn as_state(self) -> Option { + fn as_list_state(self) -> Option { match self { - Self::Planning => Some(TicketWorkflowState::Planning), - Self::Ready => Some(TicketWorkflowState::Ready), - Self::Queued => Some(TicketWorkflowState::Queued), - Self::Inprogress => Some(TicketWorkflowState::InProgress), - Self::Done => Some(TicketWorkflowState::Done), - Self::Closed => Some(TicketWorkflowState::Closed), + Self::Planning => Some(TicketListState::Planning), + Self::Ready => Some(TicketListState::Ready), + Self::Queued => Some(TicketListState::Queued), + Self::Inprogress => Some(TicketListState::InProgress), + Self::Done => Some(TicketListState::Done), + Self::Closed => Some(TicketListState::Closed), Self::Active | Self::All => None, } } @@ -428,15 +439,15 @@ impl TicketListParams { "TicketList `states` must include at least one workflow state".to_string(), )); } - crate::TicketListQuery::states(states.into_iter().map(|state| state.into_state())) + crate::TicketListQuery::states(states.into_iter().map(|state| state.into_list_state())) } else { match self.state.unwrap_or(TicketListStateParam::Active) { TicketListStateParam::Active => crate::TicketListQuery::active(), TicketListStateParam::All => crate::TicketListQuery::all(), state => crate::TicketListQuery::state( state - .as_state() - .expect("workflow state list param maps to TicketWorkflowState"), + .as_list_state() + .expect("workflow state list param maps to TicketListState"), ), } }; diff --git a/crates/yoi/src/ticket_cli.rs b/crates/yoi/src/ticket_cli.rs index f3a26475..3d8312df 100644 --- a/crates/yoi/src/ticket_cli.rs +++ b/crates/yoi/src/ticket_cli.rs @@ -12,7 +12,8 @@ use ticket::config::{ use ticket::{ LocalTicketBackend, MarkdownText, NewTicket, NewTicketEvent, NewTicketRelation, TicketBackend, TicketDoctorSeverity, TicketEventKind, TicketIdOrSlug, TicketIntakeSummary, TicketListQuery, - TicketRelationKind, TicketReview, TicketReviewResult, TicketSummary, TicketWorkflowState, + TicketListState, TicketRelationKind, TicketReview, TicketReviewResult, TicketSummary, + TicketWorkflowState, }; const DEFAULT_LIST_LIMIT: usize = 50; @@ -49,7 +50,7 @@ pub struct CreateOptions { pub enum ListState { Active, All, - States(Vec), + States(Vec), } #[derive(Debug, Clone, PartialEq, Eq)] @@ -1060,7 +1061,7 @@ fn parse_list_state(raw: &str) -> Result { let mut states = Vec::new(); for token in tokens { - let state = TicketWorkflowState::parse(token).ok_or_else(|| { + let state = TicketListState::parse(token).ok_or_else(|| { TicketCliError::new(format!( "invalid state: {token}; expected active, all, planning, ready, queued, inprogress, done, closed" )) @@ -1554,18 +1555,17 @@ mod tests { fn ticket_cli_list_defaults_to_active_and_accepts_multi_state_filter() { let default = parse_ticket_args(&args(&["list"])).unwrap(); match default { - TicketCommand::List(options) => assert_eq!(options.state, ListState::Active), + TicketCli::Command(TicketCommand::List(options)) => { + assert_eq!(options.state, ListState::Active) + } other => panic!("unexpected command: {other:?}"), } let explicit = parse_ticket_args(&args(&["list", "--state", "planning,closed"])).unwrap(); match explicit { - TicketCommand::List(options) => assert_eq!( + TicketCli::Command(TicketCommand::List(options)) => assert_eq!( options.state, - ListState::States(vec![ - TicketWorkflowState::Planning, - TicketWorkflowState::Closed - ]) + ListState::States(vec![TicketListState::Planning, TicketListState::Closed]) ), other => panic!("unexpected command: {other:?}"), }