use std::collections::HashMap;
use crate::{Record, Relation};
pub type ScopeFunction<T> = Box<dyn Fn(Relation<T>) -> Relation<T> + Send + Sync>;
pub struct ScopeRegistry<T: Record> {
scopes: HashMap<String, ScopeFunction<T>>,
}
impl<T: Record> Default for ScopeRegistry<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: Record> ScopeRegistry<T> {
#[must_use]
pub fn new() -> Self {
Self {
scopes: HashMap::new(),
}
}
pub fn add(
&mut self,
name: impl Into<String>,
scope: impl Fn(Relation<T>) -> Relation<T> + Send + Sync + 'static,
) {
self.scopes.insert(name.into(), Box::new(scope));
}
#[must_use]
pub fn apply(&self, name: &str, relation: Relation<T>) -> Option<Relation<T>> {
self.scopes.get(name).map(|scope| scope(relation))
}
#[must_use]
pub fn names(&self) -> Vec<&String> {
self.scopes.keys().collect()
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use serde_json::json;
use super::ScopeRegistry;
use crate::{
OrderDirection, Relation,
base::test_support::{TestUser, seed_users, setup_db},
};
fn named_scope(
expected: &'static str,
) -> impl Fn(Relation<TestUser>) -> Relation<TestUser> + Send + Sync + 'static {
move |relation| relation.r#where(HashMap::from([("name".to_owned(), json!(expected))]))
}
#[tokio::test]
async fn apply_returns_none_for_unknown_scope() {
let registry = ScopeRegistry::<TestUser>::new();
assert!(registry.apply("missing", Relation::new()).is_none());
}
#[tokio::test]
async fn apply_runs_registered_scope() {
let db = setup_db().await;
seed_users(&db).await;
let mut registry = ScopeRegistry::<TestUser>::new();
registry.add("named_bob", |relation| {
relation.r#where(HashMap::from([("name".to_owned(), json!("Bob"))]))
});
let relation = registry
.apply("named_bob", Relation::new())
.expect("scope should exist");
let users = relation
.load(&db)
.await
.expect("scoped query should succeed");
assert_eq!(users.len(), 1);
assert_eq!(users[0].name, "Bob");
}
#[tokio::test]
async fn scope_can_modify_relation_with_limit() {
let db = setup_db().await;
seed_users(&db).await;
let mut registry = ScopeRegistry::<TestUser>::new();
registry.add("first_two", |relation| relation.limit(2));
let relation = registry
.apply("first_two", Relation::new())
.expect("scope should exist");
let users = relation
.load(&db)
.await
.expect("limited query should succeed");
assert_eq!(users.len(), 2);
}
#[tokio::test]
async fn add_replaces_existing_scope_name() {
let db = setup_db().await;
seed_users(&db).await;
let mut registry = ScopeRegistry::<TestUser>::new();
registry.add("window", |relation| relation.limit(1));
registry.add("window", |relation| relation.limit(2));
let relation = registry
.apply("window", Relation::new())
.expect("scope should exist");
let users = relation.load(&db).await.expect("query should succeed");
assert_eq!(users.len(), 2);
}
#[test]
fn names_returns_registered_scope_names() {
let mut registry = ScopeRegistry::<TestUser>::new();
registry.add("recent", |relation| relation.limit(1));
registry.add("alphabetical", |relation| relation);
let mut names = registry
.names()
.into_iter()
.map(|name| name.as_str())
.collect::<Vec<_>>();
names.sort_unstable();
assert_eq!(names, vec!["alphabetical", "recent"]);
}
#[test]
fn default_registry_starts_without_names() {
let registry = ScopeRegistry::<TestUser>::default();
assert!(registry.names().is_empty());
}
#[tokio::test]
async fn scope_name_with_spaces_is_applied() {
let db = setup_db().await;
seed_users(&db).await;
let mut registry = ScopeRegistry::<TestUser>::new();
registry.add("named bob", named_scope("Bob"));
let users = registry
.apply("named bob", Relation::new())
.expect("scope should exist")
.load(&db)
.await
.expect("scoped query should succeed");
assert_eq!(users.len(), 1);
assert_eq!(users[0].name, "Bob");
}
#[tokio::test]
async fn scope_can_capture_external_value() {
let db = setup_db().await;
seed_users(&db).await;
let expected = "Carol".to_owned();
let mut registry = ScopeRegistry::<TestUser>::new();
registry.add("captured_name", move |relation| {
relation.r#where(HashMap::from([(
"name".to_owned(),
json!(expected.clone()),
)]))
});
let users = registry
.apply("captured_name", Relation::new())
.expect("scope should exist")
.load(&db)
.await
.expect("captured query should succeed");
assert_eq!(users.len(), 1);
assert_eq!(users[0].email, "carol@example.com");
}
#[tokio::test]
async fn scope_factory_with_argument_filters_expected_row() {
let db = setup_db().await;
seed_users(&db).await;
let mut registry = ScopeRegistry::<TestUser>::new();
registry.add("named_alice", named_scope("Alice"));
let user = registry
.apply("named_alice", Relation::new())
.expect("scope should exist")
.first(&db)
.await
.expect("query should succeed")
.expect("alice should exist");
assert_eq!(user.email, "alice@example.com");
}
#[tokio::test]
async fn scope_chaining_applies_multiple_registered_scopes() {
let db = setup_db().await;
seed_users(&db).await;
let mut registry = ScopeRegistry::<TestUser>::new();
registry.add("without_alice", |relation| {
relation.not(HashMap::from([("name".to_owned(), json!("Alice"))]))
});
registry.add("descending", |relation| {
relation.order("name", OrderDirection::Desc)
});
let relation = registry
.apply("without_alice", Relation::new())
.expect("first scope should exist");
let relation = registry
.apply("descending", relation)
.expect("second scope should exist");
let users = relation
.load(&db)
.await
.expect("chained scope should succeed");
let names = users.into_iter().map(|user| user.name).collect::<Vec<_>>();
assert_eq!(names, vec!["Carol", "Bob"]);
}
#[tokio::test]
async fn scope_can_extend_existing_relation_filters() {
let db = setup_db().await;
seed_users(&db).await;
let mut registry = ScopeRegistry::<TestUser>::new();
registry.add("first_match", |relation| relation.limit(1));
let base = Relation::new().r#where(HashMap::from([("name".to_owned(), json!("Carol"))]));
let users = registry
.apply("first_match", base)
.expect("scope should exist")
.load(&db)
.await
.expect("merged query should succeed");
assert_eq!(users.len(), 1);
assert_eq!(users[0].name, "Carol");
}
#[tokio::test]
async fn scope_can_order_results_descending() {
let db = setup_db().await;
seed_users(&db).await;
let mut registry = ScopeRegistry::<TestUser>::new();
registry.add("desc_by_name", |relation| {
relation.order("name", OrderDirection::Desc)
});
let first = registry
.apply("desc_by_name", Relation::new())
.expect("scope should exist")
.first(&db)
.await
.expect("ordered query should succeed")
.expect("a row should exist");
assert_eq!(first.name, "Carol");
}
#[tokio::test]
async fn scope_can_skip_rows_with_offset() {
let db = setup_db().await;
seed_users(&db).await;
let mut registry = ScopeRegistry::<TestUser>::new();
registry.add("skip_first", |relation| {
relation.order("id", OrderDirection::Asc).offset(1)
});
let users = registry
.apply("skip_first", Relation::new())
.expect("scope should exist")
.load(&db)
.await
.expect("offset query should succeed");
let names = users.into_iter().map(|user| user.name).collect::<Vec<_>>();
assert_eq!(names, vec!["Bob", "Carol"]);
}
#[tokio::test]
async fn scope_can_add_negated_filters() {
let db = setup_db().await;
seed_users(&db).await;
let mut registry = ScopeRegistry::<TestUser>::new();
registry.add("without_bob", |relation| {
relation.not(HashMap::from([("name".to_owned(), json!("Bob"))]))
});
let users = registry
.apply("without_bob", Relation::new())
.expect("scope should exist")
.load(&db)
.await
.expect("negated query should succeed");
let names = users.into_iter().map(|user| user.name).collect::<Vec<_>>();
assert_eq!(names, vec!["Alice", "Carol"]);
}
#[tokio::test]
async fn scope_merge_like_composition_preserves_all_constraints() {
let db = setup_db().await;
seed_users(&db).await;
let mut registry = ScopeRegistry::<TestUser>::new();
registry.add("only_carol", named_scope("Carol"));
registry.add("take_one", |relation| relation.limit(1));
let relation = registry
.apply(
"only_carol",
Relation::new().order("id", OrderDirection::Desc),
)
.expect("first scope should exist");
let exists = registry
.apply("take_one", relation)
.expect("second scope should exist")
.exists(&db)
.await
.expect("exists query should succeed");
assert!(exists);
}
#[test]
fn replacing_scope_keeps_single_registered_name() {
let mut registry = ScopeRegistry::<TestUser>::new();
registry.add("window", |relation| relation.limit(1));
registry.add("window", |relation| relation.limit(2));
let mut names = registry
.names()
.into_iter()
.map(|name| name.as_str())
.collect::<Vec<_>>();
names.sort_unstable();
assert_eq!(names, vec!["window"]);
}
#[tokio::test]
async fn scope_application_is_repeatable_without_state_leakage() {
let db = setup_db().await;
seed_users(&db).await;
let mut registry = ScopeRegistry::<TestUser>::new();
registry.add("without_alice", |relation| {
relation.not(HashMap::from([("name".to_owned(), json!("Alice"))]))
});
let first = registry
.apply("without_alice", Relation::new())
.expect("scope should exist")
.load(&db)
.await
.expect("first scoped query should succeed");
let second = registry
.apply("without_alice", Relation::new())
.expect("scope should exist")
.load(&db)
.await
.expect("second scoped query should succeed");
let first_names = first.into_iter().map(|user| user.name).collect::<Vec<_>>();
let second_names = second.into_iter().map(|user| user.name).collect::<Vec<_>>();
assert_eq!(first_names, vec!["Bob", "Carol"]);
assert_eq!(second_names, vec!["Bob", "Carol"]);
}
#[tokio::test]
async fn scope_preserves_existing_order_when_it_only_adds_filters() {
let db = setup_db().await;
seed_users(&db).await;
let mut registry = ScopeRegistry::<TestUser>::new();
registry.add("without_carol", |relation| {
relation.not(HashMap::from([("name".to_owned(), json!("Carol"))]))
});
let users = registry
.apply(
"without_carol",
Relation::new().order("name", OrderDirection::Desc),
)
.expect("scope should exist")
.load(&db)
.await
.expect("ordered scoped query should succeed");
let names = users.into_iter().map(|user| user.name).collect::<Vec<_>>();
assert_eq!(names, vec!["Bob", "Alice"]);
}
}