1use std::cell::RefCell;
16use std::collections::{HashMap, HashSet};
17
18use serde_json::{Map, Value};
19
20use crate::compose::{ComposeErrorCode, ComposeLoadError};
21use crate::expr::{Evaluator, Expression, Node};
22
23use super::choice_data::{check_single_choice_data, is_single_choice};
24use super::group_source::{group_choices, group_source_repeat, is_group_source, GroupChoicesError};
25use super::errors::{FormInputError, ValidateError};
26use super::keyed_maps::check_keyed_map_keys;
27use super::membership::Comparable;
28use super::parameters::{check, check_declared, Patterns};
29use super::rules::{get_rule, is_condition_expression, RuleContext};
30use super::visibility::is_visible;
31
32#[derive(Debug, Clone, PartialEq)]
34pub struct ValidationError {
35 pub path: String,
37 pub field: String,
39 pub rule: String,
41 pub message: String,
43 pub value: Value,
45}
46
47impl ValidationError {
48 pub fn to_value(&self) -> Value {
50 let mut m = Map::new();
51 m.insert("path".to_string(), Value::String(self.path.clone()));
52 m.insert("field".to_string(), Value::String(self.field.clone()));
53 m.insert("rule".to_string(), Value::String(self.rule.clone()));
54 m.insert("message".to_string(), Value::String(self.message.clone()));
55 m.insert("value".to_string(), self.value.clone());
56 Value::Object(m)
57 }
58}
59
60#[derive(Debug, Clone, PartialEq)]
62pub struct ValidationResult {
63 pub valid: bool,
65 pub errors: Vec<ValidationError>,
67 pub hidden: Vec<String>,
70}
71
72const ARRAY_LEVEL_RULES: &[&str] = &["required", "unique", "mincount", "maxcount"];
78
79const PATH_REFERENCE_RULES: &[&str] = &["equalTo", "containedIn", "typeFrom", "minFrom", "maxFrom", "notEqual", "unique", "enddate", "reference"];
81
82const LITERAL_PARAM_RULES: &[&str] = &["accept", "conditionTree"];
84
85const PATTERN_PARAM_RULES: &[&str] = &["match", "pattern"];
87
88const MEMBERSHIP_PARAM_RULES: &[&str] = &["in"];
95
96pub(crate) struct Run<'a> {
98 pub(crate) data: &'a Value,
100 pub(crate) unique_duplicates: RefCell<HashMap<String, HashSet<String>>>,
103 pub(crate) properties: &'a Map<String, Value>,
105 pub(crate) reference_ids: RefCell<HashMap<String, Vec<Comparable>>>,
107}
108
109pub struct Validator {
111 properties: Map<String, Value>,
112 patterns: Patterns,
113}
114
115impl Validator {
116 pub fn new(properties: Map<String, Value>) -> Result<Self, ValidateError> {
120 let patterns = check_declared(&properties)?;
121 Ok(Validator {
122 properties,
123 patterns,
124 })
125 }
126
127 pub fn validate(&self, data: &Value) -> Result<ValidationResult, ValidateError> {
132 if !data.is_object() {
133 return Err(FormInputError::new("Form data must be an object").into());
134 }
135 let mut errors: Vec<ValidationError> = Vec::new();
136 let run = Run {
137 data,
138 unique_duplicates: RefCell::new(HashMap::new()),
139 properties: &self.properties,
140 reference_ids: RefCell::new(HashMap::new()),
141 };
142 self.validate_properties(
143 &self.properties,
144 data,
145 &[],
146 &[],
147 &[],
148 &run,
149 false,
150 &mut errors,
151 )?;
152 let mut hidden = Vec::new();
153 collect_hidden(&self.properties, data, &[], &[], data, &mut hidden);
154 Ok(ValidationResult {
155 valid: errors.is_empty(),
156 errors,
157 hidden,
158 })
159 }
160
161 #[allow(clippy::too_many_arguments, reason = "the walk of the validator passes its traversal state explicitly, as the engines of the other languages do")]
169 fn validate_properties(
170 &self,
171 properties: &Map<String, Value>,
172 data: &Value,
173 current_path: &[String],
174 row_keys: &[usize],
175 declaration_path: &[String],
176 run: &Run<'_>,
177 hidden: bool,
178 errors: &mut Vec<ValidationError>,
179 ) -> Result<(), ValidateError> {
180 if let Some(object) = data.as_object() {
181 let mut unknown: Vec<&String> = object
182 .keys()
183 .filter(|name| !properties.contains_key(name.as_str()))
184 .collect();
185 unknown.sort();
186 if let Some(name) = unknown.first() {
187 let mut path = current_path.to_vec();
188 path.push((*name).clone());
189 return Err(FormInputError::new(format!(
190 "Unknown form data field: {}",
191 path_to_string(&path)
192 ))
193 .into());
194 }
195 }
196 let all_data = run.data;
197 let empty = Value::Object(Map::new());
198 for (property_key, field) in properties {
199 if !field.is_object() {
200 continue;
201 }
202 let is_multiple = is_multiple(field);
203 let mut field_path = current_path.to_vec();
204 field_path.push(property_key.clone());
205 let mut declaration = declaration_path.to_vec();
206 declaration.push(property_key.clone());
207 let declaration = declaration.as_slice();
208 let hidden = hidden || !is_visible(field, all_data, &field_path, row_keys);
211 let present = data.get(property_key);
212 let field_value = present.cloned().unwrap_or(Value::Null);
213
214 if is_multiple && present.is_some_and(|value| !value.is_object()) {
215 return Err(FormInputError::new(format!(
216 "Repeated data must be a keyed object: {}",
217 path_to_string(&field_path)
218 ))
219 .into());
220 }
221 if let Some(rows) = present.and_then(Value::as_object).filter(|_| is_multiple) {
222 check_keyed_map_keys(field, rows, &field_path, row_keys, all_data)?;
223 }
224 if let Some(value) = present.filter(|_| is_single_choice(field)) {
225 check_single_choice_data(value, &field_path, is_multiple)?;
226 }
227
228 if let Some(child_props) = is_group_with_properties(field) {
229 if is_multiple {
230 if let Some(Value::Object(rows)) = present {
232 let mut keys: Vec<&String> = rows.keys().collect();
235 keys.sort();
236 for key in keys {
237 let mut row_path = field_path.clone();
238 row_path.push(key.clone());
239 let row = &rows[key.as_str()];
240 if !row.is_object() {
241 return Err(FormInputError::new(format!(
242 "Group data must be an object: {}",
243 path_to_string(&row_path)
244 ))
245 .into());
246 }
247 let mut row_rows = row_keys.to_vec();
248 row_rows.push(field_path.len());
249 self.validate_properties(
250 child_props,
251 row,
252 &row_path,
253 &row_rows,
254 declaration,
255 run,
256 hidden,
257 errors,
258 )?;
259 }
260 }
261 if !hidden {
262 errors.extend(self.validate_field_rules(
263 field,
264 &field_value,
265 &field_path,
266 row_keys,
267 declaration,
268 run,
269 false,
270 )?);
271 }
272 } else {
273 if present.is_some_and(|value| !value.is_object()) {
274 return Err(FormInputError::new(format!(
275 "Group data must be an object: {}",
276 path_to_string(&field_path)
277 ))
278 .into());
279 }
280 let nested = present.unwrap_or(&empty);
281 self.validate_properties(
282 child_props,
283 nested,
284 &field_path,
285 row_keys,
286 declaration,
287 run,
288 hidden,
289 errors,
290 )?;
291 if !hidden {
292 errors.extend(self.validate_field_rules(
293 field,
294 &field_value,
295 &field_path,
296 row_keys,
297 declaration,
298 run,
299 false,
300 )?);
301 }
302 }
303 } else if hidden {
304 } else if is_multiple {
306 let no_rows = Map::new();
308 let rows = match present {
309 Some(Value::Object(rows)) => rows,
310 _ => &no_rows,
311 };
312 self.validate_multiple_field_rules(
313 field,
314 &field_value,
315 rows,
316 &field_path,
317 row_keys,
318 declaration,
319 run,
320 errors,
321 )?;
322 } else {
323 errors.extend(self.validate_field_rules(
324 field,
325 &field_value,
326 &field_path,
327 row_keys,
328 declaration,
329 run,
330 false,
331 )?);
332 }
333 }
334 Ok(())
335 }
336
337 #[allow(clippy::too_many_arguments, reason = "the walk of the validator passes its traversal state explicitly, as the engines of the other languages do")]
343 fn validate_multiple_field_rules(
344 &self,
345 field: &Value,
346 values: &Value,
347 rows: &Map<String, Value>,
348 field_path: &[String],
349 row_keys: &[usize],
350 declaration: &[String],
351 run: &Run<'_>,
352 errors: &mut Vec<ValidationError>,
353 ) -> Result<(), ValidateError> {
354 let messages = field.get("messages");
355 let mut entries: Vec<(String, &Value)> = rows.iter().map(|(k, v)| (k.clone(), v)).collect();
356 entries.sort_by(|a, b| a.0.cmp(&b.0));
357
358 if let Some(rules) = normalize_validate_slot(field) {
360 for (rule_name, rule_value) in rules {
361 if !ARRAY_LEVEL_RULES.contains(&rule_name.as_str()) {
362 continue;
363 }
364 if let Some(message) = self.run_rule(
365 rule_name,
366 rule_value,
367 values,
368 field_path,
369 row_keys,
370 declaration,
371 messages,
372 run,
373 )? {
374 errors.push(ValidationError {
375 path: path_to_string(field_path),
376 field: field_name(field_path),
377 rule: rule_name.clone(),
378 message,
379 value: values.clone(),
380 });
381 return Ok(());
382 }
383 }
384 }
385
386 for (key, value) in entries {
388 let mut item_path = field_path.to_vec();
389 item_path.push(key);
390 let mut item_rows = row_keys.to_vec();
391 item_rows.push(field_path.len());
392 errors.extend(self.validate_field_rules(
393 field,
394 value,
395 &item_path,
396 &item_rows,
397 declaration,
398 run,
399 true,
400 )?);
401 }
402 Ok(())
403 }
404
405 #[allow(clippy::too_many_arguments, reason = "the walk of the validator passes its traversal state explicitly, as the engines of the other languages do")]
408 fn validate_field_rules(
409 &self,
410 field: &Value,
411 value: &Value,
412 field_path: &[String],
413 row_keys: &[usize],
414 declaration: &[String],
415 run: &Run<'_>,
416 element_level: bool,
417 ) -> Result<Option<ValidationError>, ValidateError> {
418 let messages = field.get("messages");
419 let rules = normalize_validate_slot(field);
420
421 if let Some(error) =
422 self.run_implicit_number(field, rules, value, field_path, row_keys, messages, run)?
423 {
424 return Ok(Some(error));
425 }
426 if let Some(error) =
427 self.run_group_source(field, value, field_path, row_keys, declaration, messages, run)?
428 {
429 return Ok(Some(error));
430 }
431 let rules = match rules {
432 Some(r) => r,
433 None => return Ok(None),
434 };
435 for (rule_name, rule_value) in rules {
436 if element_level && ARRAY_LEVEL_RULES.contains(&rule_name.as_str()) {
437 continue;
438 }
439 if let Some(message) = self.run_rule(
440 rule_name,
441 rule_value,
442 value,
443 field_path,
444 row_keys,
445 declaration,
446 messages,
447 run,
448 )? {
449 return Ok(Some(ValidationError {
450 path: path_to_string(field_path),
451 field: field_name(field_path),
452 rule: rule_name.clone(),
453 message,
454 value: value.clone(),
455 }));
456 }
457 }
458 Ok(None)
459 }
460
461 #[allow(clippy::too_many_arguments, reason = "the walk of the validator passes its traversal state explicitly, as the engines of the other languages do")]
465 fn run_group_source(
466 &self,
467 field: &Value,
468 value: &Value,
469 path: &[String],
470 row_keys: &[usize],
471 declaration: &[String],
472 messages: Option<&Value>,
473 run: &Run<'_>,
474 ) -> Result<Option<ValidationError>, ValidateError> {
475 let Some(items) = field.get("items").filter(|items| is_group_source(items)) else {
476 return Ok(None);
477 };
478 if let Some(repeat) = group_source_repeat(items, run.data) {
480 return Err(FormInputError::new(repeat).into());
481 }
482 let choices = group_choices(items, run.data, &declaration.join(".")).map_err(|error| match error {
483 GroupChoicesError::Declaration(message) => ValidateError::Load(ComposeLoadError::with_trace(
484 ComposeErrorCode::InvalidRuleParameter,
485 message,
486 declaration.to_vec(),
487 )),
488 GroupChoicesError::Repeated(message) => ValidateError::Input(FormInputError::new(message)),
489 })?;
490 let members = Value::Array(
491 choices
492 .into_iter()
493 .map(|choice| Value::String(choice.value))
494 .collect(),
495 );
496 let Some(message) =
497 self.run_rule("in", &members, value, path, row_keys, declaration, messages, run)?
498 else {
499 return Ok(None);
500 };
501 Ok(Some(ValidationError {
502 path: path_to_string(path),
503 field: field_name(path),
504 rule: "in".to_string(),
505 message,
506 value: value.clone(),
507 }))
508 }
509
510 #[allow(clippy::too_many_arguments, reason = "the walk of the validator passes its traversal state explicitly, as the engines of the other languages do")]
513 fn run_implicit_number(
514 &self,
515 field: &Value,
516 rules: Option<&Map<String, Value>>,
517 value: &Value,
518 path: &[String],
519 row_keys: &[usize],
520 messages: Option<&Value>,
521 run: &Run<'_>,
522 ) -> Result<Option<ValidationError>, ValidateError> {
523 if field.get("type").and_then(Value::as_str) != Some("number") {
524 return Ok(None);
525 }
526 if rules.map(|r| r.contains_key("number")).unwrap_or(false) {
527 return Ok(None);
528 }
529 if let Some(message) = self.run_rule(
531 "number",
532 &Value::Bool(true),
533 value,
534 path,
535 row_keys,
536 &[],
537 messages,
538 run,
539 )? {
540 return Ok(Some(ValidationError {
541 path: path_to_string(path),
542 field: field_name(path),
543 rule: "number".to_string(),
544 message,
545 value: value.clone(),
546 }));
547 }
548 Ok(None)
549 }
550
551 #[allow(clippy::too_many_arguments, reason = "the walk of the validator passes its traversal state explicitly, as the engines of the other languages do")]
560 fn run_rule(
561 &self,
562 rule_name: &str,
563 rule_value: &Value,
564 value: &Value,
565 current_path: &[String],
566 row_keys: &[usize],
567 declaration: &[String],
568 messages: Option<&Value>,
569 run: &Run<'_>,
570 ) -> Result<Option<String>, ValidateError> {
571 let all_data = run.data;
572 let effective =
573 self.resolve_rule_value(rule_name, rule_value, current_path, row_keys, all_data);
574
575 if is_disabled(&effective) {
577 return Ok(None);
578 }
579
580 let Some(rule_fn) = get_rule(rule_name) else {
582 unreachable!("rule {rule_name} is not registered");
583 };
584
585 let parameter = check(rule_name, &effective, declaration, &self.patterns)?;
588
589 let ctx = RuleContext {
590 value,
591 rule_param: &effective,
592 parameter: ¶meter,
593 messages,
594 rule_name,
595 path_segments: current_path,
596 row_keys,
597 form_data: all_data,
598 run,
599 };
600 Ok(rule_fn(&ctx))
601 }
602
603 fn resolve_rule_value(
605 &self,
606 rule_name: &str,
607 rule_value: &Value,
608 current_path: &[String],
609 row_keys: &[usize],
610 all_data: &Value,
611 ) -> Value {
612 match resolution(rule_name, rule_value) {
613 Resolution::Verbatim | Resolution::Literal => rule_value.clone(),
614 Resolution::ConditionMap(map) => {
615 self.resolve_condition_map(map, current_path, row_keys, all_data)
616 }
617 Resolution::Ternary(node) => {
618 Evaluator::new(all_data, current_path, row_keys).evaluate_value(&node)
619 }
620 Resolution::Expression(expression) => {
621 evaluate_expression_value(expression, all_data, current_path, row_keys)
622 }
623 }
624 }
625
626 fn resolve_condition_map(
629 &self,
630 map: &Map<String, Value>,
631 current_path: &[String],
632 row_keys: &[usize],
633 all_data: &Value,
634 ) -> Value {
635 for (key, val) in map {
636 if key == "true" {
637 continue;
638 }
639 if evaluate_condition(key, all_data, current_path, row_keys) {
640 return val.clone();
641 }
642 }
643 if let Some(v) = map.get("true") {
644 return v.clone();
645 }
646 Value::Null
647 }
648}
649
650enum Resolution<'a> {
656 Verbatim,
658 ConditionMap(&'a Map<String, Value>),
660 Ternary(Node),
662 Expression(&'a str),
664 Literal,
666}
667
668fn resolution<'a>(rule_name: &str, rule_value: &'a Value) -> Resolution<'a> {
669 if PATH_REFERENCE_RULES.contains(&rule_name)
673 || LITERAL_PARAM_RULES.contains(&rule_name)
674 || PATTERN_PARAM_RULES.contains(&rule_name)
675 || MEMBERSHIP_PARAM_RULES.contains(&rule_name)
676 {
677 return Resolution::Verbatim;
678 }
679 match rule_value {
680 Value::Object(map) => Resolution::ConditionMap(map),
681 Value::String(s) => {
682 if let Ok(node @ Node::Ternary { .. }) = Expression::parse(s) {
685 Resolution::Ternary(node)
686 } else if is_condition_expression(s) && !has_ternary_regex(s) {
687 Resolution::Expression(s)
688 } else {
689 Resolution::Literal
690 }
691 }
692 _ => Resolution::Literal,
693 }
694}
695
696pub(crate) fn is_conditional(rule_name: &str, rule_value: &Value) -> bool {
699 matches!(
700 resolution(rule_name, rule_value),
701 Resolution::ConditionMap(_) | Resolution::Ternary(_) | Resolution::Expression(_)
702 )
703}
704
705pub(crate) fn selectable_literals(rule_name: &str, rule_value: &Value) -> Vec<Value> {
710 let mut literals = Vec::new();
711 match resolution(rule_name, rule_value) {
712 Resolution::ConditionMap(map) => literals.extend(map.values().cloned()),
713 Resolution::Ternary(node) => {
714 let mut pending = vec![&node];
715 while let Some(node) = pending.pop() {
716 match node {
717 Node::Ternary {
718 true_value,
719 false_value,
720 ..
721 } => {
722 pending.push(false_value);
724 pending.push(true_value);
725 }
726 Node::Group(inner) => pending.push(inner),
727 Node::Literal(literal) => literals.push(literal.to_value()),
728 _ => {}
729 }
730 }
731 }
732 Resolution::Verbatim | Resolution::Expression(_) | Resolution::Literal => {}
733 }
734 literals
735}
736
737pub(crate) fn is_disabled(parameter: &Value) -> bool {
739 matches!(parameter, Value::Bool(false) | Value::Null)
740}
741
742fn collect_hidden(
746 properties: &Map<String, Value>,
747 data: &Value,
748 current_path: &[String],
749 row_keys: &[usize],
750 all_data: &Value,
751 hidden: &mut Vec<String>,
752) {
753 for (name, field) in properties {
754 if !field.is_object() {
755 continue;
756 }
757 let mut field_path = current_path.to_vec();
758 field_path.push(name.clone());
759 if !is_visible(field, all_data, &field_path, row_keys) {
760 hidden.push(path_to_string(&field_path));
761 }
762 let Some(children) = is_group_with_properties(field) else {
763 continue;
764 };
765 let empty = Value::Object(Map::new());
766 let value = data
767 .get(name)
768 .filter(|value| value.is_object())
769 .unwrap_or(&empty);
770 if !is_multiple(field) {
771 collect_hidden(children, value, &field_path, row_keys, all_data, hidden);
772 continue;
773 }
774 let rows = value.as_object().expect("an object");
775 let mut keys: Vec<&String> = rows.keys().collect();
776 keys.sort();
777 for key in keys {
778 let mut row_path = field_path.clone();
779 row_path.push(key.clone());
780 let mut row_rows = row_keys.to_vec();
781 row_rows.push(field_path.len());
782 let row = Some(&rows[key.as_str()])
783 .filter(|row| row.is_object())
784 .unwrap_or(&empty);
785 collect_hidden(children, row, &row_path, &row_rows, all_data, hidden);
786 }
787 }
788}
789
790pub(crate) fn is_group_with_properties(field: &Value) -> Option<&Map<String, Value>> {
792 if field.get("type").and_then(Value::as_str) != Some("group") {
793 return None;
794 }
795 field.get("properties").and_then(Value::as_object)
796}
797
798#[allow(clippy::disallowed_methods, reason = "a condition that does not parse is false")]
806fn evaluate_condition(
807 expression: &str,
808 form_data: &Value,
809 current_path: &[String],
810 row_keys: &[usize],
811) -> bool {
812 Expression::evaluate(expression, form_data, current_path, row_keys).unwrap_or(false)
813}
814
815#[allow(clippy::disallowed_methods, reason = "a value expression that does not parse is false")]
819fn evaluate_expression_value(
820 expression: &str,
821 form_data: &Value,
822 current_path: &[String],
823 row_keys: &[usize],
824) -> Value {
825 Expression::evaluate_value(expression, form_data, current_path, row_keys)
826 .unwrap_or(Value::Bool(false))
827}
828
829pub(crate) fn is_multiple(field: &Value) -> bool {
835 match field.get("multiple") {
836 Some(Value::Bool(true)) | Some(Value::Object(_)) => true,
837 Some(Value::String(keyword)) => keyword == "only",
838 _ => false,
839 }
840}
841
842fn normalize_validate_slot(field: &Value) -> Option<&Map<String, Value>> {
845 match field.get("validate") {
846 Some(Value::Object(m)) => Some(m),
847 _ => None,
848 }
849}
850
851pub(crate) fn path_to_string(path: &[String]) -> String {
856 path.join(".")
857}
858
859fn field_name(path: &[String]) -> String {
860 path.last().cloned().unwrap_or_default()
861}
862
863pub(crate) fn has_ternary_regex(s: &str) -> bool {
869 if let Some(q) = s.find('?') {
870 return s[q + 1..].contains(':');
871 }
872 false
873}