#[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 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,
..
})
));
}
}
}