1use std::collections::{BTreeMap, BTreeSet};
5
6use crate::ast::Criteria;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
10#[non_exhaustive]
11pub enum FieldType {
12 Text,
14 Number,
16 Date,
18 Boolean,
20 Any,
22}
23
24#[derive(Clone, Debug, Default)]
36pub struct Schema {
37 fields: BTreeMap<String, FieldType>,
38 collections: BTreeMap<String, Schema>,
39 parameters: BTreeMap<String, FieldType>,
40 functions: BTreeSet<String>,
41}
42
43impl Schema {
44 pub fn new() -> Self {
46 Self::default()
47 }
48
49 pub fn field(mut self, path: &str, kind: FieldType) -> Self {
51 self.fields.insert(path.to_owned(), kind);
52 self
53 }
54
55 pub fn collection(mut self, path: &str, rows: Schema) -> Self {
57 self.collections.insert(path.to_owned(), rows);
58 self
59 }
60
61 pub fn parameter(mut self, name: &str, kind: FieldType) -> Self {
63 self.parameters.insert(name.to_owned(), kind);
64 self
65 }
66
67 pub fn function(mut self, name: &str) -> Self {
69 self.functions.insert(name.to_lowercase());
70 self
71 }
72}
73
74#[derive(Clone, Debug, PartialEq, Eq)]
76#[non_exhaustive]
77pub enum Problem {
78 UnknownField(String),
80 UnknownCollection(String),
82 UnknownParameter(String),
84 UnknownFunction(String),
86}
87
88impl std::fmt::Display for Problem {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 match self {
91 Self::UnknownField(n) => write!(f, "unknown field '{n}'"),
92 Self::UnknownCollection(n) => write!(f, "unknown collection '{n}'"),
93 Self::UnknownParameter(n) => write!(f, "unknown parameter '{n}'"),
94 Self::UnknownFunction(n) => write!(f, "unknown function '{n}'"),
95 }
96 }
97}
98
99pub fn validate(criteria: &Criteria, schema: &Schema) -> Vec<Problem> {
101 let mut problems = Vec::new();
102 check(criteria, schema, None, &mut problems);
103 problems
104}
105
106fn add(problems: &mut Vec<Problem>, p: Problem) {
107 if !problems.contains(&p) {
108 problems.push(p);
109 }
110}
111
112fn check(c: &Criteria, schema: &Schema, parent: Option<&Schema>, problems: &mut Vec<Problem>) {
113 match c {
114 Criteria::Property(path) => {
115 let (owner, path) = match path.split_first() {
116 Some((first, rest)) if first == "^" => (parent.unwrap_or(schema), rest),
117 _ => (schema, path.as_slice()),
118 };
119 let joined = path.join(".");
120 if !owner.fields.contains_key(&joined) && !owner.collections.contains_key(&joined) {
121 add(problems, Problem::UnknownField(joined));
122 }
123 }
124 Criteria::Parameter(name) => {
125 if !schema.parameters.contains_key(name)
126 && !parent.is_some_and(|p| p.parameters.contains_key(name))
127 {
128 add(problems, Problem::UnknownParameter(name.clone()));
129 }
130 }
131 Criteria::Custom(name, args) => {
132 if !schema.functions.contains(&name.to_lowercase())
133 && !parent.is_some_and(|p| p.functions.contains(&name.to_lowercase()))
134 {
135 add(problems, Problem::UnknownFunction(name.clone()));
136 }
137 args.iter().for_each(|a| check(a, schema, parent, problems));
138 }
139 Criteria::Aggregate(call) => match &call.collection {
140 Some(path) => {
141 let joined = path.join(".");
142 match schema.collections.get(&joined) {
143 Some(rows) => {
144 let inner = call.condition.iter().chain(&call.args);
145 inner.for_each(|a| check(a, rows, Some(schema), problems));
146 }
147 None => add(problems, Problem::UnknownCollection(joined)),
148 }
149 }
150 None => call.condition.iter().chain(&call.args).for_each(|a| check(a, schema, parent, problems)),
151 },
152 Criteria::Value(_) => {}
153 Criteria::Unary(_, a) | Criteria::IsNull(a) => check(a, schema, parent, problems),
154 Criteria::Binary(_, a, b) => {
155 check(a, schema, parent, problems);
156 check(b, schema, parent, problems);
157 }
158 Criteria::Group(_, items) | Criteria::Function(_, items) => {
159 items.iter().for_each(|a| check(a, schema, parent, problems));
160 }
161 Criteria::In(a, items) => {
162 check(a, schema, parent, problems);
163 items.iter().for_each(|i| check(i, schema, parent, problems));
164 }
165 Criteria::Between(a, b, c) => {
166 check(a, schema, parent, problems);
167 check(b, schema, parent, problems);
168 check(c, schema, parent, problems);
169 }
170 }
171}