Skip to main content

frp_loom/
query.rs

1// ---------------------------------------------------------------------------
2// Query
3// ---------------------------------------------------------------------------
4
5/// A composable filter + pagination descriptor for store queries.
6///
7/// Build via the fluent API:
8/// ```rust
9/// # use frp_loom::query::Query;
10/// let q = Query::new()
11///     .kind("Atom::Source")
12///     .tag("layer:domain")
13///     .limit(20)
14///     .offset(40);
15/// ```
16#[derive(Debug, Clone, Default)]
17pub struct Query {
18    /// If set, only return entities whose `kind` string matches this value.
19    pub kind_filter: Option<String>,
20    /// All listed tags must be present on the entity (AND semantics).
21    pub tag_filter: Vec<String>,
22    /// Maximum number of items to return. `None` means no limit.
23    pub limit: Option<usize>,
24    /// Number of matching items to skip before returning results.
25    pub offset: usize,
26}
27
28impl Query {
29    /// Create a new, unconstrained query.
30    pub fn new() -> Self {
31        Self::default()
32    }
33
34    /// Filter by entity kind (exact match).
35    pub fn kind(mut self, k: impl Into<String>) -> Self {
36        self.kind_filter = Some(k.into());
37        self
38    }
39
40    /// Require a specific tag to be present on matching entities.
41    /// Multiple calls accumulate (AND semantics).
42    pub fn tag(mut self, t: impl Into<String>) -> Self {
43        self.tag_filter.push(t.into());
44        self
45    }
46
47    /// Maximum number of results to return.
48    pub fn limit(mut self, n: usize) -> Self {
49        self.limit = Some(n);
50        self
51    }
52
53    /// Number of matching items to skip (for pagination).
54    pub fn offset(mut self, n: usize) -> Self {
55        self.offset = n;
56        self
57    }
58}
59
60// ---------------------------------------------------------------------------
61// QueryResult
62// ---------------------------------------------------------------------------
63
64/// The result of a store query, including pagination metadata.
65#[derive(Debug, Clone)]
66pub struct QueryResult<T> {
67    /// The matched items in this page.
68    pub items: Vec<T>,
69    /// Total number of matching items before pagination was applied.
70    pub total: usize,
71    /// The offset that was used to produce this page.
72    pub offset: usize,
73}
74
75impl<T> QueryResult<T> {
76    /// Construct a new `QueryResult`.
77    pub fn new(items: Vec<T>, total: usize, offset: usize) -> Self {
78        Self { items, total, offset }
79    }
80
81    /// An empty result with zero total.
82    pub fn empty() -> Self {
83        Self { items: Vec::new(), total: 0, offset: 0 }
84    }
85
86    /// Number of items in this page.
87    pub fn len(&self) -> usize {
88        self.items.len()
89    }
90
91    /// Returns `true` if no items are in this page.
92    pub fn is_empty(&self) -> bool {
93        self.items.is_empty()
94    }
95}
96
97// ---------------------------------------------------------------------------
98// Tests
99// ---------------------------------------------------------------------------
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn query_builder_sets_fields() {
107        let q = Query::new().kind("Source").tag("a").tag("b").limit(10).offset(5);
108        assert_eq!(q.kind_filter.as_deref(), Some("Source"));
109        assert_eq!(q.tag_filter, vec!["a", "b"]);
110        assert_eq!(q.limit, Some(10));
111        assert_eq!(q.offset, 5);
112    }
113
114    #[test]
115    fn query_default_is_unconstrained() {
116        let q = Query::new();
117        assert!(q.kind_filter.is_none());
118        assert!(q.tag_filter.is_empty());
119        assert!(q.limit.is_none());
120        assert_eq!(q.offset, 0);
121    }
122
123    #[test]
124    fn query_result_len_and_empty() {
125        let r: QueryResult<i32> = QueryResult::new(vec![1, 2, 3], 10, 0);
126        assert_eq!(r.len(), 3);
127        assert!(!r.is_empty());
128
129        let e: QueryResult<i32> = QueryResult::empty();
130        assert!(e.is_empty());
131    }
132}