ticket: use list state tokens in query

This commit is contained in:
Keisuke Hirata 2026-07-20 17:16:40 +09:00
parent 39be7a5a0a
commit 556adb429c
No known key found for this signature in database
3 changed files with 89 additions and 29 deletions

View File

@ -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<Self> {
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)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum TicketStateSelector { pub enum TicketStateSelector {
@ -517,8 +564,8 @@ pub enum TicketStateSelector {
Active, Active,
/// Every workflow state, including closed. /// Every workflow state, including closed.
All, All,
/// An explicit set of workflow states. /// An explicit set of list-query state tokens.
States(BTreeSet<TicketWorkflowState>), States(BTreeSet<TicketListState>),
} }
impl Default for TicketStateSelector { 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]) Self::states([state])
} }
pub fn states(states: impl IntoIterator<Item = TicketWorkflowState>) -> Self { pub fn states(states: impl IntoIterator<Item = TicketListState>) -> Self {
Self { Self {
state: TicketStateSelector::States(states.into_iter().collect()), state: TicketStateSelector::States(states.into_iter().collect()),
} }
@ -565,7 +612,9 @@ impl TicketListQuery {
match &self.state { match &self.state {
TicketStateSelector::Active => state != TicketWorkflowState::Closed, TicketStateSelector::Active => state != TicketWorkflowState::Closed,
TicketStateSelector::All => true, 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())); assert!(all_ids.contains(&closed.id.as_str()));
let ready_only = backend let ready_only = backend
.list(TicketListQuery::state(TicketWorkflowState::Ready)) .list(TicketListQuery::state(TicketListState::Ready))
.unwrap(); .unwrap();
assert_eq!(ready_only.len(), 1); assert_eq!(ready_only.len(), 1);
assert_eq!(ready_only[0].id, ready.id); assert_eq!(ready_only[0].id, ready.id);
let planning_or_closed = backend let planning_or_closed = backend
.list(TicketListQuery::states([ .list(TicketListQuery::states([
TicketWorkflowState::Planning, TicketListState::Planning,
TicketWorkflowState::Closed, TicketListState::Closed,
])) ]))
.unwrap(); .unwrap();
let explicit_ids = planning_or_closed let explicit_ids = planning_or_closed

View File

@ -16,8 +16,8 @@ use crate::{
NewTicket, NewTicketEvent, NewTicketRelation, OrchestrationPlanKind, OrchestrationPlanRecord, NewTicket, NewTicketEvent, NewTicketRelation, OrchestrationPlanKind, OrchestrationPlanRecord,
Result as TicketResult, Ticket, TicketBackend, TicketDoctorDiagnostic, TicketDoctorReport, Result as TicketResult, Ticket, TicketBackend, TicketDoctorDiagnostic, TicketDoctorReport,
TicketDoctorSeverity, TicketError, TicketEventKind, TicketIdOrSlug, TicketIntakeSummary, TicketDoctorSeverity, TicketError, TicketEventKind, TicketIdOrSlug, TicketIntakeSummary,
TicketRef, TicketRelation, TicketRelationKind, TicketRelationView, TicketReview, TicketListState, TicketRef, TicketRelation, TicketRelationKind, TicketRelationView,
TicketReviewResult, TicketStateChange, TicketSummary, TicketWorkflowState, TicketReview, TicketReviewResult, TicketStateChange, TicketSummary, TicketWorkflowState,
}; };
const DEFAULT_LIST_LIMIT: usize = 50; const DEFAULT_LIST_LIMIT: usize = 50;
@ -373,6 +373,17 @@ impl TicketWorkflowStateParam {
Self::Closed => TicketWorkflowState::Closed, 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)] #[derive(Debug, Clone, Copy, Deserialize, schemars::JsonSchema)]
@ -389,14 +400,14 @@ enum TicketListStateParam {
} }
impl TicketListStateParam { impl TicketListStateParam {
fn as_state(self) -> Option<TicketWorkflowState> { fn as_list_state(self) -> Option<TicketListState> {
match self { match self {
Self::Planning => Some(TicketWorkflowState::Planning), Self::Planning => Some(TicketListState::Planning),
Self::Ready => Some(TicketWorkflowState::Ready), Self::Ready => Some(TicketListState::Ready),
Self::Queued => Some(TicketWorkflowState::Queued), Self::Queued => Some(TicketListState::Queued),
Self::Inprogress => Some(TicketWorkflowState::InProgress), Self::Inprogress => Some(TicketListState::InProgress),
Self::Done => Some(TicketWorkflowState::Done), Self::Done => Some(TicketListState::Done),
Self::Closed => Some(TicketWorkflowState::Closed), Self::Closed => Some(TicketListState::Closed),
Self::Active | Self::All => None, Self::Active | Self::All => None,
} }
} }
@ -428,15 +439,15 @@ impl TicketListParams {
"TicketList `states` must include at least one workflow state".to_string(), "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 { } else {
match self.state.unwrap_or(TicketListStateParam::Active) { match self.state.unwrap_or(TicketListStateParam::Active) {
TicketListStateParam::Active => crate::TicketListQuery::active(), TicketListStateParam::Active => crate::TicketListQuery::active(),
TicketListStateParam::All => crate::TicketListQuery::all(), TicketListStateParam::All => crate::TicketListQuery::all(),
state => crate::TicketListQuery::state( state => crate::TicketListQuery::state(
state state
.as_state() .as_list_state()
.expect("workflow state list param maps to TicketWorkflowState"), .expect("workflow state list param maps to TicketListState"),
), ),
} }
}; };

View File

@ -12,7 +12,8 @@ use ticket::config::{
use ticket::{ use ticket::{
LocalTicketBackend, MarkdownText, NewTicket, NewTicketEvent, NewTicketRelation, TicketBackend, LocalTicketBackend, MarkdownText, NewTicket, NewTicketEvent, NewTicketRelation, TicketBackend,
TicketDoctorSeverity, TicketEventKind, TicketIdOrSlug, TicketIntakeSummary, TicketListQuery, TicketDoctorSeverity, TicketEventKind, TicketIdOrSlug, TicketIntakeSummary, TicketListQuery,
TicketRelationKind, TicketReview, TicketReviewResult, TicketSummary, TicketWorkflowState, TicketListState, TicketRelationKind, TicketReview, TicketReviewResult, TicketSummary,
TicketWorkflowState,
}; };
const DEFAULT_LIST_LIMIT: usize = 50; const DEFAULT_LIST_LIMIT: usize = 50;
@ -49,7 +50,7 @@ pub struct CreateOptions {
pub enum ListState { pub enum ListState {
Active, Active,
All, All,
States(Vec<TicketWorkflowState>), States(Vec<TicketListState>),
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@ -1060,7 +1061,7 @@ fn parse_list_state(raw: &str) -> Result<ListState, TicketCliError> {
let mut states = Vec::new(); let mut states = Vec::new();
for token in tokens { for token in tokens {
let state = TicketWorkflowState::parse(token).ok_or_else(|| { let state = TicketListState::parse(token).ok_or_else(|| {
TicketCliError::new(format!( TicketCliError::new(format!(
"invalid state: {token}; expected active, all, planning, ready, queued, inprogress, done, closed" "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() { fn ticket_cli_list_defaults_to_active_and_accepts_multi_state_filter() {
let default = parse_ticket_args(&args(&["list"])).unwrap(); let default = parse_ticket_args(&args(&["list"])).unwrap();
match default { 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:?}"), other => panic!("unexpected command: {other:?}"),
} }
let explicit = parse_ticket_args(&args(&["list", "--state", "planning,closed"])).unwrap(); let explicit = parse_ticket_args(&args(&["list", "--state", "planning,closed"])).unwrap();
match explicit { match explicit {
TicketCommand::List(options) => assert_eq!( TicketCli::Command(TicketCommand::List(options)) => assert_eq!(
options.state, options.state,
ListState::States(vec![ ListState::States(vec![TicketListState::Planning, TicketListState::Closed])
TicketWorkflowState::Planning,
TicketWorkflowState::Closed
])
), ),
other => panic!("unexpected command: {other:?}"), other => panic!("unexpected command: {other:?}"),
} }