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}