Skip to main content

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}