1#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
19pub struct Span {
20 pub start: usize,
21 pub end: usize,
22 pub line: usize,
23 pub col: usize,
24}
25
26pub struct DepthTracker {
28 depth: usize,
29 max_depth: usize,
30}
31
32impl DepthTracker {
33 pub fn with_max_depth(max_depth: usize) -> Self {
34 Self {
35 depth: 0,
36 max_depth,
37 }
38 }
39
40 pub fn push_depth(&mut self) -> Result<(), usize> {
42 self.depth += 1;
43 if self.depth > self.max_depth {
44 return Err(self.depth);
45 }
46 Ok(())
47 }
48
49 pub fn pop_depth(&mut self) {
50 if self.depth > 0 {
51 self.depth -= 1;
52 }
53 }
54
55 pub fn max_depth(&self) -> usize {
56 self.max_depth
57 }
58}
59
60impl Default for DepthTracker {
61 fn default() -> Self {
62 Self {
63 depth: 0,
64 max_depth: 5,
65 }
66 }
67}
68
69use crate::parsing::source::Source;
74use rust_decimal::Decimal;
75use serde::Serialize;
76use std::cmp::Ordering;
77use std::fmt;
78use std::hash::{Hash, Hasher};
79use std::sync::Arc;
80
81pub use crate::literals::{
82 BooleanValue, DateTimeValue, DurationUnit, TimeValue, TimezoneValue, Value,
83};
84
85#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
88pub struct LemmaSpec {
89 pub name: String,
91 pub from_registry: bool,
93 pub effective_from: Option<DateTimeValue>,
94 pub attribute: Option<String>,
95 pub start_line: usize,
96 pub commentary: Option<String>,
97 pub types: Vec<TypeDef>,
98 pub facts: Vec<LemmaFact>,
99 pub rules: Vec<LemmaRule>,
100 pub meta_fields: Vec<MetaField>,
101}
102
103#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
104pub struct MetaField {
105 pub key: String,
106 pub value: MetaValue,
107 pub source_location: Source,
108}
109
110#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
111#[serde(rename_all = "snake_case")]
112pub enum MetaValue {
113 Literal(Value),
114 Unquoted(String),
115}
116
117impl fmt::Display for MetaValue {
118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119 match self {
120 MetaValue::Literal(v) => write!(f, "{}", v),
121 MetaValue::Unquoted(s) => write!(f, "{}", s),
122 }
123 }
124}
125
126impl fmt::Display for MetaField {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 write!(f, "meta {}: {}", self.key, self.value)
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
133pub struct LemmaFact {
134 pub reference: Reference,
135 pub value: FactValue,
136 pub source_location: Source,
137}
138
139#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
145pub struct UnlessClause {
146 pub condition: Expression,
147 pub result: Expression,
148 pub source_location: Source,
149}
150
151#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
153pub struct LemmaRule {
154 pub name: String,
155 pub expression: Expression,
156 pub unless_clauses: Vec<UnlessClause>,
157 pub source_location: Source,
158}
159
160#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
166pub struct Expression {
167 pub kind: ExpressionKind,
168 pub source_location: Option<Source>,
169}
170
171impl Expression {
172 #[must_use]
174 pub fn new(kind: ExpressionKind, source_location: Source) -> Self {
175 Self {
176 kind,
177 source_location: Some(source_location),
178 }
179 }
180
181 pub fn get_source_text(
185 &self,
186 sources: &std::collections::HashMap<String, String>,
187 ) -> Option<String> {
188 let loc = self.source_location.as_ref()?;
189 sources
190 .get(&loc.attribute)
191 .and_then(|source| loc.extract_text(source))
192 }
193}
194
195impl PartialEq for Expression {
197 fn eq(&self, other: &Self) -> bool {
198 self.kind == other.kind
199 }
200}
201
202impl Eq for Expression {}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
206#[serde(rename_all = "snake_case")]
207pub enum DateRelativeKind {
208 InPast,
209 InFuture,
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
214#[serde(rename_all = "snake_case")]
215pub enum DateCalendarKind {
216 Current,
217 Past,
218 Future,
219 NotIn,
220}
221
222#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
224#[serde(rename_all = "snake_case")]
225pub enum CalendarUnit {
226 Year,
227 Month,
228 Week,
229}
230
231impl fmt::Display for DateRelativeKind {
232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233 match self {
234 DateRelativeKind::InPast => write!(f, "in past"),
235 DateRelativeKind::InFuture => write!(f, "in future"),
236 }
237 }
238}
239
240impl fmt::Display for DateCalendarKind {
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 match self {
243 DateCalendarKind::Current => write!(f, "in calendar"),
244 DateCalendarKind::Past => write!(f, "in past calendar"),
245 DateCalendarKind::Future => write!(f, "in future calendar"),
246 DateCalendarKind::NotIn => write!(f, "not in calendar"),
247 }
248 }
249}
250
251impl fmt::Display for CalendarUnit {
252 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253 match self {
254 CalendarUnit::Year => write!(f, "year"),
255 CalendarUnit::Month => write!(f, "month"),
256 CalendarUnit::Week => write!(f, "week"),
257 }
258 }
259}
260
261#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
263#[serde(rename_all = "snake_case")]
264pub enum ExpressionKind {
265 Literal(Value),
267 Reference(Reference),
269 UnresolvedUnitLiteral(Decimal, String),
272 Now,
274 DateRelative(DateRelativeKind, Arc<Expression>, Option<Arc<Expression>>),
277 DateCalendar(DateCalendarKind, CalendarUnit, Arc<Expression>),
280 LogicalAnd(Arc<Expression>, Arc<Expression>),
281 Arithmetic(Arc<Expression>, ArithmeticComputation, Arc<Expression>),
282 Comparison(Arc<Expression>, ComparisonComputation, Arc<Expression>),
283 UnitConversion(Arc<Expression>, ConversionTarget),
284 LogicalNegation(Arc<Expression>, NegationType),
285 MathematicalComputation(MathematicalComputation, Arc<Expression>),
286 Veto(VetoExpression),
287}
288
289#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
299pub struct Reference {
300 pub segments: Vec<String>,
301 pub name: String,
302}
303
304impl Reference {
305 #[must_use]
306 pub fn local(name: String) -> Self {
307 Self {
308 segments: Vec::new(),
309 name,
310 }
311 }
312
313 #[must_use]
314 pub fn from_path(path: Vec<String>) -> Self {
315 if path.is_empty() {
316 Self {
317 segments: Vec::new(),
318 name: String::new(),
319 }
320 } else {
321 let name = path[path.len() - 1].clone();
323 let segments = path[..path.len() - 1].to_vec();
324 Self { segments, name }
325 }
326 }
327
328 #[must_use]
329 pub fn is_local(&self) -> bool {
330 self.segments.is_empty()
331 }
332
333 #[must_use]
334 pub fn full_path(&self) -> Vec<String> {
335 let mut path = self.segments.clone();
336 path.push(self.name.clone());
337 path
338 }
339}
340
341impl fmt::Display for Reference {
342 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343 for segment in &self.segments {
344 write!(f, "{}.", segment)?;
345 }
346 write!(f, "{}", self.name)
347 }
348}
349
350#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
352#[serde(rename_all = "snake_case")]
353pub enum ArithmeticComputation {
354 Add,
355 Subtract,
356 Multiply,
357 Divide,
358 Modulo,
359 Power,
360}
361
362impl ArithmeticComputation {
363 #[must_use]
365 pub fn symbol(&self) -> &'static str {
366 match self {
367 ArithmeticComputation::Add => "+",
368 ArithmeticComputation::Subtract => "-",
369 ArithmeticComputation::Multiply => "*",
370 ArithmeticComputation::Divide => "/",
371 ArithmeticComputation::Modulo => "%",
372 ArithmeticComputation::Power => "^",
373 }
374 }
375}
376
377#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
379#[serde(rename_all = "snake_case")]
380pub enum ComparisonComputation {
381 GreaterThan,
382 LessThan,
383 GreaterThanOrEqual,
384 LessThanOrEqual,
385 Equal,
386 NotEqual,
387 Is,
388 IsNot,
389}
390
391impl ComparisonComputation {
392 #[must_use]
394 pub fn symbol(&self) -> &'static str {
395 match self {
396 ComparisonComputation::GreaterThan => ">",
397 ComparisonComputation::LessThan => "<",
398 ComparisonComputation::GreaterThanOrEqual => ">=",
399 ComparisonComputation::LessThanOrEqual => "<=",
400 ComparisonComputation::Equal => "==",
401 ComparisonComputation::NotEqual => "!=",
402 ComparisonComputation::Is => "is",
403 ComparisonComputation::IsNot => "is not",
404 }
405 }
406
407 #[must_use]
409 pub fn is_equal(&self) -> bool {
410 matches!(
411 self,
412 ComparisonComputation::Equal | ComparisonComputation::Is
413 )
414 }
415
416 #[must_use]
418 pub fn is_not_equal(&self) -> bool {
419 matches!(
420 self,
421 ComparisonComputation::NotEqual | ComparisonComputation::IsNot
422 )
423 }
424}
425
426#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
429#[serde(rename_all = "snake_case")]
430pub enum ConversionTarget {
431 Duration(DurationUnit),
432 Unit(String),
433}
434
435#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
437#[serde(rename_all = "snake_case")]
438pub enum NegationType {
439 Not,
440}
441
442#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
450pub struct VetoExpression {
451 pub message: Option<String>,
452}
453
454#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
456#[serde(rename_all = "snake_case")]
457pub enum MathematicalComputation {
458 Sqrt,
459 Sin,
460 Cos,
461 Tan,
462 Asin,
463 Acos,
464 Atan,
465 Log,
466 Exp,
467 Abs,
468 Floor,
469 Ceil,
470 Round,
471}
472
473#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
479pub struct SpecRef {
480 pub name: String,
482 pub from_registry: bool,
484 pub hash_pin: Option<String>,
486 pub effective: Option<DateTimeValue>,
488}
489
490impl std::fmt::Display for SpecRef {
491 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
492 write!(f, "{}", self.name)?;
493 if let Some(ref h) = self.hash_pin {
494 write!(f, "~{}", h)?;
495 }
496 if let Some(ref d) = self.effective {
497 write!(f, " {}", d)?;
498 }
499 Ok(())
500 }
501}
502
503impl SpecRef {
504 pub fn local(name: impl Into<String>) -> Self {
506 Self {
507 name: name.into(),
508 from_registry: false,
509 hash_pin: None,
510 effective: None,
511 }
512 }
513
514 pub fn registry(name: impl Into<String>) -> Self {
516 Self {
517 name: name.into(),
518 from_registry: true,
519 hash_pin: None,
520 effective: None,
521 }
522 }
523
524 pub fn resolution_key(&self) -> String {
525 self.name.clone()
526 }
527}
528
529#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
538#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
539pub enum CommandArg {
540 Number(String),
542 Boolean(BooleanValue),
544 Text(String),
547 Label(String),
549}
550
551impl CommandArg {
552 pub fn value(&self) -> &str {
557 match self {
558 CommandArg::Number(s) | CommandArg::Text(s) | CommandArg::Label(s) => s.as_str(),
559 CommandArg::Boolean(bv) => bv.as_str(),
560 }
561 }
562}
563
564impl fmt::Display for CommandArg {
565 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
566 write!(f, "{}", self.value())
567 }
568}
569
570#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
572#[serde(rename_all = "snake_case")]
573pub enum TypeConstraintCommand {
574 Help,
575 Default,
576 Unit,
577 Minimum,
578 Maximum,
579 Decimals,
580 Precision,
581 Option,
582 Options,
583 Length,
584}
585
586impl fmt::Display for TypeConstraintCommand {
587 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
588 let s = match self {
589 TypeConstraintCommand::Help => "help",
590 TypeConstraintCommand::Default => "default",
591 TypeConstraintCommand::Unit => "unit",
592 TypeConstraintCommand::Minimum => "minimum",
593 TypeConstraintCommand::Maximum => "maximum",
594 TypeConstraintCommand::Decimals => "decimals",
595 TypeConstraintCommand::Precision => "precision",
596 TypeConstraintCommand::Option => "option",
597 TypeConstraintCommand::Options => "options",
598 TypeConstraintCommand::Length => "length",
599 };
600 write!(f, "{}", s)
601 }
602}
603
604#[must_use]
606pub fn try_parse_type_constraint_command(s: &str) -> Option<TypeConstraintCommand> {
607 match s.trim().to_lowercase().as_str() {
608 "help" => Some(TypeConstraintCommand::Help),
609 "default" => Some(TypeConstraintCommand::Default),
610 "unit" => Some(TypeConstraintCommand::Unit),
611 "minimum" => Some(TypeConstraintCommand::Minimum),
612 "maximum" => Some(TypeConstraintCommand::Maximum),
613 "decimals" => Some(TypeConstraintCommand::Decimals),
614 "precision" => Some(TypeConstraintCommand::Precision),
615 "option" => Some(TypeConstraintCommand::Option),
616 "options" => Some(TypeConstraintCommand::Options),
617 "length" => Some(TypeConstraintCommand::Length),
618 _ => None,
619 }
620}
621
622pub type Constraint = (TypeConstraintCommand, Vec<CommandArg>);
624
625#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
626#[serde(rename_all = "snake_case")]
627pub enum FactValue {
629 Literal(Value),
631 SpecReference(SpecRef),
633 TypeDeclaration {
635 base: ParentType,
636 constraints: Option<Vec<Constraint>>,
637 from: Option<SpecRef>,
638 },
639}
640
641impl fmt::Display for FactValue {
643 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
644 match self {
645 FactValue::Literal(v) => write!(f, "{}", v),
646 FactValue::SpecReference(spec_ref) => {
647 write!(f, "spec {}", spec_ref)
648 }
649 FactValue::TypeDeclaration {
650 base,
651 constraints,
652 from,
653 } => {
654 let base_str = if let Some(from_spec) = from {
655 format!("{} from {}", base, from_spec)
656 } else {
657 format!("{}", base)
658 };
659 if let Some(ref constraints_vec) = constraints {
660 let constraint_str = constraints_vec
661 .iter()
662 .map(|(cmd, args)| {
663 let args_str: Vec<&str> = args.iter().map(|a| a.value()).collect();
664 let joined = args_str.join(" ");
665 if joined.is_empty() {
666 format!("{}", cmd)
667 } else {
668 format!("{} {}", cmd, joined)
669 }
670 })
671 .collect::<Vec<_>>()
672 .join(" -> ");
673 write!(f, "[{} -> {}]", base_str, constraint_str)
674 } else {
675 write!(f, "[{}]", base_str)
676 }
677 }
678 }
679 }
680}
681
682impl LemmaFact {
683 #[must_use]
684 pub fn new(reference: Reference, value: FactValue, source_location: Source) -> Self {
685 Self {
686 reference,
687 value,
688 source_location,
689 }
690 }
691}
692
693impl LemmaSpec {
694 #[must_use]
695 pub fn new(name: String) -> Self {
696 let from_registry = name.starts_with('@');
697 Self {
698 name,
699 from_registry,
700 effective_from: None,
701 attribute: None,
702 start_line: 1,
703 commentary: None,
704 types: Vec::new(),
705 facts: Vec::new(),
706 rules: Vec::new(),
707 meta_fields: Vec::new(),
708 }
709 }
710
711 pub fn effective_from(&self) -> Option<&DateTimeValue> {
713 self.effective_from.as_ref()
714 }
715
716 #[must_use]
717 pub fn with_attribute(mut self, attribute: String) -> Self {
718 self.attribute = Some(attribute);
719 self
720 }
721
722 #[must_use]
723 pub fn with_start_line(mut self, start_line: usize) -> Self {
724 self.start_line = start_line;
725 self
726 }
727
728 #[must_use]
729 pub fn set_commentary(mut self, commentary: String) -> Self {
730 self.commentary = Some(commentary);
731 self
732 }
733
734 #[must_use]
735 pub fn add_fact(mut self, fact: LemmaFact) -> Self {
736 self.facts.push(fact);
737 self
738 }
739
740 #[must_use]
741 pub fn add_rule(mut self, rule: LemmaRule) -> Self {
742 self.rules.push(rule);
743 self
744 }
745
746 #[must_use]
747 pub fn add_type(mut self, type_def: TypeDef) -> Self {
748 self.types.push(type_def);
749 self
750 }
751
752 #[must_use]
753 pub fn add_meta_field(mut self, meta: MetaField) -> Self {
754 self.meta_fields.push(meta);
755 self
756 }
757}
758
759impl PartialEq for LemmaSpec {
760 fn eq(&self, other: &Self) -> bool {
761 self.name == other.name && self.effective_from() == other.effective_from()
762 }
763}
764
765impl Eq for LemmaSpec {}
766
767impl PartialOrd for LemmaSpec {
768 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
769 Some(self.cmp(other))
770 }
771}
772
773impl Ord for LemmaSpec {
774 fn cmp(&self, other: &Self) -> Ordering {
775 (self.name.as_str(), self.effective_from())
776 .cmp(&(other.name.as_str(), other.effective_from()))
777 }
778}
779
780impl Hash for LemmaSpec {
781 fn hash<H: Hasher>(&self, state: &mut H) {
782 self.name.hash(state);
783 match self.effective_from() {
784 Some(d) => d.hash(state),
785 None => 0u8.hash(state),
786 }
787 }
788}
789
790impl fmt::Display for LemmaSpec {
791 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
792 write!(f, "spec {}", self.name)?;
793 if let Some(ref af) = self.effective_from {
794 write!(f, " {}", af)?;
795 }
796 writeln!(f)?;
797
798 if let Some(ref commentary) = self.commentary {
799 writeln!(f, "\"\"\"")?;
800 writeln!(f, "{}", commentary)?;
801 writeln!(f, "\"\"\"")?;
802 }
803
804 let named_types: Vec<_> = self
805 .types
806 .iter()
807 .filter(|t| !matches!(t, TypeDef::Inline { .. }))
808 .collect();
809 if !named_types.is_empty() {
810 writeln!(f)?;
811 for (index, type_def) in named_types.iter().enumerate() {
812 if index > 0 {
813 writeln!(f)?;
814 }
815 write!(f, "{}", type_def)?;
816 writeln!(f)?;
817 }
818 }
819
820 if !self.facts.is_empty() {
821 writeln!(f)?;
822 for fact in &self.facts {
823 write!(f, "{}", fact)?;
824 }
825 }
826
827 if !self.rules.is_empty() {
828 writeln!(f)?;
829 for (index, rule) in self.rules.iter().enumerate() {
830 if index > 0 {
831 writeln!(f)?;
832 }
833 write!(f, "{}", rule)?;
834 }
835 }
836
837 if !self.meta_fields.is_empty() {
838 writeln!(f)?;
839 for meta in &self.meta_fields {
840 writeln!(f, "{}", meta)?;
841 }
842 }
843
844 Ok(())
845 }
846}
847
848impl fmt::Display for LemmaFact {
849 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
850 writeln!(f, "fact {}: {}", self.reference, self.value)
851 }
852}
853
854impl fmt::Display for LemmaRule {
855 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
856 write!(f, "rule {}: {}", self.name, self.expression)?;
857 for unless_clause in &self.unless_clauses {
858 write!(
859 f,
860 "\n unless {} then {}",
861 unless_clause.condition, unless_clause.result
862 )?;
863 }
864 writeln!(f)?;
865 Ok(())
866 }
867}
868
869pub fn expression_precedence(kind: &ExpressionKind) -> u8 {
874 match kind {
875 ExpressionKind::LogicalAnd(..) => 2,
876 ExpressionKind::LogicalNegation(..) => 3,
877 ExpressionKind::Comparison(..) => 4,
878 ExpressionKind::UnitConversion(..) => 4,
879 ExpressionKind::Arithmetic(_, op, _) => match op {
880 ArithmeticComputation::Add | ArithmeticComputation::Subtract => 5,
881 ArithmeticComputation::Multiply
882 | ArithmeticComputation::Divide
883 | ArithmeticComputation::Modulo => 6,
884 ArithmeticComputation::Power => 7,
885 },
886 ExpressionKind::MathematicalComputation(..) => 8,
887 ExpressionKind::DateRelative(..) | ExpressionKind::DateCalendar(..) => 4,
888 ExpressionKind::Literal(..)
889 | ExpressionKind::Reference(..)
890 | ExpressionKind::UnresolvedUnitLiteral(..)
891 | ExpressionKind::Now
892 | ExpressionKind::Veto(..) => 10,
893 }
894}
895
896fn write_expression_child(
897 f: &mut fmt::Formatter<'_>,
898 child: &Expression,
899 parent_prec: u8,
900) -> fmt::Result {
901 let child_prec = expression_precedence(&child.kind);
902 if child_prec < parent_prec {
903 write!(f, "({})", child)
904 } else {
905 write!(f, "{}", child)
906 }
907}
908
909impl fmt::Display for Expression {
910 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
911 match &self.kind {
912 ExpressionKind::Literal(lit) => write!(f, "{}", lit),
913 ExpressionKind::Reference(r) => write!(f, "{}", r),
914 ExpressionKind::Arithmetic(left, op, right) => {
915 let my_prec = expression_precedence(&self.kind);
916 write_expression_child(f, left, my_prec)?;
917 write!(f, " {} ", op)?;
918 write_expression_child(f, right, my_prec)
919 }
920 ExpressionKind::Comparison(left, op, right) => {
921 let my_prec = expression_precedence(&self.kind);
922 write_expression_child(f, left, my_prec)?;
923 write!(f, " {} ", op)?;
924 write_expression_child(f, right, my_prec)
925 }
926 ExpressionKind::UnitConversion(value, target) => {
927 let my_prec = expression_precedence(&self.kind);
928 write_expression_child(f, value, my_prec)?;
929 write!(f, " in {}", target)
930 }
931 ExpressionKind::LogicalNegation(expr, _) => {
932 let my_prec = expression_precedence(&self.kind);
933 write!(f, "not ")?;
934 write_expression_child(f, expr, my_prec)
935 }
936 ExpressionKind::LogicalAnd(left, right) => {
937 let my_prec = expression_precedence(&self.kind);
938 write_expression_child(f, left, my_prec)?;
939 write!(f, " and ")?;
940 write_expression_child(f, right, my_prec)
941 }
942 ExpressionKind::MathematicalComputation(op, operand) => {
943 let my_prec = expression_precedence(&self.kind);
944 write!(f, "{} ", op)?;
945 write_expression_child(f, operand, my_prec)
946 }
947 ExpressionKind::Veto(veto) => match &veto.message {
948 Some(msg) => write!(f, "veto {}", quote_lemma_text(msg)),
949 None => write!(f, "veto"),
950 },
951 ExpressionKind::UnresolvedUnitLiteral(number, unit_name) => {
952 write!(f, "{} {}", format_decimal_source(number), unit_name)
953 }
954 ExpressionKind::Now => write!(f, "now"),
955 ExpressionKind::DateRelative(kind, date_expr, tolerance) => {
956 write!(f, "{} {}", date_expr, kind)?;
957 if let Some(tol) = tolerance {
958 write!(f, " {}", tol)?;
959 }
960 Ok(())
961 }
962 ExpressionKind::DateCalendar(kind, unit, date_expr) => {
963 write!(f, "{} {} {}", date_expr, kind, unit)
964 }
965 }
966 }
967}
968
969impl fmt::Display for ConversionTarget {
970 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
971 match self {
972 ConversionTarget::Duration(unit) => write!(f, "{}", unit),
973 ConversionTarget::Unit(unit) => write!(f, "{}", unit),
974 }
975 }
976}
977
978impl fmt::Display for ArithmeticComputation {
979 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
980 match self {
981 ArithmeticComputation::Add => write!(f, "+"),
982 ArithmeticComputation::Subtract => write!(f, "-"),
983 ArithmeticComputation::Multiply => write!(f, "*"),
984 ArithmeticComputation::Divide => write!(f, "/"),
985 ArithmeticComputation::Modulo => write!(f, "%"),
986 ArithmeticComputation::Power => write!(f, "^"),
987 }
988 }
989}
990
991impl fmt::Display for ComparisonComputation {
992 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
993 match self {
994 ComparisonComputation::GreaterThan => write!(f, ">"),
995 ComparisonComputation::LessThan => write!(f, "<"),
996 ComparisonComputation::GreaterThanOrEqual => write!(f, ">="),
997 ComparisonComputation::LessThanOrEqual => write!(f, "<="),
998 ComparisonComputation::Equal => write!(f, "=="),
999 ComparisonComputation::NotEqual => write!(f, "!="),
1000 ComparisonComputation::Is => write!(f, "is"),
1001 ComparisonComputation::IsNot => write!(f, "is not"),
1002 }
1003 }
1004}
1005
1006impl fmt::Display for MathematicalComputation {
1007 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1008 match self {
1009 MathematicalComputation::Sqrt => write!(f, "sqrt"),
1010 MathematicalComputation::Sin => write!(f, "sin"),
1011 MathematicalComputation::Cos => write!(f, "cos"),
1012 MathematicalComputation::Tan => write!(f, "tan"),
1013 MathematicalComputation::Asin => write!(f, "asin"),
1014 MathematicalComputation::Acos => write!(f, "acos"),
1015 MathematicalComputation::Atan => write!(f, "atan"),
1016 MathematicalComputation::Log => write!(f, "log"),
1017 MathematicalComputation::Exp => write!(f, "exp"),
1018 MathematicalComputation::Abs => write!(f, "abs"),
1019 MathematicalComputation::Floor => write!(f, "floor"),
1020 MathematicalComputation::Ceil => write!(f, "ceil"),
1021 MathematicalComputation::Round => write!(f, "round"),
1022 }
1023 }
1024}
1025
1026#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1032#[serde(rename_all = "snake_case")]
1033pub enum PrimitiveKind {
1034 Boolean,
1035 Scale,
1036 Number,
1037 Percent,
1038 Ratio,
1039 Text,
1040 Date,
1041 Time,
1042 Duration,
1043}
1044
1045impl std::fmt::Display for PrimitiveKind {
1046 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1047 let s = match self {
1048 PrimitiveKind::Boolean => "boolean",
1049 PrimitiveKind::Scale => "scale",
1050 PrimitiveKind::Number => "number",
1051 PrimitiveKind::Percent => "percent",
1052 PrimitiveKind::Ratio => "ratio",
1053 PrimitiveKind::Text => "text",
1054 PrimitiveKind::Date => "date",
1055 PrimitiveKind::Time => "time",
1056 PrimitiveKind::Duration => "duration",
1057 };
1058 write!(f, "{}", s)
1059 }
1060}
1061
1062#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1064#[serde(tag = "kind", rename_all = "snake_case")]
1065pub enum ParentType {
1066 Primitive(PrimitiveKind),
1067 Custom(String),
1068}
1069
1070impl std::fmt::Display for ParentType {
1071 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1072 match self {
1073 ParentType::Primitive(k) => write!(f, "{}", k),
1074 ParentType::Custom(name) => write!(f, "{}", name),
1075 }
1076 }
1077}
1078
1079#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1086#[serde(tag = "kind", rename_all = "snake_case")]
1087pub enum TypeDef {
1088 Regular {
1089 source_location: Source,
1090 name: String,
1091 parent: ParentType,
1092 constraints: Option<Vec<Constraint>>,
1093 },
1094 Import {
1095 source_location: Source,
1096 name: String,
1097 source_type: String,
1098 from: SpecRef,
1099 constraints: Option<Vec<Constraint>>,
1100 },
1101 Inline {
1102 source_location: Source,
1103 parent: ParentType,
1104 constraints: Option<Vec<Constraint>>,
1105 fact_ref: Reference,
1106 from: Option<SpecRef>,
1107 },
1108}
1109
1110impl TypeDef {
1111 pub fn source_location(&self) -> &Source {
1112 match self {
1113 TypeDef::Regular {
1114 source_location, ..
1115 }
1116 | TypeDef::Import {
1117 source_location, ..
1118 }
1119 | TypeDef::Inline {
1120 source_location, ..
1121 } => source_location,
1122 }
1123 }
1124
1125 pub fn name(&self) -> String {
1126 match self {
1127 TypeDef::Regular { name, .. } | TypeDef::Import { name, .. } => name.clone(),
1128 TypeDef::Inline { parent, .. } => format!("{}", parent),
1129 }
1130 }
1131}
1132
1133impl fmt::Display for TypeDef {
1134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1135 match self {
1136 TypeDef::Regular {
1137 name,
1138 parent,
1139 constraints,
1140 ..
1141 } => {
1142 write!(f, "type {}: {}", name, parent)?;
1143 if let Some(constraints) = constraints {
1144 for (cmd, args) in constraints {
1145 write!(f, "\n -> {}", cmd)?;
1146 for arg in args {
1147 write!(f, " {}", arg.value())?;
1148 }
1149 }
1150 }
1151 Ok(())
1152 }
1153 TypeDef::Import {
1154 name,
1155 from,
1156 constraints,
1157 ..
1158 } => {
1159 write!(f, "type {} from {}", name, from)?;
1160 if let Some(constraints) = constraints {
1161 for (cmd, args) in constraints {
1162 write!(f, " -> {}", cmd)?;
1163 for arg in args {
1164 write!(f, " {}", arg.value())?;
1165 }
1166 }
1167 }
1168 Ok(())
1169 }
1170 TypeDef::Inline { .. } => Ok(()),
1171 }
1172 }
1173}
1174
1175pub struct AsLemmaSource<'a, T: ?Sized>(pub &'a T);
1190
1191pub fn quote_lemma_text(s: &str) -> String {
1194 let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
1195 format!("\"{}\"", escaped)
1196}
1197
1198fn format_decimal_source(n: &Decimal) -> String {
1203 let norm = n.normalize();
1204 let raw = if norm.fract().is_zero() {
1205 norm.trunc().to_string()
1206 } else {
1207 norm.to_string()
1208 };
1209 group_digits(&raw)
1210}
1211
1212fn group_digits(s: &str) -> String {
1216 let (sign, rest) = if s.starts_with('-') || s.starts_with('+') {
1217 (&s[..1], &s[1..])
1218 } else {
1219 ("", s)
1220 };
1221
1222 let (int_part, frac_part) = match rest.find('.') {
1223 Some(pos) => (&rest[..pos], &rest[pos..]),
1224 None => (rest, ""),
1225 };
1226
1227 if int_part.len() < 4 {
1228 return s.to_string();
1229 }
1230
1231 let mut grouped = String::with_capacity(int_part.len() + int_part.len() / 3);
1232 for (i, ch) in int_part.chars().enumerate() {
1233 let digits_remaining = int_part.len() - i;
1234 if i > 0 && digits_remaining % 3 == 0 {
1235 grouped.push('_');
1236 }
1237 grouped.push(ch);
1238 }
1239
1240 format!("{}{}{}", sign, grouped, frac_part)
1241}
1242
1243impl<'a> fmt::Display for AsLemmaSource<'a, CommandArg> {
1246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1247 match self.0 {
1248 CommandArg::Text(s) => write!(f, "{}", quote_lemma_text(s)),
1249 CommandArg::Number(s) => {
1250 let clean: String = s.chars().filter(|c| *c != '_' && *c != ',').collect();
1251 write!(f, "{}", group_digits(&clean))
1252 }
1253 CommandArg::Boolean(bv) => write!(f, "{}", bv),
1254 CommandArg::Label(s) => write!(f, "{}", s),
1255 }
1256 }
1257}
1258
1259fn format_constraint_as_source(cmd: &TypeConstraintCommand, args: &[CommandArg]) -> String {
1264 if args.is_empty() {
1265 cmd.to_string()
1266 } else {
1267 let args_str: Vec<String> = args
1268 .iter()
1269 .map(|a| format!("{}", AsLemmaSource(a)))
1270 .collect();
1271 format!("{} {}", cmd, args_str.join(" "))
1272 }
1273}
1274
1275fn format_constraints_as_source(constraints: &[Constraint], separator: &str) -> String {
1278 constraints
1279 .iter()
1280 .map(|(cmd, args)| format_constraint_as_source(cmd, args))
1281 .collect::<Vec<_>>()
1282 .join(separator)
1283}
1284
1285impl<'a> fmt::Display for AsLemmaSource<'a, FactValue> {
1288 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1289 match self.0 {
1290 FactValue::Literal(v) => write!(f, "{}", AsLemmaSource(v)),
1291 FactValue::SpecReference(spec_ref) => {
1292 write!(f, "spec {}", spec_ref)
1293 }
1294 FactValue::TypeDeclaration {
1295 base,
1296 constraints,
1297 from,
1298 } => {
1299 let base_str = if let Some(from_spec) = from {
1300 format!("{} from {}", base, from_spec)
1301 } else {
1302 format!("{}", base)
1303 };
1304 if let Some(ref constraints_vec) = constraints {
1305 let constraint_str = format_constraints_as_source(constraints_vec, " -> ");
1306 write!(f, "[{} -> {}]", base_str, constraint_str)
1307 } else {
1308 write!(f, "[{}]", base_str)
1309 }
1310 }
1311 }
1312 }
1313}
1314
1315impl<'a> fmt::Display for AsLemmaSource<'a, Value> {
1318 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1319 match self.0 {
1320 Value::Number(n) => write!(f, "{}", format_decimal_source(n)),
1321 Value::Text(s) => write!(f, "{}", quote_lemma_text(s)),
1322 Value::Date(dt) => {
1323 let is_date_only =
1324 dt.hour == 0 && dt.minute == 0 && dt.second == 0 && dt.timezone.is_none();
1325 if is_date_only {
1326 write!(f, "{:04}-{:02}-{:02}", dt.year, dt.month, dt.day)
1327 } else {
1328 write!(
1329 f,
1330 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
1331 dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second
1332 )?;
1333 if let Some(tz) = &dt.timezone {
1334 write!(f, "{}", tz)?;
1335 }
1336 Ok(())
1337 }
1338 }
1339 Value::Time(t) => {
1340 write!(f, "{:02}:{:02}:{:02}", t.hour, t.minute, t.second)?;
1341 if let Some(tz) = &t.timezone {
1342 write!(f, "{}", tz)?;
1343 }
1344 Ok(())
1345 }
1346 Value::Boolean(b) => write!(f, "{}", b),
1347 Value::Scale(n, u) => write!(f, "{} {}", format_decimal_source(n), u),
1348 Value::Duration(n, u) => write!(f, "{} {}", format_decimal_source(n), u),
1349 Value::Ratio(n, unit) => match unit.as_deref() {
1350 Some("percent") => {
1351 let display_value = *n * Decimal::from(100);
1352 write!(f, "{}%", format_decimal_source(&display_value))
1353 }
1354 Some("permille") => {
1355 let display_value = *n * Decimal::from(1000);
1356 write!(f, "{}%%", format_decimal_source(&display_value))
1357 }
1358 Some(unit_name) => write!(f, "{} {}", format_decimal_source(n), unit_name),
1359 None => write!(f, "{}", format_decimal_source(n)),
1360 },
1361 }
1362 }
1363}
1364
1365impl<'a> fmt::Display for AsLemmaSource<'a, MetaValue> {
1368 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1369 match self.0 {
1370 MetaValue::Literal(v) => write!(f, "{}", AsLemmaSource(v)),
1371 MetaValue::Unquoted(s) => write!(f, "{}", s),
1372 }
1373 }
1374}
1375
1376impl<'a> fmt::Display for AsLemmaSource<'a, TypeDef> {
1379 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1380 match self.0 {
1381 TypeDef::Regular {
1382 name,
1383 parent,
1384 constraints,
1385 ..
1386 } => {
1387 write!(f, "type {}: {}", name, parent)?;
1388 if let Some(constraints) = constraints {
1389 for (cmd, args) in constraints {
1390 write!(f, "\n -> {}", format_constraint_as_source(cmd, args))?;
1391 }
1392 }
1393 Ok(())
1394 }
1395 TypeDef::Import {
1396 name,
1397 from,
1398 constraints,
1399 ..
1400 } => {
1401 write!(f, "type {} from {}", name, from)?;
1402 if let Some(constraints) = constraints {
1403 for (cmd, args) in constraints {
1404 write!(f, " -> {}", format_constraint_as_source(cmd, args))?;
1405 }
1406 }
1407 Ok(())
1408 }
1409 TypeDef::Inline { .. } => Ok(()),
1410 }
1411 }
1412}
1413
1414#[cfg(test)]
1415mod tests {
1416 use super::*;
1417
1418 #[test]
1419 fn test_duration_unit_display() {
1420 assert_eq!(format!("{}", DurationUnit::Second), "seconds");
1421 assert_eq!(format!("{}", DurationUnit::Minute), "minutes");
1422 assert_eq!(format!("{}", DurationUnit::Hour), "hours");
1423 assert_eq!(format!("{}", DurationUnit::Day), "days");
1424 assert_eq!(format!("{}", DurationUnit::Week), "weeks");
1425 assert_eq!(format!("{}", DurationUnit::Millisecond), "milliseconds");
1426 assert_eq!(format!("{}", DurationUnit::Microsecond), "microseconds");
1427 }
1428
1429 #[test]
1430 fn test_conversion_target_display() {
1431 assert_eq!(
1432 format!("{}", ConversionTarget::Duration(DurationUnit::Hour)),
1433 "hours"
1434 );
1435 assert_eq!(
1436 format!("{}", ConversionTarget::Unit("usd".to_string())),
1437 "usd"
1438 );
1439 }
1440
1441 #[test]
1442 fn test_value_ratio_display() {
1443 use rust_decimal::Decimal;
1444 use std::str::FromStr;
1445 let percent = Value::Ratio(
1446 Decimal::from_str("0.10").unwrap(),
1447 Some("percent".to_string()),
1448 );
1449 assert_eq!(format!("{}", percent), "10%");
1450 let permille = Value::Ratio(
1451 Decimal::from_str("0.005").unwrap(),
1452 Some("permille".to_string()),
1453 );
1454 assert_eq!(format!("{}", permille), "5%%");
1455 }
1456
1457 #[test]
1458 fn test_datetime_value_display() {
1459 let dt = DateTimeValue {
1460 year: 2024,
1461 month: 12,
1462 day: 25,
1463 hour: 14,
1464 minute: 30,
1465 second: 45,
1466 microsecond: 0,
1467 timezone: Some(TimezoneValue {
1468 offset_hours: 1,
1469 offset_minutes: 0,
1470 }),
1471 };
1472 assert_eq!(format!("{}", dt), "2024-12-25T14:30:45+01:00");
1473 }
1474
1475 #[test]
1476 fn test_datetime_value_display_date_only() {
1477 let dt = DateTimeValue {
1478 year: 2026,
1479 month: 3,
1480 day: 4,
1481 hour: 0,
1482 minute: 0,
1483 second: 0,
1484 microsecond: 0,
1485 timezone: None,
1486 };
1487 assert_eq!(format!("{}", dt), "2026-03-04");
1488 }
1489
1490 #[test]
1491 fn test_datetime_value_display_microseconds() {
1492 let dt = DateTimeValue {
1493 year: 2026,
1494 month: 2,
1495 day: 23,
1496 hour: 14,
1497 minute: 30,
1498 second: 45,
1499 microsecond: 123456,
1500 timezone: Some(TimezoneValue {
1501 offset_hours: 0,
1502 offset_minutes: 0,
1503 }),
1504 };
1505 assert_eq!(format!("{}", dt), "2026-02-23T14:30:45.123456Z");
1506 }
1507
1508 #[test]
1509 fn test_datetime_microsecond_in_ordering() {
1510 let a = DateTimeValue {
1511 year: 2026,
1512 month: 1,
1513 day: 1,
1514 hour: 0,
1515 minute: 0,
1516 second: 0,
1517 microsecond: 100,
1518 timezone: None,
1519 };
1520 let b = DateTimeValue {
1521 year: 2026,
1522 month: 1,
1523 day: 1,
1524 hour: 0,
1525 minute: 0,
1526 second: 0,
1527 microsecond: 200,
1528 timezone: None,
1529 };
1530 assert!(a < b);
1531 }
1532
1533 #[test]
1534 fn test_datetime_parse_iso_week() {
1535 let dt: DateTimeValue = "2026-W01".parse().unwrap();
1536 assert_eq!(dt.year, 2025);
1537 assert_eq!(dt.month, 12);
1538 assert_eq!(dt.day, 29);
1539 assert_eq!(dt.microsecond, 0);
1540 }
1541
1542 #[test]
1543 fn test_time_value_display() {
1544 let time = TimeValue {
1545 hour: 14,
1546 minute: 30,
1547 second: 45,
1548 timezone: Some(TimezoneValue {
1549 offset_hours: -5,
1550 offset_minutes: 30,
1551 }),
1552 };
1553 let display = format!("{}", time);
1554 assert!(display.contains("14"));
1555 assert!(display.contains("30"));
1556 assert!(display.contains("45"));
1557 }
1558
1559 #[test]
1560 fn test_timezone_value() {
1561 let tz_positive = TimezoneValue {
1562 offset_hours: 5,
1563 offset_minutes: 30,
1564 };
1565 assert_eq!(tz_positive.offset_hours, 5);
1566 assert_eq!(tz_positive.offset_minutes, 30);
1567
1568 let tz_negative = TimezoneValue {
1569 offset_hours: -8,
1570 offset_minutes: 0,
1571 };
1572 assert_eq!(tz_negative.offset_hours, -8);
1573 }
1574
1575 #[test]
1576 fn test_negation_types() {
1577 let json = serde_json::to_string(&NegationType::Not).expect("serialize NegationType");
1578 let decoded: NegationType = serde_json::from_str(&json).expect("deserialize NegationType");
1579 assert_eq!(decoded, NegationType::Not);
1580 }
1581
1582 #[test]
1583 fn test_veto_expression() {
1584 let veto_with_message = VetoExpression {
1585 message: Some("Must be over 18".to_string()),
1586 };
1587 assert_eq!(
1588 veto_with_message.message,
1589 Some("Must be over 18".to_string())
1590 );
1591
1592 let veto_without_message = VetoExpression { message: None };
1593 assert!(veto_without_message.message.is_none());
1594 }
1595
1596 #[test]
1605 fn as_lemma_source_text_default_is_quoted() {
1606 let fv = FactValue::TypeDeclaration {
1607 base: ParentType::Primitive(PrimitiveKind::Text),
1608 constraints: Some(vec![(
1609 TypeConstraintCommand::Default,
1610 vec![CommandArg::Text("single".to_string())],
1611 )]),
1612 from: None,
1613 };
1614 assert_eq!(
1615 format!("{}", AsLemmaSource(&fv)),
1616 "[text -> default \"single\"]"
1617 );
1618 }
1619
1620 #[test]
1621 fn as_lemma_source_number_default_not_quoted() {
1622 let fv = FactValue::TypeDeclaration {
1623 base: ParentType::Primitive(PrimitiveKind::Number),
1624 constraints: Some(vec![(
1625 TypeConstraintCommand::Default,
1626 vec![CommandArg::Number("10".to_string())],
1627 )]),
1628 from: None,
1629 };
1630 assert_eq!(format!("{}", AsLemmaSource(&fv)), "[number -> default 10]");
1631 }
1632
1633 #[test]
1634 fn as_lemma_source_help_always_quoted() {
1635 let fv = FactValue::TypeDeclaration {
1636 base: ParentType::Primitive(PrimitiveKind::Number),
1637 constraints: Some(vec![(
1638 TypeConstraintCommand::Help,
1639 vec![CommandArg::Text("Enter a quantity".to_string())],
1640 )]),
1641 from: None,
1642 };
1643 assert_eq!(
1644 format!("{}", AsLemmaSource(&fv)),
1645 "[number -> help \"Enter a quantity\"]"
1646 );
1647 }
1648
1649 #[test]
1650 fn as_lemma_source_text_option_quoted() {
1651 let fv = FactValue::TypeDeclaration {
1652 base: ParentType::Primitive(PrimitiveKind::Text),
1653 constraints: Some(vec![
1654 (
1655 TypeConstraintCommand::Option,
1656 vec![CommandArg::Text("active".to_string())],
1657 ),
1658 (
1659 TypeConstraintCommand::Option,
1660 vec![CommandArg::Text("inactive".to_string())],
1661 ),
1662 ]),
1663 from: None,
1664 };
1665 assert_eq!(
1666 format!("{}", AsLemmaSource(&fv)),
1667 "[text -> option \"active\" -> option \"inactive\"]"
1668 );
1669 }
1670
1671 #[test]
1672 fn as_lemma_source_scale_unit_not_quoted() {
1673 let fv = FactValue::TypeDeclaration {
1674 base: ParentType::Primitive(PrimitiveKind::Scale),
1675 constraints: Some(vec![
1676 (
1677 TypeConstraintCommand::Unit,
1678 vec![
1679 CommandArg::Label("eur".to_string()),
1680 CommandArg::Number("1.00".to_string()),
1681 ],
1682 ),
1683 (
1684 TypeConstraintCommand::Unit,
1685 vec![
1686 CommandArg::Label("usd".to_string()),
1687 CommandArg::Number("1.10".to_string()),
1688 ],
1689 ),
1690 ]),
1691 from: None,
1692 };
1693 assert_eq!(
1694 format!("{}", AsLemmaSource(&fv)),
1695 "[scale -> unit eur 1.00 -> unit usd 1.10]"
1696 );
1697 }
1698
1699 #[test]
1700 fn as_lemma_source_scale_minimum_with_unit() {
1701 let fv = FactValue::TypeDeclaration {
1702 base: ParentType::Primitive(PrimitiveKind::Scale),
1703 constraints: Some(vec![(
1704 TypeConstraintCommand::Minimum,
1705 vec![
1706 CommandArg::Number("0".to_string()),
1707 CommandArg::Label("eur".to_string()),
1708 ],
1709 )]),
1710 from: None,
1711 };
1712 assert_eq!(
1713 format!("{}", AsLemmaSource(&fv)),
1714 "[scale -> minimum 0 eur]"
1715 );
1716 }
1717
1718 #[test]
1719 fn as_lemma_source_boolean_default() {
1720 let fv = FactValue::TypeDeclaration {
1721 base: ParentType::Primitive(PrimitiveKind::Boolean),
1722 constraints: Some(vec![(
1723 TypeConstraintCommand::Default,
1724 vec![CommandArg::Boolean(BooleanValue::True)],
1725 )]),
1726 from: None,
1727 };
1728 assert_eq!(
1729 format!("{}", AsLemmaSource(&fv)),
1730 "[boolean -> default true]"
1731 );
1732 }
1733
1734 #[test]
1735 fn as_lemma_source_duration_default() {
1736 let fv = FactValue::TypeDeclaration {
1737 base: ParentType::Primitive(PrimitiveKind::Duration),
1738 constraints: Some(vec![(
1739 TypeConstraintCommand::Default,
1740 vec![
1741 CommandArg::Number("40".to_string()),
1742 CommandArg::Label("hours".to_string()),
1743 ],
1744 )]),
1745 from: None,
1746 };
1747 assert_eq!(
1748 format!("{}", AsLemmaSource(&fv)),
1749 "[duration -> default 40 hours]"
1750 );
1751 }
1752
1753 #[test]
1754 fn as_lemma_source_named_type_default_quoted() {
1755 let fv = FactValue::TypeDeclaration {
1758 base: ParentType::Custom("filing_status_type".to_string()),
1759 constraints: Some(vec![(
1760 TypeConstraintCommand::Default,
1761 vec![CommandArg::Text("single".to_string())],
1762 )]),
1763 from: None,
1764 };
1765 assert_eq!(
1766 format!("{}", AsLemmaSource(&fv)),
1767 "[filing_status_type -> default \"single\"]"
1768 );
1769 }
1770
1771 #[test]
1772 fn as_lemma_source_help_escapes_quotes() {
1773 let fv = FactValue::TypeDeclaration {
1774 base: ParentType::Primitive(PrimitiveKind::Text),
1775 constraints: Some(vec![(
1776 TypeConstraintCommand::Help,
1777 vec![CommandArg::Text("say \"hello\"".to_string())],
1778 )]),
1779 from: None,
1780 };
1781 assert_eq!(
1782 format!("{}", AsLemmaSource(&fv)),
1783 "[text -> help \"say \\\"hello\\\"\"]"
1784 );
1785 }
1786
1787 #[test]
1788 fn as_lemma_source_typedef_regular_options_quoted() {
1789 let td = TypeDef::Regular {
1790 source_location: Source::new(
1791 "test",
1792 Span {
1793 start: 0,
1794 end: 0,
1795 line: 1,
1796 col: 0,
1797 },
1798 ),
1799 name: "status".to_string(),
1800 parent: ParentType::Primitive(PrimitiveKind::Text),
1801 constraints: Some(vec![
1802 (
1803 TypeConstraintCommand::Option,
1804 vec![CommandArg::Text("active".to_string())],
1805 ),
1806 (
1807 TypeConstraintCommand::Option,
1808 vec![CommandArg::Text("inactive".to_string())],
1809 ),
1810 ]),
1811 };
1812 let output = format!("{}", AsLemmaSource(&td));
1813 assert!(output.contains("option \"active\""), "got: {}", output);
1814 assert!(output.contains("option \"inactive\""), "got: {}", output);
1815 }
1816
1817 #[test]
1818 fn as_lemma_source_typedef_scale_units_not_quoted() {
1819 let td = TypeDef::Regular {
1820 source_location: Source::new(
1821 "test",
1822 Span {
1823 start: 0,
1824 end: 0,
1825 line: 1,
1826 col: 0,
1827 },
1828 ),
1829 name: "money".to_string(),
1830 parent: ParentType::Primitive(PrimitiveKind::Scale),
1831 constraints: Some(vec![
1832 (
1833 TypeConstraintCommand::Unit,
1834 vec![
1835 CommandArg::Label("eur".to_string()),
1836 CommandArg::Number("1.00".to_string()),
1837 ],
1838 ),
1839 (
1840 TypeConstraintCommand::Decimals,
1841 vec![CommandArg::Number("2".to_string())],
1842 ),
1843 (
1844 TypeConstraintCommand::Minimum,
1845 vec![CommandArg::Number("0".to_string())],
1846 ),
1847 ]),
1848 };
1849 let output = format!("{}", AsLemmaSource(&td));
1850 assert!(output.contains("unit eur 1.00"), "got: {}", output);
1851 assert!(output.contains("decimals 2"), "got: {}", output);
1852 assert!(output.contains("minimum 0"), "got: {}", output);
1853 }
1854}