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