Skip to main content

rustrails_record/
scoping.rs

1use std::collections::HashMap;
2
3use crate::{Record, Relation};
4
5/// A named scope that transforms a [`Relation`].
6pub type ScopeFunction<T> = Box<dyn Fn(Relation<T>) -> Relation<T> + Send + Sync>;
7
8/// Registry of named scopes for a record type.
9pub struct ScopeRegistry<T: Record> {
10    scopes: HashMap<String, ScopeFunction<T>>,
11}
12
13impl<T: Record> Default for ScopeRegistry<T> {
14    fn default() -> Self {
15        Self::new()
16    }
17}
18
19impl<T: Record> ScopeRegistry<T> {
20    /// Creates an empty scope registry.
21    #[must_use]
22    pub fn new() -> Self {
23        Self {
24            scopes: HashMap::new(),
25        }
26    }
27
28    /// Registers or replaces a named scope.
29    pub fn add(
30        &mut self,
31        name: impl Into<String>,
32        scope: impl Fn(Relation<T>) -> Relation<T> + Send + Sync + 'static,
33    ) {
34        self.scopes.insert(name.into(), Box::new(scope));
35    }
36
37    /// Applies a named scope to the provided relation.
38    #[must_use]
39    pub fn apply(&self, name: &str, relation: Relation<T>) -> Option<Relation<T>> {
40        self.scopes.get(name).map(|scope| scope(relation))
41    }
42
43    /// Returns the registered scope names.
44    #[must_use]
45    pub fn names(&self) -> Vec<&String> {
46        self.scopes.keys().collect()
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use std::collections::HashMap;
53
54    use serde_json::json;
55
56    use super::ScopeRegistry;
57    use crate::{
58        OrderDirection, Relation,
59        base::test_support::{TestUser, seed_users, setup_db},
60    };
61
62    fn named_scope(
63        expected: &'static str,
64    ) -> impl Fn(Relation<TestUser>) -> Relation<TestUser> + Send + Sync + 'static {
65        move |relation| relation.r#where(HashMap::from([("name".to_owned(), json!(expected))]))
66    }
67
68    #[tokio::test]
69    async fn apply_returns_none_for_unknown_scope() {
70        let registry = ScopeRegistry::<TestUser>::new();
71
72        assert!(registry.apply("missing", Relation::new()).is_none());
73    }
74
75    #[tokio::test]
76    async fn apply_runs_registered_scope() {
77        let db = setup_db().await;
78        seed_users(&db).await;
79
80        let mut registry = ScopeRegistry::<TestUser>::new();
81        registry.add("named_bob", |relation| {
82            relation.r#where(HashMap::from([("name".to_owned(), json!("Bob"))]))
83        });
84
85        let relation = registry
86            .apply("named_bob", Relation::new())
87            .expect("scope should exist");
88        let users = relation
89            .load(&db)
90            .await
91            .expect("scoped query should succeed");
92
93        assert_eq!(users.len(), 1);
94        assert_eq!(users[0].name, "Bob");
95    }
96
97    #[tokio::test]
98    async fn scope_can_modify_relation_with_limit() {
99        let db = setup_db().await;
100        seed_users(&db).await;
101
102        let mut registry = ScopeRegistry::<TestUser>::new();
103        registry.add("first_two", |relation| relation.limit(2));
104
105        let relation = registry
106            .apply("first_two", Relation::new())
107            .expect("scope should exist");
108        let users = relation
109            .load(&db)
110            .await
111            .expect("limited query should succeed");
112
113        assert_eq!(users.len(), 2);
114    }
115
116    #[tokio::test]
117    async fn add_replaces_existing_scope_name() {
118        let db = setup_db().await;
119        seed_users(&db).await;
120
121        let mut registry = ScopeRegistry::<TestUser>::new();
122        registry.add("window", |relation| relation.limit(1));
123        registry.add("window", |relation| relation.limit(2));
124
125        let relation = registry
126            .apply("window", Relation::new())
127            .expect("scope should exist");
128        let users = relation.load(&db).await.expect("query should succeed");
129
130        assert_eq!(users.len(), 2);
131    }
132
133    #[test]
134    fn names_returns_registered_scope_names() {
135        let mut registry = ScopeRegistry::<TestUser>::new();
136        registry.add("recent", |relation| relation.limit(1));
137        registry.add("alphabetical", |relation| relation);
138
139        let mut names = registry
140            .names()
141            .into_iter()
142            .map(|name| name.as_str())
143            .collect::<Vec<_>>();
144        names.sort_unstable();
145
146        assert_eq!(names, vec!["alphabetical", "recent"]);
147    }
148
149    #[test]
150    fn default_registry_starts_without_names() {
151        let registry = ScopeRegistry::<TestUser>::default();
152
153        assert!(registry.names().is_empty());
154    }
155
156    #[tokio::test]
157    async fn scope_name_with_spaces_is_applied() {
158        let db = setup_db().await;
159        seed_users(&db).await;
160
161        let mut registry = ScopeRegistry::<TestUser>::new();
162        registry.add("named bob", named_scope("Bob"));
163
164        let users = registry
165            .apply("named bob", Relation::new())
166            .expect("scope should exist")
167            .load(&db)
168            .await
169            .expect("scoped query should succeed");
170
171        assert_eq!(users.len(), 1);
172        assert_eq!(users[0].name, "Bob");
173    }
174
175    #[tokio::test]
176    async fn scope_can_capture_external_value() {
177        let db = setup_db().await;
178        seed_users(&db).await;
179
180        let expected = "Carol".to_owned();
181        let mut registry = ScopeRegistry::<TestUser>::new();
182        registry.add("captured_name", move |relation| {
183            relation.r#where(HashMap::from([(
184                "name".to_owned(),
185                json!(expected.clone()),
186            )]))
187        });
188
189        let users = registry
190            .apply("captured_name", Relation::new())
191            .expect("scope should exist")
192            .load(&db)
193            .await
194            .expect("captured query should succeed");
195
196        assert_eq!(users.len(), 1);
197        assert_eq!(users[0].email, "carol@example.com");
198    }
199
200    #[tokio::test]
201    async fn scope_factory_with_argument_filters_expected_row() {
202        let db = setup_db().await;
203        seed_users(&db).await;
204
205        let mut registry = ScopeRegistry::<TestUser>::new();
206        registry.add("named_alice", named_scope("Alice"));
207
208        let user = registry
209            .apply("named_alice", Relation::new())
210            .expect("scope should exist")
211            .first(&db)
212            .await
213            .expect("query should succeed")
214            .expect("alice should exist");
215
216        assert_eq!(user.email, "alice@example.com");
217    }
218
219    #[tokio::test]
220    async fn scope_chaining_applies_multiple_registered_scopes() {
221        let db = setup_db().await;
222        seed_users(&db).await;
223
224        let mut registry = ScopeRegistry::<TestUser>::new();
225        registry.add("without_alice", |relation| {
226            relation.not(HashMap::from([("name".to_owned(), json!("Alice"))]))
227        });
228        registry.add("descending", |relation| {
229            relation.order("name", OrderDirection::Desc)
230        });
231
232        let relation = registry
233            .apply("without_alice", Relation::new())
234            .expect("first scope should exist");
235        let relation = registry
236            .apply("descending", relation)
237            .expect("second scope should exist");
238        let users = relation
239            .load(&db)
240            .await
241            .expect("chained scope should succeed");
242
243        let names = users.into_iter().map(|user| user.name).collect::<Vec<_>>();
244        assert_eq!(names, vec!["Carol", "Bob"]);
245    }
246
247    #[tokio::test]
248    async fn scope_can_extend_existing_relation_filters() {
249        let db = setup_db().await;
250        seed_users(&db).await;
251
252        let mut registry = ScopeRegistry::<TestUser>::new();
253        registry.add("first_match", |relation| relation.limit(1));
254
255        let base = Relation::new().r#where(HashMap::from([("name".to_owned(), json!("Carol"))]));
256        let users = registry
257            .apply("first_match", base)
258            .expect("scope should exist")
259            .load(&db)
260            .await
261            .expect("merged query should succeed");
262
263        assert_eq!(users.len(), 1);
264        assert_eq!(users[0].name, "Carol");
265    }
266
267    #[tokio::test]
268    async fn scope_can_order_results_descending() {
269        let db = setup_db().await;
270        seed_users(&db).await;
271
272        let mut registry = ScopeRegistry::<TestUser>::new();
273        registry.add("desc_by_name", |relation| {
274            relation.order("name", OrderDirection::Desc)
275        });
276
277        let first = registry
278            .apply("desc_by_name", Relation::new())
279            .expect("scope should exist")
280            .first(&db)
281            .await
282            .expect("ordered query should succeed")
283            .expect("a row should exist");
284
285        assert_eq!(first.name, "Carol");
286    }
287
288    #[tokio::test]
289    async fn scope_can_skip_rows_with_offset() {
290        let db = setup_db().await;
291        seed_users(&db).await;
292
293        let mut registry = ScopeRegistry::<TestUser>::new();
294        registry.add("skip_first", |relation| {
295            relation.order("id", OrderDirection::Asc).offset(1)
296        });
297
298        let users = registry
299            .apply("skip_first", Relation::new())
300            .expect("scope should exist")
301            .load(&db)
302            .await
303            .expect("offset query should succeed");
304
305        let names = users.into_iter().map(|user| user.name).collect::<Vec<_>>();
306        assert_eq!(names, vec!["Bob", "Carol"]);
307    }
308
309    #[tokio::test]
310    async fn scope_can_add_negated_filters() {
311        let db = setup_db().await;
312        seed_users(&db).await;
313
314        let mut registry = ScopeRegistry::<TestUser>::new();
315        registry.add("without_bob", |relation| {
316            relation.not(HashMap::from([("name".to_owned(), json!("Bob"))]))
317        });
318
319        let users = registry
320            .apply("without_bob", Relation::new())
321            .expect("scope should exist")
322            .load(&db)
323            .await
324            .expect("negated query should succeed");
325
326        let names = users.into_iter().map(|user| user.name).collect::<Vec<_>>();
327        assert_eq!(names, vec!["Alice", "Carol"]);
328    }
329
330    #[tokio::test]
331    async fn scope_merge_like_composition_preserves_all_constraints() {
332        let db = setup_db().await;
333        seed_users(&db).await;
334
335        let mut registry = ScopeRegistry::<TestUser>::new();
336        registry.add("only_carol", named_scope("Carol"));
337        registry.add("take_one", |relation| relation.limit(1));
338
339        let relation = registry
340            .apply(
341                "only_carol",
342                Relation::new().order("id", OrderDirection::Desc),
343            )
344            .expect("first scope should exist");
345        let exists = registry
346            .apply("take_one", relation)
347            .expect("second scope should exist")
348            .exists(&db)
349            .await
350            .expect("exists query should succeed");
351
352        assert!(exists);
353    }
354
355    #[test]
356    fn replacing_scope_keeps_single_registered_name() {
357        let mut registry = ScopeRegistry::<TestUser>::new();
358        registry.add("window", |relation| relation.limit(1));
359        registry.add("window", |relation| relation.limit(2));
360
361        let mut names = registry
362            .names()
363            .into_iter()
364            .map(|name| name.as_str())
365            .collect::<Vec<_>>();
366        names.sort_unstable();
367
368        assert_eq!(names, vec!["window"]);
369    }
370
371    #[tokio::test]
372    async fn scope_application_is_repeatable_without_state_leakage() {
373        let db = setup_db().await;
374        seed_users(&db).await;
375
376        let mut registry = ScopeRegistry::<TestUser>::new();
377        registry.add("without_alice", |relation| {
378            relation.not(HashMap::from([("name".to_owned(), json!("Alice"))]))
379        });
380
381        let first = registry
382            .apply("without_alice", Relation::new())
383            .expect("scope should exist")
384            .load(&db)
385            .await
386            .expect("first scoped query should succeed");
387        let second = registry
388            .apply("without_alice", Relation::new())
389            .expect("scope should exist")
390            .load(&db)
391            .await
392            .expect("second scoped query should succeed");
393
394        let first_names = first.into_iter().map(|user| user.name).collect::<Vec<_>>();
395        let second_names = second.into_iter().map(|user| user.name).collect::<Vec<_>>();
396
397        assert_eq!(first_names, vec!["Bob", "Carol"]);
398        assert_eq!(second_names, vec!["Bob", "Carol"]);
399    }
400
401    #[tokio::test]
402    async fn scope_preserves_existing_order_when_it_only_adds_filters() {
403        let db = setup_db().await;
404        seed_users(&db).await;
405
406        let mut registry = ScopeRegistry::<TestUser>::new();
407        registry.add("without_carol", |relation| {
408            relation.not(HashMap::from([("name".to_owned(), json!("Carol"))]))
409        });
410
411        let users = registry
412            .apply(
413                "without_carol",
414                Relation::new().order("name", OrderDirection::Desc),
415            )
416            .expect("scope should exist")
417            .load(&db)
418            .await
419            .expect("ordered scoped query should succeed");
420
421        let names = users.into_iter().map(|user| user.name).collect::<Vec<_>>();
422        assert_eq!(names, vec!["Bob", "Alice"]);
423    }
424}