1use crate::parsing::ast::{DateTimeValue, LemmaSpec, TimeValue};
7use crate::planning::semantics::{
8 Expression, ExpressionKind, FactPath, LemmaType, RulePath, SemanticConversionTarget,
9 TypeSpecification,
10};
11use crate::Error;
12use crate::Source;
13use indexmap::IndexMap;
14use rust_decimal::Decimal;
15use std::cmp::Ordering;
16use std::collections::{HashMap, HashSet};
17use std::sync::Arc;
18
19pub fn validate_type_specifications(
29 specs: &TypeSpecification,
30 type_name: &str,
31 source: &Source,
32) -> Vec<Error> {
33 let mut errors = Vec::new();
34
35 match specs {
36 TypeSpecification::Scale {
37 minimum,
38 maximum,
39 decimals,
40 precision,
41 default,
42 units,
43 ..
44 } => {
45 if let (Some(min), Some(max)) = (minimum, maximum) {
47 if min > max {
48 errors.push(Error::validation(
49 format!(
50 "Type '{}' has invalid range: minimum {} is greater than maximum {}",
51 type_name, min, max
52 ),
53 Some(source.clone()),
54 None::<String>,
55 ));
56 }
57 }
58
59 if let Some(d) = decimals {
61 if *d > 28 {
62 errors.push(Error::validation(
63 format!(
64 "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
65 type_name, d
66 ),
67 Some(source.clone()),
68 None::<String>,
69 ));
70 }
71 }
72
73 if let Some(prec) = precision {
75 if *prec <= Decimal::ZERO {
76 errors.push(Error::validation(
77 format!(
78 "Type '{}' has invalid precision: {}. Must be positive",
79 type_name, prec
80 ),
81 Some(source.clone()),
82 None::<String>,
83 ));
84 }
85 }
86
87 if let Some((def_value, def_unit)) = default {
89 if !units.iter().any(|u| u.name == *def_unit) {
91 errors.push(Error::validation(
92 format!(
93 "Type '{}' default unit '{}' is not a valid unit. Valid units: {}",
94 type_name,
95 def_unit,
96 units
97 .iter()
98 .map(|u| u.name.clone())
99 .collect::<Vec<_>>()
100 .join(", ")
101 ),
102 Some(source.clone()),
103 None::<String>,
104 ));
105 }
106 if let Some(min) = minimum {
107 if *def_value < *min {
108 errors.push(Error::validation(
109 format!(
110 "Type '{}' default value {} {} is less than minimum {}",
111 type_name, def_value, def_unit, min
112 ),
113 Some(source.clone()),
114 None::<String>,
115 ));
116 }
117 }
118 if let Some(max) = maximum {
119 if *def_value > *max {
120 errors.push(Error::validation(
121 format!(
122 "Type '{}' default value {} {} is greater than maximum {}",
123 type_name, def_value, def_unit, max
124 ),
125 Some(source.clone()),
126 None::<String>,
127 ));
128 }
129 }
130 }
131
132 if units.is_empty() {
134 errors.push(Error::validation(
135 format!(
136 "Type '{}' is a scale type but has no units. Scale types must define at least one unit (e.g. -> unit eur 1).",
137 type_name
138 ),
139 Some(source.clone()),
140 None::<String>,
141 ));
142 }
143
144 if !units.is_empty() {
146 let mut seen_names: Vec<String> = Vec::new();
147 for unit in units.iter() {
148 if unit.name.trim().is_empty() {
150 errors.push(Error::validation(
151 format!(
152 "Type '{}' has a unit with empty name. Unit names cannot be empty.",
153 type_name
154 ),
155 Some(source.clone()),
156 None::<String>,
157 ));
158 }
159
160 let lower_name = unit.name.to_lowercase();
162 if seen_names
163 .iter()
164 .any(|seen| seen.to_lowercase() == lower_name)
165 {
166 errors.push(Error::validation(
167 format!("Type '{}' has duplicate unit name '{}' (case-insensitive). Unit names must be unique within a type.", type_name, unit.name),
168 Some(source.clone()),
169 None::<String>,
170 ));
171 } else {
172 seen_names.push(unit.name.clone());
173 }
174
175 if unit.value <= Decimal::ZERO {
177 errors.push(Error::validation(
178 format!("Type '{}' has unit '{}' with invalid value {}. Unit values must be positive (conversion factor relative to type base).", type_name, unit.name, unit.value),
179 Some(source.clone()),
180 None::<String>,
181 ));
182 }
183 }
184 }
185 }
186 TypeSpecification::Number {
187 minimum,
188 maximum,
189 decimals,
190 precision,
191 default,
192 ..
193 } => {
194 if let (Some(min), Some(max)) = (minimum, maximum) {
196 if min > max {
197 errors.push(Error::validation(
198 format!(
199 "Type '{}' has invalid range: minimum {} is greater than maximum {}",
200 type_name, min, max
201 ),
202 Some(source.clone()),
203 None::<String>,
204 ));
205 }
206 }
207
208 if let Some(d) = decimals {
210 if *d > 28 {
211 errors.push(Error::validation(
212 format!(
213 "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
214 type_name, d
215 ),
216 Some(source.clone()),
217 None::<String>,
218 ));
219 }
220 }
221
222 if let Some(prec) = precision {
224 if *prec <= Decimal::ZERO {
225 errors.push(Error::validation(
226 format!(
227 "Type '{}' has invalid precision: {}. Must be positive",
228 type_name, prec
229 ),
230 Some(source.clone()),
231 None::<String>,
232 ));
233 }
234 }
235
236 if let Some(def) = default {
238 if let Some(min) = minimum {
239 if *def < *min {
240 errors.push(Error::validation(
241 format!(
242 "Type '{}' default value {} is less than minimum {}",
243 type_name, def, min
244 ),
245 Some(source.clone()),
246 None::<String>,
247 ));
248 }
249 }
250 if let Some(max) = maximum {
251 if *def > *max {
252 errors.push(Error::validation(
253 format!(
254 "Type '{}' default value {} is greater than maximum {}",
255 type_name, def, max
256 ),
257 Some(source.clone()),
258 None::<String>,
259 ));
260 }
261 }
262 }
263 }
265
266 TypeSpecification::Ratio {
267 minimum,
268 maximum,
269 decimals,
270 default,
271 units,
272 ..
273 } => {
274 if let Some(d) = decimals {
276 if *d > 28 {
277 errors.push(Error::validation(
278 format!(
279 "Type '{}' has invalid decimals value: {}. Must be between 0 and 28",
280 type_name, d
281 ),
282 Some(source.clone()),
283 None::<String>,
284 ));
285 }
286 }
287
288 if let (Some(min), Some(max)) = (minimum, maximum) {
290 if min > max {
291 errors.push(Error::validation(
292 format!(
293 "Type '{}' has invalid range: minimum {} is greater than maximum {}",
294 type_name, min, max
295 ),
296 Some(source.clone()),
297 None::<String>,
298 ));
299 }
300 }
301
302 if let Some(def) = default {
304 if let Some(min) = minimum {
305 if *def < *min {
306 errors.push(Error::validation(
307 format!(
308 "Type '{}' default value {} is less than minimum {}",
309 type_name, def, min
310 ),
311 Some(source.clone()),
312 None::<String>,
313 ));
314 }
315 }
316 if let Some(max) = maximum {
317 if *def > *max {
318 errors.push(Error::validation(
319 format!(
320 "Type '{}' default value {} is greater than maximum {}",
321 type_name, def, max
322 ),
323 Some(source.clone()),
324 None::<String>,
325 ));
326 }
327 }
328 }
329
330 if !units.is_empty() {
334 let mut seen_names: Vec<String> = Vec::new();
335 for unit in units.iter() {
336 if unit.name.trim().is_empty() {
338 errors.push(Error::validation(
339 format!(
340 "Type '{}' has a unit with empty name. Unit names cannot be empty.",
341 type_name
342 ),
343 Some(source.clone()),
344 None::<String>,
345 ));
346 }
347
348 let lower_name = unit.name.to_lowercase();
350 if seen_names
351 .iter()
352 .any(|seen| seen.to_lowercase() == lower_name)
353 {
354 errors.push(Error::validation(
355 format!("Type '{}' has duplicate unit name '{}' (case-insensitive). Unit names must be unique within a type.", type_name, unit.name),
356 Some(source.clone()),
357 None::<String>,
358 ));
359 } else {
360 seen_names.push(unit.name.clone());
361 }
362
363 if unit.value <= Decimal::ZERO {
365 errors.push(Error::validation(
366 format!("Type '{}' has unit '{}' with invalid value {}. Unit values must be positive (conversion factor relative to type base).", type_name, unit.name, unit.value),
367 Some(source.clone()),
368 None::<String>,
369 ));
370 }
371 }
372 }
373 }
374
375 TypeSpecification::Text {
376 minimum,
377 maximum,
378 length,
379 options,
380 default,
381 ..
382 } => {
383 if let (Some(min), Some(max)) = (minimum, maximum) {
385 if min > max {
386 errors.push(Error::validation(
387 format!("Type '{}' has invalid range: minimum length {} is greater than maximum length {}", type_name, min, max),
388 Some(source.clone()),
389 None::<String>,
390 ));
391 }
392 }
393
394 if let Some(len) = length {
396 if let Some(min) = minimum {
397 if *len < *min {
398 errors.push(Error::validation(
399 format!("Type '{}' has inconsistent length constraint: length {} is less than minimum {}", type_name, len, min),
400 Some(source.clone()),
401 None::<String>,
402 ));
403 }
404 }
405 if let Some(max) = maximum {
406 if *len > *max {
407 errors.push(Error::validation(
408 format!("Type '{}' has inconsistent length constraint: length {} is greater than maximum {}", type_name, len, max),
409 Some(source.clone()),
410 None::<String>,
411 ));
412 }
413 }
414 }
415
416 if let Some(def) = default {
418 let def_len = def.len();
419
420 if let Some(min) = minimum {
421 if def_len < *min {
422 errors.push(Error::validation(
423 format!(
424 "Type '{}' default value length {} is less than minimum {}",
425 type_name, def_len, min
426 ),
427 Some(source.clone()),
428 None::<String>,
429 ));
430 }
431 }
432 if let Some(max) = maximum {
433 if def_len > *max {
434 errors.push(Error::validation(
435 format!(
436 "Type '{}' default value length {} is greater than maximum {}",
437 type_name, def_len, max
438 ),
439 Some(source.clone()),
440 None::<String>,
441 ));
442 }
443 }
444 if let Some(len) = length {
445 if def_len != *len {
446 errors.push(Error::validation(
447 format!("Type '{}' default value length {} does not match required length {}", type_name, def_len, len),
448 Some(source.clone()),
449 None::<String>,
450 ));
451 }
452 }
453 if !options.is_empty() && !options.contains(def) {
454 errors.push(Error::validation(
455 format!(
456 "Type '{}' default value '{}' is not in allowed options: {:?}",
457 type_name, def, options
458 ),
459 Some(source.clone()),
460 None::<String>,
461 ));
462 }
463 }
464 }
465
466 TypeSpecification::Date {
467 minimum,
468 maximum,
469 default,
470 ..
471 } => {
472 if let (Some(min), Some(max)) = (minimum, maximum) {
474 if compare_date_values(min, max) == Ordering::Greater {
475 errors.push(Error::validation(
476 format!(
477 "Type '{}' has invalid date range: minimum {} is after maximum {}",
478 type_name, min, max
479 ),
480 Some(source.clone()),
481 None::<String>,
482 ));
483 }
484 }
485
486 if let Some(def) = default {
488 if let Some(min) = minimum {
489 if compare_date_values(def, min) == Ordering::Less {
490 errors.push(Error::validation(
491 format!(
492 "Type '{}' default date {} is before minimum {}",
493 type_name, def, min
494 ),
495 Some(source.clone()),
496 None::<String>,
497 ));
498 }
499 }
500 if let Some(max) = maximum {
501 if compare_date_values(def, max) == Ordering::Greater {
502 errors.push(Error::validation(
503 format!(
504 "Type '{}' default date {} is after maximum {}",
505 type_name, def, max
506 ),
507 Some(source.clone()),
508 None::<String>,
509 ));
510 }
511 }
512 }
513 }
514
515 TypeSpecification::Time {
516 minimum,
517 maximum,
518 default,
519 ..
520 } => {
521 if let (Some(min), Some(max)) = (minimum, maximum) {
523 if compare_time_values(min, max) == Ordering::Greater {
524 errors.push(Error::validation(
525 format!(
526 "Type '{}' has invalid time range: minimum {} is after maximum {}",
527 type_name, min, max
528 ),
529 Some(source.clone()),
530 None::<String>,
531 ));
532 }
533 }
534
535 if let Some(def) = default {
537 if let Some(min) = minimum {
538 if compare_time_values(def, min) == Ordering::Less {
539 errors.push(Error::validation(
540 format!(
541 "Type '{}' default time {} is before minimum {}",
542 type_name, def, min
543 ),
544 Some(source.clone()),
545 None::<String>,
546 ));
547 }
548 }
549 if let Some(max) = maximum {
550 if compare_time_values(def, max) == Ordering::Greater {
551 errors.push(Error::validation(
552 format!(
553 "Type '{}' default time {} is after maximum {}",
554 type_name, def, max
555 ),
556 Some(source.clone()),
557 None::<String>,
558 ));
559 }
560 }
561 }
562 }
563
564 TypeSpecification::Boolean { .. } | TypeSpecification::Duration { .. } => {
565 }
567 TypeSpecification::Veto { .. } => {
568 }
571 TypeSpecification::Undetermined => unreachable!(
572 "BUG: validate_type_specification_constraints called with Undetermined sentinel type; this type exists only during type inference"
573 ),
574 }
575
576 errors
577}
578
579fn compare_date_values(left: &DateTimeValue, right: &DateTimeValue) -> Ordering {
581 left.year
583 .cmp(&right.year)
584 .then_with(|| left.month.cmp(&right.month))
585 .then_with(|| left.day.cmp(&right.day))
586 .then_with(|| left.hour.cmp(&right.hour))
587 .then_with(|| left.minute.cmp(&right.minute))
588 .then_with(|| left.second.cmp(&right.second))
589}
590
591fn compare_time_values(left: &TimeValue, right: &TimeValue) -> Ordering {
593 left.hour
595 .cmp(&right.hour)
596 .then_with(|| left.minute.cmp(&right.minute))
597 .then_with(|| left.second.cmp(&right.second))
598}
599
600pub struct RuleEntryForBindingCheck {
606 pub rule_type: LemmaType,
607 pub depends_on_rules: std::collections::BTreeSet<RulePath>,
608 pub branches: Vec<(Option<Expression>, Expression)>,
609}
610
611#[derive(Clone, Copy, Debug)]
613enum ExpectedRuleTypeConstraint {
614 Numeric,
615 Boolean,
616 Comparable,
617 Number,
618 Duration,
619 Ratio,
620 Scale,
621 Any,
622}
623
624fn lemma_type_to_expected_constraint(lemma_type: &LemmaType) -> ExpectedRuleTypeConstraint {
627 if lemma_type.is_boolean() {
628 return ExpectedRuleTypeConstraint::Boolean;
629 }
630 if lemma_type.is_number() {
631 return ExpectedRuleTypeConstraint::Number;
632 }
633 if lemma_type.is_scale() {
634 return ExpectedRuleTypeConstraint::Scale;
635 }
636 if lemma_type.is_duration() {
637 return ExpectedRuleTypeConstraint::Duration;
638 }
639 if lemma_type.is_ratio() {
640 return ExpectedRuleTypeConstraint::Ratio;
641 }
642 if lemma_type.is_text() || lemma_type.is_date() || lemma_type.is_time() {
643 return ExpectedRuleTypeConstraint::Comparable;
644 }
645 ExpectedRuleTypeConstraint::Any
646}
647
648fn rule_type_satisfies_constraint(
649 lemma_type: &LemmaType,
650 constraint: ExpectedRuleTypeConstraint,
651) -> bool {
652 match constraint {
653 ExpectedRuleTypeConstraint::Any => true,
654 ExpectedRuleTypeConstraint::Boolean => lemma_type.is_boolean(),
655 ExpectedRuleTypeConstraint::Number => lemma_type.is_number(),
656 ExpectedRuleTypeConstraint::Duration => lemma_type.is_duration(),
657 ExpectedRuleTypeConstraint::Ratio => lemma_type.is_ratio(),
658 ExpectedRuleTypeConstraint::Scale => lemma_type.is_scale(),
659 ExpectedRuleTypeConstraint::Numeric => {
660 lemma_type.is_number() || lemma_type.is_scale() || lemma_type.is_ratio()
661 }
662 ExpectedRuleTypeConstraint::Comparable => {
663 lemma_type.is_boolean()
664 || lemma_type.is_text()
665 || lemma_type.is_number()
666 || lemma_type.is_ratio()
667 || lemma_type.is_date()
668 || lemma_type.is_time()
669 || lemma_type.is_scale()
670 || lemma_type.is_duration()
671 }
672 }
673}
674
675fn collect_expected_constraints_for_rule_ref(
676 expr: &Expression,
677 rule_path: &RulePath,
678 expected: ExpectedRuleTypeConstraint,
679) -> Vec<(Option<Source>, ExpectedRuleTypeConstraint)> {
680 let mut out = Vec::new();
681 match &expr.kind {
682 ExpressionKind::RulePath(rp) => {
683 if rp == rule_path {
684 out.push((expr.source_location.clone(), expected));
685 }
686 }
687 ExpressionKind::LogicalAnd(left, right) => {
688 out.extend(collect_expected_constraints_for_rule_ref(
689 left,
690 rule_path,
691 ExpectedRuleTypeConstraint::Boolean,
692 ));
693 out.extend(collect_expected_constraints_for_rule_ref(
694 right,
695 rule_path,
696 ExpectedRuleTypeConstraint::Boolean,
697 ));
698 }
699 ExpressionKind::LogicalNegation(operand, _) => {
700 out.extend(collect_expected_constraints_for_rule_ref(
701 operand,
702 rule_path,
703 ExpectedRuleTypeConstraint::Boolean,
704 ));
705 }
706 ExpressionKind::Comparison(left, _, right) => {
707 out.extend(collect_expected_constraints_for_rule_ref(
708 left,
709 rule_path,
710 ExpectedRuleTypeConstraint::Comparable,
711 ));
712 out.extend(collect_expected_constraints_for_rule_ref(
713 right,
714 rule_path,
715 ExpectedRuleTypeConstraint::Comparable,
716 ));
717 }
718 ExpressionKind::Arithmetic(left, _, right) => {
719 out.extend(collect_expected_constraints_for_rule_ref(
720 left,
721 rule_path,
722 ExpectedRuleTypeConstraint::Numeric,
723 ));
724 out.extend(collect_expected_constraints_for_rule_ref(
725 right,
726 rule_path,
727 ExpectedRuleTypeConstraint::Numeric,
728 ));
729 }
730 ExpressionKind::UnitConversion(source, target) => {
731 let constraint = match target {
732 SemanticConversionTarget::Duration(_) => ExpectedRuleTypeConstraint::Duration,
733 SemanticConversionTarget::ScaleUnit(_) => ExpectedRuleTypeConstraint::Scale,
734 SemanticConversionTarget::RatioUnit(_) => ExpectedRuleTypeConstraint::Ratio,
735 };
736 out.extend(collect_expected_constraints_for_rule_ref(
737 source, rule_path, constraint,
738 ));
739 }
740 ExpressionKind::MathematicalComputation(_, operand) => {
741 out.extend(collect_expected_constraints_for_rule_ref(
742 operand,
743 rule_path,
744 ExpectedRuleTypeConstraint::Number,
745 ));
746 }
747 ExpressionKind::DateRelative(_, date_expr, tolerance) => {
748 out.extend(collect_expected_constraints_for_rule_ref(
749 date_expr,
750 rule_path,
751 ExpectedRuleTypeConstraint::Comparable,
752 ));
753 if let Some(tol) = tolerance {
754 out.extend(collect_expected_constraints_for_rule_ref(
755 tol,
756 rule_path,
757 ExpectedRuleTypeConstraint::Duration,
758 ));
759 }
760 }
761 ExpressionKind::DateCalendar(_, _, date_expr) => {
762 out.extend(collect_expected_constraints_for_rule_ref(
763 date_expr,
764 rule_path,
765 ExpectedRuleTypeConstraint::Comparable,
766 ));
767 }
768 ExpressionKind::Literal(_)
769 | ExpressionKind::FactPath(_)
770 | ExpressionKind::Veto(_)
771 | ExpressionKind::Now => {}
772 }
773 out
774}
775
776fn expected_constraint_name(c: ExpectedRuleTypeConstraint) -> &'static str {
777 match c {
778 ExpectedRuleTypeConstraint::Numeric => "numeric (number, scale, or ratio)",
779 ExpectedRuleTypeConstraint::Boolean => "boolean",
780 ExpectedRuleTypeConstraint::Comparable => "comparable",
781 ExpectedRuleTypeConstraint::Number => "number",
782 ExpectedRuleTypeConstraint::Duration => "duration",
783 ExpectedRuleTypeConstraint::Ratio => "ratio",
784 ExpectedRuleTypeConstraint::Scale => "scale",
785 ExpectedRuleTypeConstraint::Any => "any",
786 }
787}
788
789fn spec_interface_error(
790 source: &Source,
791 message: impl Into<String>,
792 related_spec: Option<Arc<LemmaSpec>>,
793) -> Error {
794 Error::validation_with_context(
795 message.into(),
796 Some(source.clone()),
797 None::<String>,
798 related_spec,
799 )
800}
801
802pub fn validate_spec_interfaces(
806 referenced_rules: &HashMap<Vec<String>, HashSet<String>>,
807 spec_ref_facts: &[(FactPath, Arc<LemmaSpec>, Source)],
808 rule_entries: &IndexMap<RulePath, RuleEntryForBindingCheck>,
809) -> Result<(), Vec<Error>> {
810 let mut errors = Vec::new();
811
812 for (fact_path, spec_arc, fact_source) in spec_ref_facts {
813 let mut full_path: Vec<String> =
814 fact_path.segments.iter().map(|s| s.fact.clone()).collect();
815 full_path.push(fact_path.fact.clone());
816
817 let Some(required_rules) = referenced_rules.get(&full_path) else {
818 continue;
819 };
820
821 let spec = spec_arc.as_ref();
822 let spec_rule_names: HashSet<&str> = spec.rules.iter().map(|r| r.name.as_str()).collect();
823
824 for required_rule in required_rules {
825 if !spec_rule_names.contains(required_rule.as_str()) {
826 errors.push(spec_interface_error(
827 fact_source,
828 format!(
829 "Spec '{}' referenced by '{}' is missing required rule '{}'",
830 spec.name, fact_path, required_rule
831 ),
832 Some(Arc::clone(spec_arc)),
833 ));
834 continue;
835 }
836
837 let ref_rule_path = RulePath::new(fact_path.segments.clone(), required_rule.clone());
838 let Some(ref_entry) = rule_entries.get(&ref_rule_path) else {
839 continue;
840 };
841 let ref_rule_type = &ref_entry.rule_type;
842
843 for (_referencing_path, entry) in rule_entries {
844 if !entry.depends_on_rules.contains(&ref_rule_path) {
845 continue;
846 }
847 let expected = lemma_type_to_expected_constraint(&entry.rule_type);
848 for (_condition, result_expr) in &entry.branches {
849 let constraints = collect_expected_constraints_for_rule_ref(
850 result_expr,
851 &ref_rule_path,
852 expected,
853 );
854 for (_source, constraint) in constraints {
855 if !rule_type_satisfies_constraint(ref_rule_type, constraint) {
856 let report_source = fact_source;
857
858 let binding_path_str = fact_path
859 .segments
860 .iter()
861 .map(|s| s.fact.as_str())
862 .collect::<Vec<_>>()
863 .join(".");
864 let binding_path_str = if binding_path_str.is_empty() {
865 fact_path.fact.clone()
866 } else {
867 format!("{}.{}", binding_path_str, fact_path.fact)
868 };
869
870 errors.push(spec_interface_error(
871 report_source,
872 format!(
873 "Fact binding '{}' sets spec reference to '{}', but that spec's rule '{}' has result type {}; the referencing expression expects a {} value",
874 binding_path_str,
875 spec.name,
876 required_rule,
877 ref_rule_type.name(),
878 expected_constraint_name(constraint),
879 ),
880 Some(Arc::clone(spec_arc)),
881 ));
882 }
883 }
884 }
885 }
886 }
887 }
888
889 if errors.is_empty() {
890 Ok(())
891 } else {
892 Err(errors)
893 }
894}
895
896#[cfg(test)]
897mod tests {
898 use super::*;
899 use crate::parsing::ast::CommandArg;
900 use crate::planning::semantics::TypeSpecification;
901 use rust_decimal::Decimal;
902 use std::sync::Arc;
903
904 fn test_source(spec_name: &str) -> Source {
905 Source::new(
906 "<test>",
907 crate::parsing::ast::Span {
908 start: 0,
909 end: 0,
910 line: 1,
911 col: 0,
912 },
913 spec_name,
914 Arc::from("spec test\nfact x: 1"),
915 )
916 }
917
918 #[test]
919 fn validate_number_minimum_greater_than_maximum() {
920 let mut specs = TypeSpecification::number();
921 specs = specs
922 .apply_constraint("minimum", &[CommandArg::Number("100".to_string())])
923 .unwrap();
924 specs = specs
925 .apply_constraint("maximum", &[CommandArg::Number("50".to_string())])
926 .unwrap();
927
928 let src = test_source("test");
929 let errors = validate_type_specifications(&specs, "test", &src);
930 assert_eq!(errors.len(), 1);
931 assert!(errors[0]
932 .to_string()
933 .contains("minimum 100 is greater than maximum 50"));
934 }
935
936 #[test]
937 fn validate_number_valid_range() {
938 let mut specs = TypeSpecification::number();
939 specs = specs
940 .apply_constraint("minimum", &[CommandArg::Number("0".to_string())])
941 .unwrap();
942 specs = specs
943 .apply_constraint("maximum", &[CommandArg::Number("100".to_string())])
944 .unwrap();
945
946 let src = test_source("test");
947 let errors = validate_type_specifications(&specs, "test", &src);
948 assert!(errors.is_empty());
949 }
950
951 #[test]
952 fn validate_number_default_below_minimum() {
953 let specs = TypeSpecification::Number {
954 minimum: Some(Decimal::from(10)),
955 maximum: None,
956 decimals: None,
957 precision: None,
958 help: String::new(),
959 default: Some(Decimal::from(5)),
960 };
961
962 let src = test_source("test");
963 let errors = validate_type_specifications(&specs, "test", &src);
964 assert_eq!(errors.len(), 1);
965 assert!(errors[0]
966 .to_string()
967 .contains("default value 5 is less than minimum 10"));
968 }
969
970 #[test]
971 fn validate_number_default_above_maximum() {
972 let specs = TypeSpecification::Number {
973 minimum: None,
974 maximum: Some(Decimal::from(100)),
975 decimals: None,
976 precision: None,
977 help: String::new(),
978 default: Some(Decimal::from(150)),
979 };
980
981 let src = test_source("test");
982 let errors = validate_type_specifications(&specs, "test", &src);
983 assert_eq!(errors.len(), 1);
984 assert!(errors[0]
985 .to_string()
986 .contains("default value 150 is greater than maximum 100"));
987 }
988
989 #[test]
990 fn validate_number_default_valid() {
991 let specs = TypeSpecification::Number {
992 minimum: Some(Decimal::from(0)),
993 maximum: Some(Decimal::from(100)),
994 decimals: None,
995 precision: None,
996 help: String::new(),
997 default: Some(Decimal::from(50)),
998 };
999
1000 let src = test_source("test");
1001 let errors = validate_type_specifications(&specs, "test", &src);
1002 assert!(errors.is_empty());
1003 }
1004
1005 #[test]
1006 fn validate_text_minimum_greater_than_maximum() {
1007 let mut specs = TypeSpecification::text();
1008 specs = specs
1009 .apply_constraint("minimum", &[CommandArg::Number("100".to_string())])
1010 .unwrap();
1011 specs = specs
1012 .apply_constraint("maximum", &[CommandArg::Number("50".to_string())])
1013 .unwrap();
1014
1015 let src = test_source("test");
1016 let errors = validate_type_specifications(&specs, "test", &src);
1017 assert_eq!(errors.len(), 1);
1018 assert!(errors[0]
1019 .to_string()
1020 .contains("minimum length 100 is greater than maximum length 50"));
1021 }
1022
1023 #[test]
1024 fn validate_text_length_inconsistent_with_minimum() {
1025 let mut specs = TypeSpecification::text();
1026 specs = specs
1027 .apply_constraint("minimum", &[CommandArg::Number("10".to_string())])
1028 .unwrap();
1029 specs = specs
1030 .apply_constraint("length", &[CommandArg::Number("5".to_string())])
1031 .unwrap();
1032
1033 let src = test_source("test");
1034 let errors = validate_type_specifications(&specs, "test", &src);
1035 assert_eq!(errors.len(), 1);
1036 assert!(errors[0]
1037 .to_string()
1038 .contains("length 5 is less than minimum 10"));
1039 }
1040
1041 #[test]
1042 fn validate_text_default_not_in_options() {
1043 let specs = TypeSpecification::Text {
1044 minimum: None,
1045 maximum: None,
1046 length: None,
1047 options: vec!["red".to_string(), "blue".to_string()],
1048 help: String::new(),
1049 default: Some("green".to_string()),
1050 };
1051
1052 let src = test_source("test");
1053 let errors = validate_type_specifications(&specs, "test", &src);
1054 assert_eq!(errors.len(), 1);
1055 assert!(errors[0]
1056 .to_string()
1057 .contains("default value 'green' is not in allowed options"));
1058 }
1059
1060 #[test]
1061 fn validate_text_default_valid_in_options() {
1062 let specs = TypeSpecification::Text {
1063 minimum: None,
1064 maximum: None,
1065 length: None,
1066 options: vec!["red".to_string(), "blue".to_string()],
1067 help: String::new(),
1068 default: Some("red".to_string()),
1069 };
1070
1071 let src = test_source("test");
1072 let errors = validate_type_specifications(&specs, "test", &src);
1073 assert!(errors.is_empty());
1074 }
1075
1076 #[test]
1077 fn validate_ratio_minimum_greater_than_maximum() {
1078 let specs = TypeSpecification::Ratio {
1079 minimum: Some(Decimal::from(2)),
1080 maximum: Some(Decimal::from(1)),
1081 decimals: None,
1082 units: crate::planning::semantics::RatioUnits::new(),
1083 help: String::new(),
1084 default: None,
1085 };
1086
1087 let src = test_source("test");
1088 let errors = validate_type_specifications(&specs, "test", &src);
1089 assert_eq!(errors.len(), 1);
1090 assert!(errors[0]
1091 .to_string()
1092 .contains("minimum 2 is greater than maximum 1"));
1093 }
1094
1095 #[test]
1096 fn validate_date_minimum_after_maximum() {
1097 let mut specs = TypeSpecification::date();
1098 specs = specs
1099 .apply_constraint("minimum", &[CommandArg::Label("2024-12-31".to_string())])
1100 .unwrap();
1101 specs = specs
1102 .apply_constraint("maximum", &[CommandArg::Label("2024-01-01".to_string())])
1103 .unwrap();
1104
1105 let src = test_source("test");
1106 let errors = validate_type_specifications(&specs, "test", &src);
1107 assert_eq!(errors.len(), 1);
1108 assert!(
1109 errors[0].to_string().contains("minimum")
1110 && errors[0].to_string().contains("is after maximum")
1111 );
1112 }
1113
1114 #[test]
1115 fn validate_date_valid_range() {
1116 let mut specs = TypeSpecification::date();
1117 specs = specs
1118 .apply_constraint("minimum", &[CommandArg::Label("2024-01-01".to_string())])
1119 .unwrap();
1120 specs = specs
1121 .apply_constraint("maximum", &[CommandArg::Label("2024-12-31".to_string())])
1122 .unwrap();
1123
1124 let src = test_source("test");
1125 let errors = validate_type_specifications(&specs, "test", &src);
1126 assert!(errors.is_empty());
1127 }
1128
1129 #[test]
1130 fn validate_time_minimum_after_maximum() {
1131 let mut specs = TypeSpecification::time();
1132 specs = specs
1133 .apply_constraint("minimum", &[CommandArg::Label("23:00:00".to_string())])
1134 .unwrap();
1135 specs = specs
1136 .apply_constraint("maximum", &[CommandArg::Label("10:00:00".to_string())])
1137 .unwrap();
1138
1139 let src = test_source("test");
1140 let errors = validate_type_specifications(&specs, "test", &src);
1141 assert_eq!(errors.len(), 1);
1142 assert!(
1143 errors[0].to_string().contains("minimum")
1144 && errors[0].to_string().contains("is after maximum")
1145 );
1146 }
1147
1148 #[test]
1149 fn validate_type_definition_with_invalid_constraints() {
1150 use crate::parsing::ast::{LemmaSpec, TypeDef};
1154 use crate::planning::types::TypeResolver;
1155 use std::sync::Arc;
1156
1157 let spec = Arc::new(LemmaSpec::new("test".to_string()));
1158 let type_def = TypeDef::Regular {
1159 source_location: crate::Source::new(
1160 "<test>",
1161 crate::parsing::ast::Span {
1162 start: 0,
1163 end: 0,
1164 line: 1,
1165 col: 0,
1166 },
1167 "test",
1168 Arc::from("spec test\nfact x: 1"),
1169 ),
1170 name: "invalid_money".to_string(),
1171 parent: "number".to_string(),
1172 constraints: Some(vec![
1173 (
1174 "minimum".to_string(),
1175 vec![CommandArg::Number("100".to_string())],
1176 ),
1177 (
1178 "maximum".to_string(),
1179 vec![CommandArg::Number("50".to_string())],
1180 ),
1181 ]),
1182 };
1183
1184 let mut sources = HashMap::new();
1186 sources.insert("<test>".to_string(), String::new());
1187 let mut type_resolver = TypeResolver::new();
1188 type_resolver
1189 .register_type(&spec, type_def)
1190 .expect("Should register type");
1191 let resolved_types = type_resolver
1192 .resolve_named_types(&spec)
1193 .expect("Should resolve types");
1194
1195 let lemma_type = resolved_types
1197 .named_types
1198 .get("invalid_money")
1199 .expect("Should have invalid_money type");
1200 let src = test_source("test");
1201 let errors =
1202 validate_type_specifications(&lemma_type.specifications, "invalid_money", &src);
1203 assert!(!errors.is_empty());
1204 assert!(errors.iter().any(|e| e
1205 .to_string()
1206 .contains("minimum 100 is greater than maximum 50")));
1207 }
1208}