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)]
#[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<TicketWorkflowState>),
/// An explicit set of list-query state tokens.
States(BTreeSet<TicketListState>),
}
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<Item = TicketWorkflowState>) -> Self {
pub fn states(states: impl IntoIterator<Item = TicketListState>) -> 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

View File

@ -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<TicketWorkflowState> {
fn as_list_state(self) -> Option<TicketListState> {
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"),
),
}
};

View File

@ -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<TicketWorkflowState>),
States(Vec<TicketListState>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
@ -1060,7 +1061,7 @@ fn parse_list_state(raw: &str) -> Result<ListState, TicketCliError> {
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:?}"),
}