solti_model/domain/query/task.rs
1//! # Task query builder.
2//!
3//! [`TaskQuery`] and [`TaskPage`] support filtered, paginated task listing.
4
5use crate::{Slot, TaskPhase};
6
7/// Default page size when the caller does not specify one.
8pub const DEFAULT_LIMIT: usize = 100;
9
10/// Hard cap on page size.
11///
12/// [`TaskQuery::with_limit`] clamps values above this silently;
13/// Upstream transports should reject oversized limits explicitly if they expose a wire contract.
14pub const MAX_LIMIT: usize = 1000;
15
16/// Query parameters for listing tasks with filtering and pagination.
17///
18/// ```text
19/// TaskQuery::new()
20/// .with_slot("build") // filter by slot
21/// .with_active() // Pending | Running
22/// .with_limit(50)
23/// .with_offset(0)
24/// │
25/// ▼ state.query(&q)
26/// TaskPage { items: [Task, ...], total: 123 }
27/// ```
28///
29/// An empty `status` filter matches **all** phases (no filtering).
30/// Multiple [`with_status`](Self::with_status) calls accumulate with OR semantics.
31///
32/// ## Also
33///
34/// - [`TaskPage`] paginated result returned by state queries.
35/// - [`TaskPhase`](crate::TaskPhase) phase values used as status filters.
36#[derive(Debug, Clone)]
37pub struct TaskQuery {
38 status: Vec<TaskPhase>,
39 slot: Option<Slot>,
40 offset: usize,
41 limit: usize,
42}
43
44impl Default for TaskQuery {
45 #[inline]
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51/// Result of a paginated task query.
52#[derive(Debug, Clone)]
53pub struct TaskPage<T> {
54 pub items: Vec<T>,
55 pub total: usize,
56}
57
58impl TaskQuery {
59 /// Create a new query with default pagination (`limit=100`, `offset=0`) and without filters.
60 #[inline]
61 pub fn new() -> Self {
62 Self {
63 limit: DEFAULT_LIMIT,
64 status: Vec::new(),
65 slot: None,
66 offset: 0,
67 }
68 }
69
70 /// Filter by slot name.
71 #[inline]
72 pub fn with_slot(mut self, slot: impl Into<Slot>) -> Self {
73 self.slot = Some(slot.into());
74 self
75 }
76
77 /// Add a phase filter. Multiple calls accumulate (OR semantics).
78 #[inline]
79 pub fn with_status(mut self, status: TaskPhase) -> Self {
80 if !self.status.contains(&status) {
81 self.status.push(status);
82 }
83 self
84 }
85
86 /// Filter by all active phases (`Pending`, `Running`).
87 #[inline]
88 pub fn with_active(self) -> Self {
89 self.with_status(TaskPhase::Pending)
90 .with_status(TaskPhase::Running)
91 }
92
93 /// Filter by all terminal phases.
94 #[inline]
95 pub fn with_terminal(self) -> Self {
96 self.with_status(TaskPhase::Succeeded)
97 .with_status(TaskPhase::Exhausted)
98 .with_status(TaskPhase::Canceled)
99 .with_status(TaskPhase::Timeout)
100 .with_status(TaskPhase::Failed)
101 }
102
103 /// Set page size. Capped at `1000`.
104 #[inline]
105 pub fn with_limit(mut self, limit: usize) -> Self {
106 self.limit = limit.min(MAX_LIMIT);
107 self
108 }
109
110 /// Set the starting offset for pagination.
111 #[inline]
112 pub fn with_offset(mut self, offset: usize) -> Self {
113 self.offset = offset;
114 self
115 }
116
117 /// Returns `true` if the given phase passes the status filter.
118 ///
119 /// An empty filter matches all phases.
120 #[inline]
121 pub fn matches_phase(&self, phase: &TaskPhase) -> bool {
122 self.status.is_empty() || self.status.contains(phase)
123 }
124
125 /// Page size limit.
126 #[inline]
127 pub fn limit(&self) -> usize {
128 self.limit
129 }
130
131 /// Starting offset for pagination.
132 #[inline]
133 pub fn offset(&self) -> usize {
134 self.offset
135 }
136
137 /// Slot filter (if any).
138 #[inline]
139 pub fn slot(&self) -> Option<&Slot> {
140 self.slot.as_ref()
141 }
142
143 /// Status filters.
144 #[inline]
145 pub fn status_filters(&self) -> &[TaskPhase] {
146 &self.status
147 }
148}