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() -> 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 Arc::from("spec test\nfact x: 1"),
914 )
915 }
916
917 #[test]
918 fn validate_number_minimum_greater_than_maximum() {
919 let mut specs = TypeSpecification::number();
920 specs = specs
921 .apply_constraint("minimum", &[CommandArg::Number("100".to_string())])
922 .unwrap();
923 specs = specs
924 .apply_constraint("maximum", &[CommandArg::Number("50".to_string())])
925 .unwrap();
926
927 let src = test_source();
928 let errors = validate_type_specifications(&specs, "test", &src);
929 assert_eq!(errors.len(), 1);
930 assert!(errors[0]
931 .to_string()
932 .contains("minimum 100 is greater than maximum 50"));
933 }
934
935 #[test]
936 fn validate_number_valid_range() {
937 let mut specs = TypeSpecification::number();
938 specs = specs
939 .apply_constraint("minimum", &[CommandArg::Number("0".to_string())])
940 .unwrap();
941 specs = specs
942 .apply_constraint("maximum", &[CommandArg::Number("100".to_string())])
943 .unwrap();
944
945 let src = test_source();
946 let errors = validate_type_specifications(&specs, "test", &src);
947 assert!(errors.is_empty());
948 }
949
950 #[test]
951 fn validate_number_default_below_minimum() {
952 let specs = TypeSpecification::Number {
953 minimum: Some(Decimal::from(10)),
954 maximum: None,
955 decimals: None,
956 precision: None,
957 help: String::new(),
958 default: Some(Decimal::from(5)),
959 };
960
961 let src = test_source();
962 let errors = validate_type_specifications(&specs, "test", &src);
963 assert_eq!(errors.len(), 1);
964 assert!(errors[0]
965 .to_string()
966 .contains("default value 5 is less than minimum 10"));
967 }
968
969 #[test]
970 fn validate_number_default_above_maximum() {
971 let specs = TypeSpecification::Number {
972 minimum: None,
973 maximum: Some(Decimal::from(100)),
974 decimals: None,
975 precision: None,
976 help: String::new(),
977 default: Some(Decimal::from(150)),
978 };
979
980 let src = test_source();
981 let errors = validate_type_specifications(&specs, "test", &src);
982 assert_eq!(errors.len(), 1);
983 assert!(errors[0]
984 .to_string()
985 .contains("default value 150 is greater than maximum 100"));
986 }
987
988 #[test]
989 fn validate_number_default_valid() {
990 let specs = TypeSpecification::Number {
991 minimum: Some(Decimal::from(0)),
992 maximum: Some(Decimal::from(100)),
993 decimals: None,
994 precision: None,
995 help: String::new(),
996 default: Some(Decimal::from(50)),
997 };
998
999 let src = test_source();
1000 let errors = validate_type_specifications(&specs, "test", &src);
1001 assert!(errors.is_empty());
1002 }
1003
1004 #[test]
1005 fn validate_text_minimum_greater_than_maximum() {
1006 let mut specs = TypeSpecification::text();
1007 specs = specs
1008 .apply_constraint("minimum", &[CommandArg::Number("100".to_string())])
1009 .unwrap();
1010 specs = specs
1011 .apply_constraint("maximum", &[CommandArg::Number("50".to_string())])
1012 .unwrap();
1013
1014 let src = test_source();
1015 let errors = validate_type_specifications(&specs, "test", &src);
1016 assert_eq!(errors.len(), 1);
1017 assert!(errors[0]
1018 .to_string()
1019 .contains("minimum length 100 is greater than maximum length 50"));
1020 }
1021
1022 #[test]
1023 fn validate_text_length_inconsistent_with_minimum() {
1024 let mut specs = TypeSpecification::text();
1025 specs = specs
1026 .apply_constraint("minimum", &[CommandArg::Number("10".to_string())])
1027 .unwrap();
1028 specs = specs
1029 .apply_constraint("length", &[CommandArg::Number("5".to_string())])
1030 .unwrap();
1031
1032 let src = test_source();
1033 let errors = validate_type_specifications(&specs, "test", &src);
1034 assert_eq!(errors.len(), 1);
1035 assert!(errors[0]
1036 .to_string()
1037 .contains("length 5 is less than minimum 10"));
1038 }
1039
1040 #[test]
1041 fn validate_text_default_not_in_options() {
1042 let specs = TypeSpecification::Text {
1043 minimum: None,
1044 maximum: None,
1045 length: None,
1046 options: vec!["red".to_string(), "blue".to_string()],
1047 help: String::new(),
1048 default: Some("green".to_string()),
1049 };
1050
1051 let src = test_source();
1052 let errors = validate_type_specifications(&specs, "test", &src);
1053 assert_eq!(errors.len(), 1);
1054 assert!(errors[0]
1055 .to_string()
1056 .contains("default value 'green' is not in allowed options"));
1057 }
1058
1059 #[test]
1060 fn validate_text_default_valid_in_options() {
1061 let specs = TypeSpecification::Text {
1062 minimum: None,
1063 maximum: None,
1064 length: None,
1065 options: vec!["red".to_string(), "blue".to_string()],
1066 help: String::new(),
1067 default: Some("red".to_string()),
1068 };
1069
1070 let src = test_source();
1071 let errors = validate_type_specifications(&specs, "test", &src);
1072 assert!(errors.is_empty());
1073 }
1074
1075 #[test]
1076 fn validate_ratio_minimum_greater_than_maximum() {
1077 let specs = TypeSpecification::Ratio {
1078 minimum: Some(Decimal::from(2)),
1079 maximum: Some(Decimal::from(1)),
1080 decimals: None,
1081 units: crate::planning::semantics::RatioUnits::new(),
1082 help: String::new(),
1083 default: None,
1084 };
1085
1086 let src = test_source();
1087 let errors = validate_type_specifications(&specs, "test", &src);
1088 assert_eq!(errors.len(), 1);
1089 assert!(errors[0]
1090 .to_string()
1091 .contains("minimum 2 is greater than maximum 1"));
1092 }
1093
1094 #[test]
1095 fn validate_date_minimum_after_maximum() {
1096 let mut specs = TypeSpecification::date();
1097 specs = specs
1098 .apply_constraint("minimum", &[CommandArg::Label("2024-12-31".to_string())])
1099 .unwrap();
1100 specs = specs
1101 .apply_constraint("maximum", &[CommandArg::Label("2024-01-01".to_string())])
1102 .unwrap();
1103
1104 let src = test_source();
1105 let errors = validate_type_specifications(&specs, "test", &src);
1106 assert_eq!(errors.len(), 1);
1107 assert!(
1108 errors[0].to_string().contains("minimum")
1109 && errors[0].to_string().contains("is after maximum")
1110 );
1111 }
1112
1113 #[test]
1114 fn validate_date_valid_range() {
1115 let mut specs = TypeSpecification::date();
1116 specs = specs
1117 .apply_constraint("minimum", &[CommandArg::Label("2024-01-01".to_string())])
1118 .unwrap();
1119 specs = specs
1120 .apply_constraint("maximum", &[CommandArg::Label("2024-12-31".to_string())])
1121 .unwrap();
1122
1123 let src = test_source();
1124 let errors = validate_type_specifications(&specs, "test", &src);
1125 assert!(errors.is_empty());
1126 }
1127
1128 #[test]
1129 fn validate_time_minimum_after_maximum() {
1130 let mut specs = TypeSpecification::time();
1131 specs = specs
1132 .apply_constraint("minimum", &[CommandArg::Label("23:00:00".to_string())])
1133 .unwrap();
1134 specs = specs
1135 .apply_constraint("maximum", &[CommandArg::Label("10:00:00".to_string())])
1136 .unwrap();
1137
1138 let src = test_source();
1139 let errors = validate_type_specifications(&specs, "test", &src);
1140 assert_eq!(errors.len(), 1);
1141 assert!(
1142 errors[0].to_string().contains("minimum")
1143 && errors[0].to_string().contains("is after maximum")
1144 );
1145 }
1146
1147 #[test]
1148 fn validate_type_definition_with_invalid_constraints() {
1149 use crate::parsing::ast::{LemmaSpec, TypeDef};
1153 use crate::planning::types::TypeResolver;
1154 use std::sync::Arc;
1155
1156 let spec = Arc::new(LemmaSpec::new("test".to_string()));
1157 let type_def = TypeDef::Regular {
1158 source_location: crate::Source::new(
1159 "<test>",
1160 crate::parsing::ast::Span {
1161 start: 0,
1162 end: 0,
1163 line: 1,
1164 col: 0,
1165 },
1166 Arc::from("spec test\nfact x: 1"),
1167 ),
1168 name: "invalid_money".to_string(),
1169 parent: "number".to_string(),
1170 constraints: Some(vec![
1171 (
1172 "minimum".to_string(),
1173 vec![CommandArg::Number("100".to_string())],
1174 ),
1175 (
1176 "maximum".to_string(),
1177 vec![CommandArg::Number("50".to_string())],
1178 ),
1179 ]),
1180 };
1181
1182 let mut sources = HashMap::new();
1184 sources.insert("<test>".to_string(), String::new());
1185 let mut type_resolver = TypeResolver::new();
1186 type_resolver
1187 .register_type(&spec, type_def)
1188 .expect("Should register type");
1189 let resolved_types = type_resolver
1190 .resolve_named_types(&spec)
1191 .expect("Should resolve types");
1192
1193 let lemma_type = resolved_types
1195 .named_types
1196 .get("invalid_money")
1197 .expect("Should have invalid_money type");
1198 let src = test_source();
1199 let errors =
1200 validate_type_specifications(&lemma_type.specifications, "invalid_money", &src);
1201 assert!(!errors.is_empty());
1202 assert!(errors.iter().any(|e| e
1203 .to_string()
1204 .contains("minimum 100 is greater than maximum 50")));
1205 }
1206}