#[cfg(test)]
#[allow(dead_code)]
pub(crate) mod dsl {
use crate::RestSql;
use crate::ast::{Ast, Constraint, Operator, Value};
use crate::dsl::validate_inner;
use crate::error::{RestSqlError, ValidationError};
pub fn run() {
new_passes_valid_query();
new_passes_or();
new_rejects_in_without_list();
new_rejects_out_without_list();
new_rejects_between_wrong_arity();
new_rejects_between_scalar();
new_accepts_null_notnull();
validate_scalar_op_rejects_list();
new_for_fields_passes_allowed();
new_for_fields_rejects_forbidden();
new_for_fields_accumulates_all_errors();
fields_single();
fields_multiple();
fields_dedup();
map_fields_identity();
map_fields_renames_single();
map_fields_renames_throughout_tree();
map_fields_preserves_values();
}
fn validation_errors(err: RestSqlError) -> Vec<ValidationError> {
match err {
RestSqlError::ValidationError(errs) => errs,
other => panic!("expected ValidationError, got {:?}", other),
}
}
fn new_passes_valid_query() {
assert!(
RestSql::new("name==Alice;age>18").is_ok(),
"new_passes_valid_query"
);
}
fn new_passes_or() {
assert!(RestSql::new("name==Alice,age>18").is_ok(), "new_passes_or");
}
fn new_rejects_in_without_list() {
let errs = validation_errors(RestSql::new("role=in=admin").unwrap_err());
assert!(
matches!(errs[0], ValidationError::ExpectedList { .. }),
"new_rejects_in_without_list"
);
}
fn new_rejects_out_without_list() {
let errs = validation_errors(RestSql::new("role=out=admin").unwrap_err());
assert!(
matches!(errs[0], ValidationError::ExpectedList { .. }),
"new_rejects_out_without_list"
);
}
fn new_rejects_between_wrong_arity() {
let errs = validation_errors(RestSql::new("age=between=(1,2,3)").unwrap_err());
assert!(
matches!(errs[0], ValidationError::BetweenArity { .. }),
"new_rejects_between_wrong_arity"
);
}
fn new_rejects_between_scalar() {
let errs = validation_errors(RestSql::new("age=between=18").unwrap_err());
assert!(
matches!(errs[0], ValidationError::ExpectedList { .. }),
"new_rejects_between_scalar"
);
}
fn new_accepts_null_notnull() {
assert!(
RestSql::new("deleted_at=null=true").is_ok(),
"new_accepts_null"
);
assert!(
RestSql::new("deleted_at=notnull=true").is_ok(),
"new_accepts_notnull"
);
}
fn validate_scalar_op_rejects_list() {
let ast = Ast::Constraint(Constraint {
field: "age".into(),
operator: Operator::Eq,
value: Value::List(vec![Value::Int(1), Value::Int(2)]),
});
let err = validate_inner(&ast, None).unwrap_err();
assert!(
matches!(err[0], ValidationError::UnexpectedList { .. }),
"validate_scalar_op_rejects_list"
);
}
fn new_for_fields_passes_allowed() {
assert!(
RestSql::new_for_fields("name==Alice;age>18", &["name", "age"]).is_ok(),
"new_for_fields_passes_allowed"
);
}
fn new_for_fields_rejects_forbidden() {
let errs =
validation_errors(RestSql::new_for_fields("secret==xyz", &["name"]).unwrap_err());
assert_eq!(errs.len(), 1);
assert!(
matches!(&errs[0], ValidationError::ForbiddenField(f) if f == "secret"),
"new_for_fields_rejects_forbidden"
);
}
fn new_for_fields_accumulates_all_errors() {
let errs = validation_errors(RestSql::new_for_fields("a==1;b==2;c==3", &[]).unwrap_err());
assert_eq!(errs.len(), 3, "new_for_fields_accumulates_all_errors");
assert!(
errs.iter()
.all(|e| matches!(e, ValidationError::ForbiddenField(_)))
);
}
fn fields_single() {
assert_eq!(
RestSql::new("name==Alice").unwrap().fields(),
vec!["name"],
"fields_single"
);
}
fn fields_multiple() {
let rsql = RestSql::new("name==Alice;age>18;active==true").unwrap();
assert_eq!(
rsql.fields(),
vec!["active", "age", "name"],
"fields_multiple"
);
}
fn fields_dedup() {
assert_eq!(
RestSql::new("name==Alice,name==Bob").unwrap().fields(),
vec!["name"],
"fields_dedup"
);
}
struct PrefixMapper(&'static str);
impl crate::FieldMapper for PrefixMapper {
fn map<'a>(&self, field: &'a str) -> std::borrow::Cow<'a, str> {
std::borrow::Cow::Owned(format!("{}.{}", self.0, field))
}
}
fn map_fields_identity() {
let rsql = RestSql::new("name==Alice;age>18").unwrap();
let mapped = rsql.map_fields(&crate::IdentityMapper);
assert_eq!(rsql.fields(), mapped.fields(), "map_fields_identity");
}
fn map_fields_renames_single() {
let mapped = RestSql::new("name==Alice")
.unwrap()
.map_fields(&PrefixMapper("t"));
assert_eq!(mapped.fields(), vec!["t.name"], "map_fields_renames_single");
}
fn map_fields_renames_throughout_tree() {
let mapped = RestSql::new("name==Alice;age>18,active==true")
.unwrap()
.map_fields(&PrefixMapper("u"));
assert_eq!(
mapped.fields(),
vec!["u.active", "u.age", "u.name"],
"map_fields_renames_throughout_tree"
);
}
fn map_fields_preserves_values() {
let mapped = RestSql::new("role=in=(admin,user)")
.unwrap()
.map_fields(&PrefixMapper("t"));
let ast = mapped.ast();
assert!(
matches!(ast, Ast::Constraint(Constraint {
field,
operator: Operator::In,
value: Value::List(_),
}) if field == "t.role"),
"map_fields_preserves_values"
);
}
}
#[cfg(test)]
pub(crate) mod filter_api {
use crate::filter::*;
use crate::{Ast, Constraint, Operator, RestSql, Value};
pub fn run() {
from_ast_basic();
from_ast_validates();
op_eq();
op_neq();
op_lt();
op_lte();
op_gt();
op_gte();
op_in();
op_out();
op_between();
op_null();
op_not_null();
op_like();
op_ilike();
value_from_bool();
value_from_i32();
value_from_i64();
value_from_f32();
value_from_f64();
value_from_str();
value_from_string();
bitand_leaf_leaf();
bitor_leaf_leaf();
bitand_and_and_flattens();
bitand_and_or_wraps();
bitor_or_or_flattens();
bitor_or_and_wraps();
ast_and_multi();
ast_or_multi();
try_and_empty_returns_none();
try_or_empty_returns_none();
try_and_single_unwraps();
try_or_single_unwraps();
consumer_pattern();
consumer_pattern_all_none();
}
fn from_ast_basic() {
let ast = eq("name", "Alice") & gt("age", 18i64);
let rsql = RestSql::from_ast(ast).unwrap();
assert_eq!(rsql.fields(), vec!["age", "name"]);
}
fn from_ast_validates() {
let ast = Ast::Constraint(Constraint {
field: "x".into(),
operator: Operator::Eq,
value: Value::List(vec![Value::Int(1)]),
});
assert!(RestSql::from_ast(ast).is_err());
}
fn op_eq() {
let Ast::Constraint(c) = eq("x", "hello") else {
panic!()
};
assert_eq!(c.field, "x");
assert_eq!(c.operator, Operator::Eq);
assert_eq!(c.value, Value::String("hello".into()));
}
fn op_neq() {
let Ast::Constraint(c) = neq("x", 42i64) else {
panic!()
};
assert_eq!(c.operator, Operator::Neq);
assert_eq!(c.value, Value::Int(42));
}
fn op_lt() {
let Ast::Constraint(c) = lt("price", 9.99f64) else {
panic!()
};
assert_eq!(c.operator, Operator::Lt);
assert_eq!(c.value, Value::Float(9.99));
}
fn op_lte() {
let Ast::Constraint(c) = lte("score", 100i64) else {
panic!()
};
assert_eq!(c.operator, Operator::Lte);
}
fn op_gt() {
let Ast::Constraint(c) = gt("age", 18i64) else {
panic!()
};
assert_eq!(c.operator, Operator::Gt);
}
fn op_gte() {
let Ast::Constraint(c) = gte("age", 18i64) else {
panic!()
};
assert_eq!(c.operator, Operator::Gte);
}
fn op_in() {
let Ast::Constraint(c) = in_("role", ["admin", "user"]) else {
panic!()
};
assert_eq!(c.operator, Operator::In);
assert_eq!(
c.value,
Value::List(vec![
Value::String("admin".into()),
Value::String("user".into()),
])
);
}
fn op_out() {
let Ast::Constraint(c) = out("role", ["guest", "anon"]) else {
panic!()
};
assert_eq!(c.operator, Operator::Out);
let Value::List(v) = c.value else { panic!() };
assert_eq!(v.len(), 2);
}
fn op_between() {
let Ast::Constraint(c) = between("age", 18i64, 65i64) else {
panic!()
};
assert_eq!(c.operator, Operator::Between);
assert_eq!(c.value, Value::List(vec![Value::Int(18), Value::Int(65)]));
}
fn op_null() {
let Ast::Constraint(c) = null("deleted_at") else {
panic!()
};
assert_eq!(c.operator, Operator::Null);
assert!(RestSql::from_ast(null("deleted_at")).is_ok());
}
fn op_not_null() {
let Ast::Constraint(c) = not_null("deleted_at") else {
panic!()
};
assert_eq!(c.operator, Operator::NotNull);
assert!(RestSql::from_ast(not_null("deleted_at")).is_ok());
}
fn op_like() {
let Ast::Constraint(c) = like("name", "Alice*") else {
panic!()
};
assert_eq!(c.operator, Operator::Like);
assert_eq!(c.value, Value::String("Alice*".into()));
}
fn op_ilike() {
let Ast::Constraint(c) = ilike("name", "alice*") else {
panic!()
};
assert_eq!(c.operator, Operator::Ilike);
}
fn value_from_bool() {
assert_eq!(Value::from(true), Value::Bool(true));
assert_eq!(Value::from(false), Value::Bool(false));
}
fn value_from_i32() {
assert_eq!(Value::from(42i32), Value::Int(42));
}
fn value_from_i64() {
assert_eq!(Value::from(100i64), Value::Int(100));
}
fn value_from_f32() {
assert_eq!(Value::from(1.5f32), Value::Float(1.5f64));
}
fn value_from_f64() {
assert_eq!(Value::from(2.5f64), Value::Float(2.5));
}
fn value_from_str() {
assert_eq!(Value::from("hello"), Value::String("hello".into()));
}
fn value_from_string() {
assert_eq!(
Value::from("world".to_owned()),
Value::String("world".into())
);
}
fn bitand_leaf_leaf() {
let merged = eq("a", 1i64) & eq("b", 2i64);
let Ast::And(children) = merged else {
panic!("expected And")
};
assert_eq!(children.len(), 2);
}
fn bitor_leaf_leaf() {
let merged = eq("a", 1i64) | eq("b", 2i64);
let Ast::Or(children) = merged else {
panic!("expected Or")
};
assert_eq!(children.len(), 2);
}
fn bitand_and_and_flattens() {
let a = eq("a", 1i64) & eq("b", 2i64);
let b = eq("c", 3i64) & eq("d", 4i64);
let merged = a & b;
let Ast::And(children) = merged else {
panic!("expected And")
};
assert_eq!(children.len(), 4);
}
fn bitand_and_or_wraps() {
let a = eq("a", 1i64) & eq("b", 2i64);
let b = eq("c", 3i64) | eq("d", 4i64);
let merged = a & b;
let Ast::And(children) = merged else {
panic!("expected And")
};
assert_eq!(children.len(), 3); assert!(matches!(children[2], Ast::Or(_)));
}
fn bitor_or_or_flattens() {
let a = eq("a", 1i64) | eq("b", 2i64);
let b = eq("c", 3i64) | eq("d", 4i64);
let merged = a | b;
let Ast::Or(children) = merged else {
panic!("expected Or")
};
assert_eq!(children.len(), 4);
}
fn bitor_or_and_wraps() {
let a = eq("a", 1i64) | eq("b", 2i64);
let b = eq("c", 3i64) & eq("d", 4i64);
let merged = a | b;
let Ast::Or(children) = merged else {
panic!("expected Or")
};
assert_eq!(children.len(), 3); assert!(matches!(children[2], Ast::And(_)));
}
fn ast_and_multi() {
let node = Ast::and([eq("a", 1i64), eq("b", 2i64), eq("c", 3i64)]);
let Ast::And(children) = node else {
panic!("expected And")
};
assert_eq!(children.len(), 3);
}
fn ast_or_multi() {
let node = Ast::or([eq("a", 1i64), eq("b", 2i64), eq("c", 3i64)]);
let Ast::Or(children) = node else {
panic!("expected Or")
};
assert_eq!(children.len(), 3);
}
fn try_and_empty_returns_none() {
assert!(Ast::try_and(std::iter::empty::<Ast>()).is_none());
}
fn try_or_empty_returns_none() {
assert!(Ast::try_or(std::iter::empty::<Ast>()).is_none());
}
fn try_and_single_unwraps() {
let node = Ast::try_and([eq("x", 1i64)]).unwrap();
assert!(matches!(node, Ast::Constraint(_)));
}
fn try_or_single_unwraps() {
let node = Ast::try_or([eq("x", 1i64)]).unwrap();
assert!(matches!(node, Ast::Constraint(_)));
}
fn consumer_pattern() {
struct FilmQuery {
title_pattern: Option<String>,
min_year: Option<i64>,
max_year: Option<i64>,
genres: Option<Vec<String>>,
}
impl TryFrom<FilmQuery> for RestSql {
type Error = crate::error::RestSqlError;
fn try_from(q: FilmQuery) -> Result<Self, Self::Error> {
let nodes: Vec<Ast> = [
q.title_pattern.map(|p| ilike("title", p)),
q.min_year.map(|y| gte("year", y)),
q.max_year.map(|y| lte("year", y)),
q.genres.map(|g| in_("genre", g)),
]
.into_iter()
.flatten()
.collect();
let ast = Ast::try_and(nodes).unwrap_or(Ast::Constraint(Constraint {
field: "_".into(),
operator: Operator::Eq,
value: Value::Bool(true),
}));
RestSql::from_ast(ast)
}
}
let q = FilmQuery {
title_pattern: Some("the*".into()),
min_year: Some(2000),
max_year: None,
genres: Some(vec!["drama".into(), "thriller".into()]),
};
let rsql = RestSql::try_from(q).unwrap();
assert_eq!(rsql.fields(), vec!["genre", "title", "year"]);
}
fn consumer_pattern_all_none() {
let nodes: Vec<Ast> = [None::<Ast>, None, None].into_iter().flatten().collect();
let ast = Ast::try_and(nodes).unwrap_or(Ast::Constraint(Constraint {
field: "_".into(),
operator: Operator::Eq,
value: Value::Bool(true),
}));
let rsql = RestSql::from_ast(ast).unwrap();
assert_eq!(rsql.fields(), vec!["_"]);
}
}
#[cfg(test)]
pub(crate) mod suite {
use crate::ast::{Ast, Constraint, Operator, Value};
use crate::error::ParseError;
pub fn run(parse: impl Fn(&str) -> Result<Ast, ParseError>) {
simple_eq(&parse);
and_expr(&parse);
or_expr(&parse);
in_list(&parse);
between(&parse);
null_op(&parse);
notnull_op(&parse);
fiql_aliases(&parse);
negation(&parse);
like_ilike(&parse);
nested_parens(&parse);
number_value(&parse);
bool_value(&parse);
quoted_string(&parse);
complex_expr(&parse);
}
fn simple_eq(parse: &impl Fn(&str) -> Result<Ast, ParseError>) {
let ast = parse("name==Alice").unwrap();
assert_eq!(
ast,
Ast::Constraint(Constraint {
field: "name".into(),
operator: Operator::Eq,
value: Value::String("Alice".into()),
}),
"simple_eq"
);
}
fn and_expr(parse: &impl Fn(&str) -> Result<Ast, ParseError>) {
let ast = parse("age>18;active==true").unwrap();
assert!(matches!(ast, Ast::And(_)), "and_expr");
}
fn or_expr(parse: &impl Fn(&str) -> Result<Ast, ParseError>) {
let ast = parse("status==active,status==pending").unwrap();
assert!(matches!(ast, Ast::Or(_)), "or_expr");
}
fn in_list(parse: &impl Fn(&str) -> Result<Ast, ParseError>) {
let ast = parse("role=in=(admin,user)").unwrap();
assert_eq!(
ast,
Ast::Constraint(Constraint {
field: "role".into(),
operator: Operator::In,
value: Value::List(vec![
Value::String("admin".into()),
Value::String("user".into()),
]),
}),
"in_list"
);
}
fn between(parse: &impl Fn(&str) -> Result<Ast, ParseError>) {
let ast = parse("age=between=(18,65)").unwrap();
assert!(
matches!(
ast,
Ast::Constraint(Constraint {
operator: Operator::Between,
..
})
),
"between"
);
}
fn null_op(parse: &impl Fn(&str) -> Result<Ast, ParseError>) {
let ast = parse("deleted_at=null=true").unwrap();
assert!(
matches!(
ast,
Ast::Constraint(Constraint {
operator: Operator::Null,
..
})
),
"null_op"
);
}
fn notnull_op(parse: &impl Fn(&str) -> Result<Ast, ParseError>) {
let ast = parse("deleted_at=notnull=true").unwrap();
assert!(
matches!(
ast,
Ast::Constraint(Constraint {
operator: Operator::NotNull,
..
})
),
"notnull_op"
);
}
fn fiql_aliases(parse: &impl Fn(&str) -> Result<Ast, ParseError>) {
let ast = parse("age=lt=30").unwrap();
assert!(
matches!(
ast,
Ast::Constraint(Constraint {
operator: Operator::Lt,
..
})
),
"fiql =lt="
);
let ast = parse("age=le=30").unwrap();
assert!(
matches!(
ast,
Ast::Constraint(Constraint {
operator: Operator::Lte,
..
})
),
"fiql =le="
);
let ast = parse("age=gt=30").unwrap();
assert!(
matches!(
ast,
Ast::Constraint(Constraint {
operator: Operator::Gt,
..
})
),
"fiql =gt="
);
let ast = parse("age=ge=30").unwrap();
assert!(
matches!(
ast,
Ast::Constraint(Constraint {
operator: Operator::Gte,
..
})
),
"fiql =ge="
);
let ast = parse("age=eq=30").unwrap();
assert!(
matches!(
ast,
Ast::Constraint(Constraint {
operator: Operator::Eq,
..
})
),
"fiql =eq="
);
let ast = parse("age=neq=30").unwrap();
assert!(
matches!(
ast,
Ast::Constraint(Constraint {
operator: Operator::Neq,
..
})
),
"fiql =neq="
);
}
fn negation(parse: &impl Fn(&str) -> Result<Ast, ParseError>) {
let ast = parse("age!=30").unwrap();
assert!(
matches!(
ast,
Ast::Constraint(Constraint {
operator: Operator::Neq,
..
})
),
"negation !="
);
}
fn like_ilike(parse: &impl Fn(&str) -> Result<Ast, ParseError>) {
let ast = parse("name=like=Alice*").unwrap();
assert!(
matches!(
ast,
Ast::Constraint(Constraint {
operator: Operator::Like,
..
})
),
"like"
);
let ast = parse("name=ilike=alice*").unwrap();
assert!(
matches!(
ast,
Ast::Constraint(Constraint {
operator: Operator::Ilike,
..
})
),
"ilike"
);
}
fn nested_parens(parse: &impl Fn(&str) -> Result<Ast, ParseError>) {
let ast = parse("(a==1,b==2);c==3").unwrap();
assert!(matches!(ast, Ast::And(_)), "nested_parens: top-level AND");
if let Ast::And(children) = ast {
assert!(
matches!(children[0], Ast::Or(_)),
"nested_parens: first child is OR"
);
}
}
fn number_value(parse: &impl Fn(&str) -> Result<Ast, ParseError>) {
let ast = parse("price==42.5").unwrap();
assert_eq!(
ast,
Ast::Constraint(Constraint {
field: "price".into(),
operator: Operator::Eq,
value: Value::Float(42.5),
}),
"number_value"
);
}
fn bool_value(parse: &impl Fn(&str) -> Result<Ast, ParseError>) {
let ast = parse("active==true").unwrap();
assert_eq!(
ast,
Ast::Constraint(Constraint {
field: "active".into(),
operator: Operator::Eq,
value: Value::Bool(true),
}),
"bool_value"
);
}
fn quoted_string(parse: &impl Fn(&str) -> Result<Ast, ParseError>) {
let ast = parse(r#"name=="John Doe""#).unwrap();
assert_eq!(
ast,
Ast::Constraint(Constraint {
field: "name".into(),
operator: Operator::Eq,
value: Value::String("John Doe".into()),
}),
"quoted_string"
);
}
fn complex_expr(parse: &impl Fn(&str) -> Result<Ast, ParseError>) {
let ast = parse("(a==1;b>2),c=in=(x,y)").unwrap();
assert!(matches!(ast, Ast::Or(_)), "complex_expr: top-level OR");
if let Ast::Or(children) = ast {
assert_eq!(children.len(), 2);
assert!(matches!(children[0], Ast::And(_)));
assert!(matches!(
children[1],
Ast::Constraint(Constraint {
operator: Operator::In,
..
})
));
}
}
}