Skip to main content

key_paths_iter/
lib.rs

1//! Query builder for collection keypaths over [rust_key_paths::KpType] when the value type is `Vec<Item>`.
2//!
3//! Enable the `rayon` feature for parallel collection operations ([`query_par`]).
4
5use rust_key_paths::KpType;
6
7#[cfg(feature = "rayon")]
8pub mod query_par;
9#[cfg(feature = "rayon")]
10pub mod rayon_optimizations;
11
12/// Query builder for collection keypaths (KpType where value is `Vec<Item>`).
13pub struct CollectionQuery<'a, Root, Item> {
14    keypath: &'a KpType<'a, Root, Vec<Item>>,
15    filters: Vec<Box<dyn Fn(&Item) -> bool + 'a>>,
16    limit: Option<usize>,
17    offset: usize,
18}
19
20impl<'a, Root, Item> CollectionQuery<'a, Root, Item> {
21    pub fn new(keypath: &'a KpType<'a, Root, Vec<Item>>) -> Self {
22        Self {
23            keypath,
24            filters: Vec::new(),
25            limit: None,
26            offset: 0,
27        }
28    }
29
30    pub fn filter<F>(mut self, predicate: F) -> Self
31    where
32        F: Fn(&Item) -> bool + 'a,
33    {
34        self.filters.push(Box::new(predicate));
35        self
36    }
37
38    pub fn limit(mut self, n: usize) -> Self {
39        self.limit = Some(n);
40        self
41    }
42
43    pub fn offset(mut self, n: usize) -> Self {
44        self.offset = n;
45        self
46    }
47
48    pub fn execute(&self, root: &'a Root) -> Vec<&'a Item> {
49        if let Some(vec) = self.keypath.get(root) {
50            let mut result: Vec<&'a Item> = vec
51                .iter()
52                .skip(self.offset)
53                .filter(|item| self.filters.iter().all(|f| f(item)))
54                .collect();
55
56            if let Some(limit) = self.limit {
57                result.truncate(limit);
58            }
59
60            result
61        } else {
62            Vec::new()
63        }
64    }
65
66    pub fn count(&self, root: &'a Root) -> usize {
67        if let Some(vec) = self.keypath.get(root) {
68            vec.iter()
69                .skip(self.offset)
70                .filter(|item| self.filters.iter().all(|f| f(item)))
71                .take(self.limit.unwrap_or(usize::MAX))
72                .count()
73        } else {
74            0
75        }
76    }
77
78    pub fn exists(&self, root: &'a Root) -> bool {
79        self.count(root) > 0
80    }
81
82    pub fn first(&self, root: &'a Root) -> Option<&'a Item> {
83        self.execute(root).into_iter().next()
84    }
85}
86
87/// Implemented for keypath types that target `Vec<Item>`, enabling `.query()`.
88/// The keypath and the reference passed to `query()` share the same lifetime.
89pub trait QueryableCollection<'a, Root, Item> {
90    fn query(&'a self) -> CollectionQuery<'a, Root, Item>;
91}
92
93impl<'a, Root, Item> QueryableCollection<'a, Root, Item> for KpType<'a, Root, Vec<Item>> {
94    fn query(&'a self) -> CollectionQuery<'a, Root, Item> {
95        CollectionQuery::new(self)
96    }
97}
98
99// --- Support for KpType<'static, Root, Vec<Item>> (e.g. from #[derive(Kp)]) ---
100
101/// Query builder for collection keypaths with `'static` lifetime (e.g. from #[derive(Kp)]).
102/// Pass the root when calling `execute`, `count`, `exists`, or `first`.
103pub struct CollectionQueryStatic<'q, Root, Item>
104where
105    Root: 'static,
106    Item: 'static,
107{
108    keypath: &'q KpType<'static, Root, Vec<Item>>,
109    filters: Vec<Box<dyn Fn(&Item) -> bool + 'q>>,
110    limit: Option<usize>,
111    offset: usize,
112}
113
114impl<'q, Root: 'static, Item: 'static> CollectionQueryStatic<'q, Root, Item> {
115    pub fn new(keypath: &'q KpType<'static, Root, Vec<Item>>) -> Self {
116        Self {
117            keypath,
118            filters: Vec::new(),
119            limit: None,
120            offset: 0,
121        }
122    }
123
124    pub fn filter<F>(mut self, predicate: F) -> Self
125    where
126        F: Fn(&Item) -> bool + 'q,
127    {
128        self.filters.push(Box::new(predicate));
129        self
130    }
131
132    pub fn limit(mut self, n: usize) -> Self {
133        self.limit = Some(n);
134        self
135    }
136
137    pub fn offset(mut self, n: usize) -> Self {
138        self.offset = n;
139        self
140    }
141
142    pub fn execute<'a>(&self, root: &'a Root) -> Vec<&'a Item> {
143        if let Some(vec) = get_vec_static(self.keypath, root) {
144            let mut result: Vec<&'a Item> = vec
145                .iter()
146                .skip(self.offset)
147                .filter(|item| self.filters.iter().all(|f| f(item)))
148                .collect();
149            if let Some(limit) = self.limit {
150                result.truncate(limit);
151            }
152            result
153        } else {
154            Vec::new()
155        }
156    }
157
158    pub fn count<'a>(&self, root: &'a Root) -> usize {
159        if let Some(vec) = get_vec_static(self.keypath, root) {
160            vec.iter()
161                .skip(self.offset)
162                .filter(|item| self.filters.iter().all(|f| f(item)))
163                .take(self.limit.unwrap_or(usize::MAX))
164                .count()
165        } else {
166            0
167        }
168    }
169
170    pub fn exists<'a>(&self, root: &'a Root) -> bool {
171        self.count(root) > 0
172    }
173
174    pub fn first<'a>(&self, root: &'a Root) -> Option<&'a Item> {
175        self.execute(root).into_iter().next()
176    }
177}
178
179/// Get `&'a Vec<Item>` from a `'static` keypath and `&'a Root`.
180/// Used by [query_par] for parallel operations. Sound because the closure in
181/// `KpType<'static, ...>` is `for<'b> fn(&'b Root) -> Option<&'b Vec<Item>>`.
182#[inline]
183pub(crate) fn get_vec_static<'a, Root: 'static, Item: 'static>(
184    keypath: &KpType<'static, Root, Vec<Item>>,
185    root: &'a Root,
186) -> Option<&'a Vec<Item>> {
187    // The closure in KpType<'static, ...> is for<'b> fn(&'b Root) -> Option<&'b Vec<Item>>,
188    // so it does not store the reference; extending to 'static for the call is sound.
189    let root_static: &'static Root = unsafe { std::mem::transmute(root) };
190    let opt = keypath.get(root_static);
191    unsafe { std::mem::transmute(opt) }
192}
193
194/// Implemented for `KpType<'static, Root, Vec<Item>>` (e.g. from #[derive(Kp)]), enabling `.query()`.
195pub trait QueryableCollectionStatic<Root, Item>
196where
197    Root: 'static,
198    Item: 'static,
199{
200    fn query(&self) -> CollectionQueryStatic<'_, Root, Item>;
201}
202
203impl<Root: 'static, Item: 'static> QueryableCollectionStatic<Root, Item>
204    for KpType<'static, Root, Vec<Item>>
205{
206    fn query(&self) -> CollectionQueryStatic<'_, Root, Item> {
207        CollectionQueryStatic::new(self)
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::{QueryableCollection, *};
214    use rust_key_paths::Kp;
215
216    #[test]
217    fn test_query_dsl() {
218        struct Database {
219            users: Vec<User>,
220        }
221
222        struct User {
223            id: u32,
224            name: String,
225            age: u32,
226            active: bool,
227        }
228
229        // Type annotation so the keypath gets a concrete lifetime tied to this scope
230        let users_kp: KpType<'_, Database, Vec<User>> = Kp::new(
231            |db: &Database| Some(&db.users),
232            |db: &mut Database| Some(&mut db.users),
233        );
234
235        let db = Database {
236            users: vec![
237                User {
238                    id: 1,
239                    name: "Alice".into(),
240                    age: 25,
241                    active: true,
242                },
243                User {
244                    id: 2,
245                    name: "Bob".into(),
246                    age: 30,
247                    active: false,
248                },
249                User {
250                    id: 3,
251                    name: "Charlie".into(),
252                    age: 35,
253                    active: true,
254                },
255                User {
256                    id: 4,
257                    name: "Diana".into(),
258                    age: 28,
259                    active: true,
260                },
261            ],
262        };
263
264        // Query: active users over 26, limit 2 (use trait to disambiguate from QueryableCollectionStatic)
265        let results = QueryableCollection::query(&users_kp)
266            .filter(|u| u.active)
267            .filter(|u| u.age > 26)
268            .limit(2)
269            .execute(&db);
270
271        assert_eq!(results.len(), 2);
272        assert_eq!(results[0].name, "Charlie");
273
274        // Check if any active user exists
275        assert!(QueryableCollection::query(&users_kp).filter(|u| u.active).exists(&db));
276
277        // Count active users
278        let count = QueryableCollection::query(&users_kp).filter(|u| u.active).count(&db);
279        assert_eq!(count, 3);
280    }
281}