use crate::error::{CoreError, Result};
use crate::filter::{FilterInput, FilterNode};
pub const MAX_LIMIT: u64 = 1000;
pub const MAX_QUERY_LEN: usize = 500;
pub const MAX_FILTER_DEPTH: usize = 5;
fn invalid(message: impl Into<String>) -> CoreError {
CoreError::Validation {
message: message.into(),
}
}
pub fn validate_limit(limit: i64) -> Result<()> {
let value = u64::try_from(limit).map_err(|_| invalid("limit must be >= 1"))?;
crate::pagination::Limit::new(value).map(|_| ())
}
pub fn validate_offset(page: i64, limit: i64) -> Result<()> {
if page < 1 {
return Err(invalid("page must be >= 1"));
}
validate_limit(limit)
}
pub fn validate_cursor(limit: i64, has_after: bool, has_before: bool) -> Result<()> {
validate_limit(limit)?;
if has_after && has_before {
return Err(invalid("after and before are mutually exclusive"));
}
Ok(())
}
pub fn validate_search_query(query: &str) -> Result<()> {
if query.chars().take(MAX_QUERY_LEN + 1).count() > MAX_QUERY_LEN {
return Err(invalid(format!(
"Query must not exceed {MAX_QUERY_LEN} characters"
)));
}
Ok(())
}
pub fn validate_filter_depth(depth: usize) -> Result<()> {
if depth > MAX_FILTER_DEPTH {
return Err(invalid(format!(
"FilterGroup nesting must not exceed {MAX_FILTER_DEPTH} levels"
)));
}
Ok(())
}
pub fn validate_filter_input(input: &FilterInput) -> Result<()> {
let depth = match input {
FilterInput::Flat(_) => 1,
FilterInput::Group(group) => group_depth(&group.conditions),
};
validate_filter_depth(depth)
}
fn group_depth(conditions: &[FilterNode]) -> usize {
let deepest_child = conditions
.iter()
.map(|node| match node {
FilterNode::Group(sub) => group_depth(&sub.conditions),
FilterNode::Spec(_) => 0,
})
.max()
.unwrap_or(0);
1 + deepest_child
}
#[cfg(test)]
mod tests {
use super::*;
fn msg(result: Result<()>) -> String {
match result {
Err(CoreError::Validation { message }) => message,
other => panic!("expected a validation error, got {other:?}"),
}
}
#[test]
fn limit_bounds() {
assert!(validate_limit(1).is_ok());
assert!(validate_limit(MAX_LIMIT as i64).is_ok());
assert_eq!(msg(validate_limit(0)), "limit must be >= 1");
assert!(msg(validate_limit(MAX_LIMIT as i64 + 1)).starts_with("limit must not exceed"));
}
#[test]
fn offset_requires_positive_page() {
assert!(validate_offset(1, 20).is_ok());
assert_eq!(msg(validate_offset(0, 20)), "page must be >= 1");
assert_eq!(msg(validate_offset(1, 0)), "limit must be >= 1");
}
#[test]
fn cursor_after_before_mutually_exclusive() {
assert!(validate_cursor(20, true, false).is_ok());
assert!(validate_cursor(20, false, false).is_ok());
assert_eq!(
msg(validate_cursor(20, true, true)),
"after and before are mutually exclusive"
);
}
#[test]
fn query_length_bound() {
assert!(validate_search_query(&"a".repeat(MAX_QUERY_LEN)).is_ok());
assert!(
msg(validate_search_query(&"a".repeat(MAX_QUERY_LEN + 1))).contains("500 characters")
);
}
#[test]
fn filter_depth_bound() {
assert!(validate_filter_depth(MAX_FILTER_DEPTH).is_ok());
assert!(msg(validate_filter_depth(MAX_FILTER_DEPTH + 1)).contains("5 levels"));
}
#[test]
fn filter_input_depth_is_measured_and_capped() {
use crate::filter::{FilterGroup, FilterLogic, FilterOp, FilterSpec};
use crate::value::Value;
fn leaf() -> FilterNode {
FilterNode::Spec(FilterSpec {
field: "a".into(),
op: FilterOp::Eq,
value: Value::Int(1),
logic: FilterLogic::And,
})
}
fn nest(n: usize) -> FilterInput {
let mut node = leaf();
for _ in 0..n {
node = FilterNode::Group(FilterGroup {
logic: FilterLogic::And,
conditions: vec![node],
});
}
match node {
FilterNode::Group(group) => FilterInput::Group(group),
FilterNode::Spec(spec) => FilterInput::Flat(vec![spec]),
}
}
assert!(validate_filter_input(&nest(MAX_FILTER_DEPTH)).is_ok()); assert!(validate_filter_input(&nest(MAX_FILTER_DEPTH + 1)).is_err()); assert!(validate_filter_input(&FilterInput::Flat(vec![])).is_ok()); }
}