Skip to main content

cherenkov_model_data/discovery/
query.rs

1//! Schema validation checks operands; adapters evaluate them against metadata.
2
3use super::{FilterOperator, SearchQuery, SearchSchema, ValueType};
4use anyhow::{Context, Result, ensure};
5use serde_json::Value;
6
7impl SearchSchema {
8    /// Reject unsupported text, unknown fields, unsupported operators, and invalid
9    /// operands. Validation does not perform I/O or interpret provider metadata.
10    pub fn validate(&self, query: &SearchQuery) -> Result<()> {
11        query.page.validate(self.max_page_size)?;
12        ensure!(
13            self.text_search || query.text.is_none(),
14            "this store does not support text search"
15        );
16
17        for filter in &query.filters {
18            let field = self
19                .fields
20                .get(&filter.field)
21                .with_context(|| format!("unsupported search field {:?}", filter.field))?;
22
23            ensure!(
24                field.operators.contains(&filter.operator),
25                "unsupported operator {:?} for search field {:?}",
26                filter.operator,
27                filter.field
28            );
29            ensure!(
30                field.value_type.accepts(&filter.value, filter.operator),
31                "invalid value for search field {:?}: expected {:?}",
32                filter.field,
33                field.value_type
34            );
35        }
36
37        Ok(())
38    }
39}
40
41impl ValueType {
42    /// Apply the field's scalar type to one operand or every `AnyOf` element.
43    fn accepts(&self, value: &Value, operator: FilterOperator) -> bool {
44        if operator != FilterOperator::AnyOf {
45            return self.accepts_scalar(value);
46        }
47
48        value.as_array().is_some_and(|values| {
49            !values.is_empty() && values.iter().all(|value| self.accepts_scalar(value))
50        })
51    }
52
53    /// Check JSON representation without coercion or loss of integer precision.
54    fn accepts_scalar(&self, value: &Value) -> bool {
55        match self {
56            Self::String => value.is_string(),
57            Self::Boolean => value.is_boolean(),
58            Self::Integer => value.is_i64() || value.is_u64(),
59            Self::Unsigned => value.is_u64(),
60            Self::Number => value.is_number(),
61            Self::Choice { values } => value
62                .as_str()
63                .is_some_and(|text| values.iter().any(|v| v == text)),
64        }
65    }
66}