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 { primitive: PrimitiveKind },
1068 Custom { name: String },
1070}
1071
1072impl std::fmt::Display for ParentType {
1073 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1074 match self {
1075 ParentType::Primitive { primitive: k } => write!(f, "{}", k),
1076 ParentType::Custom { name } => write!(f, "{}", name),
1077 }
1078 }
1079}
1080
1081#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1088#[serde(tag = "kind", rename_all = "snake_case")]
1089pub enum TypeDef {
1090 Regular {
1091 source_location: Source,
1092 name: String,
1093 parent: ParentType,
1094 constraints: Option<Vec<Constraint>>,
1095 },
1096 Import {
1097 source_location: Source,
1098 name: String,
1099 source_type: String,
1100 from: SpecRef,
1101 constraints: Option<Vec<Constraint>>,
1102 },
1103 Inline {
1104 source_location: Source,
1105 parent: ParentType,
1106 constraints: Option<Vec<Constraint>>,
1107 fact_ref: Reference,
1108 from: Option<SpecRef>,
1109 },
1110}
1111
1112impl TypeDef {
1113 pub fn source_location(&self) -> &Source {
1114 match self {
1115 TypeDef::Regular {
1116 source_location, ..
1117 }
1118 | TypeDef::Import {
1119 source_location, ..
1120 }
1121 | TypeDef::Inline {
1122 source_location, ..
1123 } => source_location,
1124 }
1125 }
1126
1127 pub fn name(&self) -> String {
1128 match self {
1129 TypeDef::Regular { name, .. } | TypeDef::Import { name, .. } => name.clone(),
1130 TypeDef::Inline { parent, .. } => format!("{}", parent),
1131 }
1132 }
1133}
1134
1135impl fmt::Display for TypeDef {
1136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1137 match self {
1138 TypeDef::Regular {
1139 name,
1140 parent,
1141 constraints,
1142 ..
1143 } => {
1144 write!(f, "type {}: {}", name, parent)?;
1145 if let Some(constraints) = constraints {
1146 for (cmd, args) in constraints {
1147 write!(f, "\n -> {}", cmd)?;
1148 for arg in args {
1149 write!(f, " {}", arg.value())?;
1150 }
1151 }
1152 }
1153 Ok(())
1154 }
1155 TypeDef::Import {
1156 name,
1157 from,
1158 constraints,
1159 ..
1160 } => {
1161 write!(f, "type {} from {}", name, from)?;
1162 if let Some(constraints) = constraints {
1163 for (cmd, args) in constraints {
1164 write!(f, " -> {}", cmd)?;
1165 for arg in args {
1166 write!(f, " {}", arg.value())?;
1167 }
1168 }
1169 }
1170 Ok(())
1171 }
1172 TypeDef::Inline { .. } => Ok(()),
1173 }
1174 }
1175}
1176
1177pub struct AsLemmaSource<'a, T: ?Sized>(pub &'a T);
1192
1193pub fn quote_lemma_text(s: &str) -> String {
1196 let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
1197 format!("\"{}\"", escaped)
1198}
1199
1200fn format_decimal_source(n: &Decimal) -> String {
1205 let norm = n.normalize();
1206 let raw = if norm.fract().is_zero() {
1207 norm.trunc().to_string()
1208 } else {
1209 norm.to_string()
1210 };
1211 group_digits(&raw)
1212}
1213
1214fn group_digits(s: &str) -> String {
1218 let (sign, rest) = if s.starts_with('-') || s.starts_with('+') {
1219 (&s[..1], &s[1..])
1220 } else {
1221 ("", s)
1222 };
1223
1224 let (int_part, frac_part) = match rest.find('.') {
1225 Some(pos) => (&rest[..pos], &rest[pos..]),
1226 None => (rest, ""),
1227 };
1228
1229 if int_part.len() < 4 {
1230 return s.to_string();
1231 }
1232
1233 let mut grouped = String::with_capacity(int_part.len() + int_part.len() / 3);
1234 for (i, ch) in int_part.chars().enumerate() {
1235 let digits_remaining = int_part.len() - i;
1236 if i > 0 && digits_remaining % 3 == 0 {
1237 grouped.push('_');
1238 }
1239 grouped.push(ch);
1240 }
1241
1242 format!("{}{}{}", sign, grouped, frac_part)
1243}
1244
1245impl<'a> fmt::Display for AsLemmaSource<'a, CommandArg> {
1248 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1249 match self.0 {
1250 CommandArg::Text(s) => write!(f, "{}", quote_lemma_text(s)),
1251 CommandArg::Number(s) => {
1252 let clean: String = s.chars().filter(|c| *c != '_' && *c != ',').collect();
1253 write!(f, "{}", group_digits(&clean))
1254 }
1255 CommandArg::Boolean(bv) => write!(f, "{}", bv),
1256 CommandArg::Label(s) => write!(f, "{}", s),
1257 }
1258 }
1259}
1260
1261fn format_constraint_as_source(cmd: &TypeConstraintCommand, args: &[CommandArg]) -> String {
1266 if args.is_empty() {
1267 cmd.to_string()
1268 } else {
1269 let args_str: Vec<String> = args
1270 .iter()
1271 .map(|a| format!("{}", AsLemmaSource(a)))
1272 .collect();
1273 format!("{} {}", cmd, args_str.join(" "))
1274 }
1275}
1276
1277fn format_constraints_as_source(constraints: &[Constraint], separator: &str) -> String {
1280 constraints
1281 .iter()
1282 .map(|(cmd, args)| format_constraint_as_source(cmd, args))
1283 .collect::<Vec<_>>()
1284 .join(separator)
1285}
1286
1287impl<'a> fmt::Display for AsLemmaSource<'a, FactValue> {
1290 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1291 match self.0 {
1292 FactValue::Literal(v) => write!(f, "{}", AsLemmaSource(v)),
1293 FactValue::SpecReference(spec_ref) => {
1294 write!(f, "spec {}", spec_ref)
1295 }
1296 FactValue::TypeDeclaration {
1297 base,
1298 constraints,
1299 from,
1300 } => {
1301 let base_str = if let Some(from_spec) = from {
1302 format!("{} from {}", base, from_spec)
1303 } else {
1304 format!("{}", base)
1305 };
1306 if let Some(ref constraints_vec) = constraints {
1307 let constraint_str = format_constraints_as_source(constraints_vec, " -> ");
1308 write!(f, "[{} -> {}]", base_str, constraint_str)
1309 } else {
1310 write!(f, "[{}]", base_str)
1311 }
1312 }
1313 }
1314 }
1315}
1316
1317impl<'a> fmt::Display for AsLemmaSource<'a, Value> {
1320 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1321 match self.0 {
1322 Value::Number(n) => write!(f, "{}", format_decimal_source(n)),
1323 Value::Text(s) => write!(f, "{}", quote_lemma_text(s)),
1324 Value::Date(dt) => {
1325 let is_date_only =
1326 dt.hour == 0 && dt.minute == 0 && dt.second == 0 && dt.timezone.is_none();
1327 if is_date_only {
1328 write!(f, "{:04}-{:02}-{:02}", dt.year, dt.month, dt.day)
1329 } else {
1330 write!(
1331 f,
1332 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
1333 dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second
1334 )?;
1335 if let Some(tz) = &dt.timezone {
1336 write!(f, "{}", tz)?;
1337 }
1338 Ok(())
1339 }
1340 }
1341 Value::Time(t) => {
1342 write!(f, "{:02}:{:02}:{:02}", t.hour, t.minute, t.second)?;
1343 if let Some(tz) = &t.timezone {
1344 write!(f, "{}", tz)?;
1345 }
1346 Ok(())
1347 }
1348 Value::Boolean(b) => write!(f, "{}", b),
1349 Value::Scale(n, u) => write!(f, "{} {}", format_decimal_source(n), u),
1350 Value::Duration(n, u) => write!(f, "{} {}", format_decimal_source(n), u),
1351 Value::Ratio(n, unit) => match unit.as_deref() {
1352 Some("percent") => {
1353 let display_value = *n * Decimal::from(100);
1354 write!(f, "{}%", format_decimal_source(&display_value))
1355 }
1356 Some("permille") => {
1357 let display_value = *n * Decimal::from(1000);
1358 write!(f, "{}%%", format_decimal_source(&display_value))
1359 }
1360 Some(unit_name) => write!(f, "{} {}", format_decimal_source(n), unit_name),
1361 None => write!(f, "{}", format_decimal_source(n)),
1362 },
1363 }
1364 }
1365}
1366
1367impl<'a> fmt::Display for AsLemmaSource<'a, MetaValue> {
1370 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1371 match self.0 {
1372 MetaValue::Literal(v) => write!(f, "{}", AsLemmaSource(v)),
1373 MetaValue::Unquoted(s) => write!(f, "{}", s),
1374 }
1375 }
1376}
1377
1378impl<'a> fmt::Display for AsLemmaSource<'a, TypeDef> {
1381 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1382 match self.0 {
1383 TypeDef::Regular {
1384 name,
1385 parent,
1386 constraints,
1387 ..
1388 } => {
1389 write!(f, "type {}: {}", name, parent)?;
1390 if let Some(constraints) = constraints {
1391 for (cmd, args) in constraints {
1392 write!(f, "\n -> {}", format_constraint_as_source(cmd, args))?;
1393 }
1394 }
1395 Ok(())
1396 }
1397 TypeDef::Import {
1398 name,
1399 from,
1400 constraints,
1401 ..
1402 } => {
1403 write!(f, "type {} from {}", name, from)?;
1404 if let Some(constraints) = constraints {
1405 for (cmd, args) in constraints {
1406 write!(f, " -> {}", format_constraint_as_source(cmd, args))?;
1407 }
1408 }
1409 Ok(())
1410 }
1411 TypeDef::Inline { .. } => Ok(()),
1412 }
1413 }
1414}
1415
1416#[cfg(test)]
1417mod tests {
1418 use super::*;
1419
1420 #[test]
1421 fn test_duration_unit_display() {
1422 assert_eq!(format!("{}", DurationUnit::Second), "seconds");
1423 assert_eq!(format!("{}", DurationUnit::Minute), "minutes");
1424 assert_eq!(format!("{}", DurationUnit::Hour), "hours");
1425 assert_eq!(format!("{}", DurationUnit::Day), "days");
1426 assert_eq!(format!("{}", DurationUnit::Week), "weeks");
1427 assert_eq!(format!("{}", DurationUnit::Millisecond), "milliseconds");
1428 assert_eq!(format!("{}", DurationUnit::Microsecond), "microseconds");
1429 }
1430
1431 #[test]
1432 fn test_conversion_target_display() {
1433 assert_eq!(
1434 format!("{}", ConversionTarget::Duration(DurationUnit::Hour)),
1435 "hours"
1436 );
1437 assert_eq!(
1438 format!("{}", ConversionTarget::Unit("usd".to_string())),
1439 "usd"
1440 );
1441 }
1442
1443 #[test]
1444 fn test_value_ratio_display() {
1445 use rust_decimal::Decimal;
1446 use std::str::FromStr;
1447 let percent = Value::Ratio(
1448 Decimal::from_str("0.10").unwrap(),
1449 Some("percent".to_string()),
1450 );
1451 assert_eq!(format!("{}", percent), "10%");
1452 let permille = Value::Ratio(
1453 Decimal::from_str("0.005").unwrap(),
1454 Some("permille".to_string()),
1455 );
1456 assert_eq!(format!("{}", permille), "5%%");
1457 }
1458
1459 #[test]
1460 fn test_datetime_value_display() {
1461 let dt = DateTimeValue {
1462 year: 2024,
1463 month: 12,
1464 day: 25,
1465 hour: 14,
1466 minute: 30,
1467 second: 45,
1468 microsecond: 0,
1469 timezone: Some(TimezoneValue {
1470 offset_hours: 1,
1471 offset_minutes: 0,
1472 }),
1473 };
1474 assert_eq!(format!("{}", dt), "2024-12-25T14:30:45+01:00");
1475 }
1476
1477 #[test]
1478 fn test_datetime_value_display_date_only() {
1479 let dt = DateTimeValue {
1480 year: 2026,
1481 month: 3,
1482 day: 4,
1483 hour: 0,
1484 minute: 0,
1485 second: 0,
1486 microsecond: 0,
1487 timezone: None,
1488 };
1489 assert_eq!(format!("{}", dt), "2026-03-04");
1490 }
1491
1492 #[test]
1493 fn test_datetime_value_display_microseconds() {
1494 let dt = DateTimeValue {
1495 year: 2026,
1496 month: 2,
1497 day: 23,
1498 hour: 14,
1499 minute: 30,
1500 second: 45,
1501 microsecond: 123456,
1502 timezone: Some(TimezoneValue {
1503 offset_hours: 0,
1504 offset_minutes: 0,
1505 }),
1506 };
1507 assert_eq!(format!("{}", dt), "2026-02-23T14:30:45.123456Z");
1508 }
1509
1510 #[test]
1511 fn test_datetime_microsecond_in_ordering() {
1512 let a = DateTimeValue {
1513 year: 2026,
1514 month: 1,
1515 day: 1,
1516 hour: 0,
1517 minute: 0,
1518 second: 0,
1519 microsecond: 100,
1520 timezone: None,
1521 };
1522 let b = DateTimeValue {
1523 year: 2026,
1524 month: 1,
1525 day: 1,
1526 hour: 0,
1527 minute: 0,
1528 second: 0,
1529 microsecond: 200,
1530 timezone: None,
1531 };
1532 assert!(a < b);
1533 }
1534
1535 #[test]
1536 fn test_datetime_parse_iso_week() {
1537 let dt: DateTimeValue = "2026-W01".parse().unwrap();
1538 assert_eq!(dt.year, 2025);
1539 assert_eq!(dt.month, 12);
1540 assert_eq!(dt.day, 29);
1541 assert_eq!(dt.microsecond, 0);
1542 }
1543
1544 #[test]
1545 fn test_time_value_display() {
1546 let time = TimeValue {
1547 hour: 14,
1548 minute: 30,
1549 second: 45,
1550 timezone: Some(TimezoneValue {
1551 offset_hours: -5,
1552 offset_minutes: 30,
1553 }),
1554 };
1555 let display = format!("{}", time);
1556 assert!(display.contains("14"));
1557 assert!(display.contains("30"));
1558 assert!(display.contains("45"));
1559 }
1560
1561 #[test]
1562 fn test_timezone_value() {
1563 let tz_positive = TimezoneValue {
1564 offset_hours: 5,
1565 offset_minutes: 30,
1566 };
1567 assert_eq!(tz_positive.offset_hours, 5);
1568 assert_eq!(tz_positive.offset_minutes, 30);
1569
1570 let tz_negative = TimezoneValue {
1571 offset_hours: -8,
1572 offset_minutes: 0,
1573 };
1574 assert_eq!(tz_negative.offset_hours, -8);
1575 }
1576
1577 #[test]
1578 fn test_negation_types() {
1579 let json = serde_json::to_string(&NegationType::Not).expect("serialize NegationType");
1580 let decoded: NegationType = serde_json::from_str(&json).expect("deserialize NegationType");
1581 assert_eq!(decoded, NegationType::Not);
1582 }
1583
1584 #[test]
1585 fn parent_type_primitive_serde_internally_tagged() {
1586 let p = ParentType::Primitive {
1587 primitive: PrimitiveKind::Number,
1588 };
1589 let json = serde_json::to_string(&p).expect("ParentType::Primitive must serialize");
1590 assert!(json.contains("\"kind\"") && json.contains("\"primitive\""));
1591 let back: ParentType = serde_json::from_str(&json).expect("deserialize");
1592 assert_eq!(back, p);
1593 }
1594
1595 #[test]
1596 fn parent_type_custom_serde_internally_tagged() {
1597 let p = ParentType::Custom {
1598 name: "money".to_string(),
1599 };
1600 let json = serde_json::to_string(&p).expect("ParentType::Custom must serialize");
1601 assert!(json.contains("\"kind\"") && json.contains("\"name\""));
1602 let back: ParentType = serde_json::from_str(&json).expect("deserialize");
1603 assert_eq!(back, p);
1604 }
1605
1606 #[test]
1607 fn test_veto_expression() {
1608 let veto_with_message = VetoExpression {
1609 message: Some("Must be over 18".to_string()),
1610 };
1611 assert_eq!(
1612 veto_with_message.message,
1613 Some("Must be over 18".to_string())
1614 );
1615
1616 let veto_without_message = VetoExpression { message: None };
1617 assert!(veto_without_message.message.is_none());
1618 }
1619
1620 #[test]
1629 fn as_lemma_source_text_default_is_quoted() {
1630 let fv = FactValue::TypeDeclaration {
1631 base: ParentType::Primitive {
1632 primitive: PrimitiveKind::Text,
1633 },
1634 constraints: Some(vec![(
1635 TypeConstraintCommand::Default,
1636 vec![CommandArg::Text("single".to_string())],
1637 )]),
1638 from: None,
1639 };
1640 assert_eq!(
1641 format!("{}", AsLemmaSource(&fv)),
1642 "[text -> default \"single\"]"
1643 );
1644 }
1645
1646 #[test]
1647 fn as_lemma_source_number_default_not_quoted() {
1648 let fv = FactValue::TypeDeclaration {
1649 base: ParentType::Primitive {
1650 primitive: PrimitiveKind::Number,
1651 },
1652 constraints: Some(vec![(
1653 TypeConstraintCommand::Default,
1654 vec![CommandArg::Number("10".to_string())],
1655 )]),
1656 from: None,
1657 };
1658 assert_eq!(format!("{}", AsLemmaSource(&fv)), "[number -> default 10]");
1659 }
1660
1661 #[test]
1662 fn as_lemma_source_help_always_quoted() {
1663 let fv = FactValue::TypeDeclaration {
1664 base: ParentType::Primitive {
1665 primitive: PrimitiveKind::Number,
1666 },
1667 constraints: Some(vec![(
1668 TypeConstraintCommand::Help,
1669 vec![CommandArg::Text("Enter a quantity".to_string())],
1670 )]),
1671 from: None,
1672 };
1673 assert_eq!(
1674 format!("{}", AsLemmaSource(&fv)),
1675 "[number -> help \"Enter a quantity\"]"
1676 );
1677 }
1678
1679 #[test]
1680 fn as_lemma_source_text_option_quoted() {
1681 let fv = FactValue::TypeDeclaration {
1682 base: ParentType::Primitive {
1683 primitive: PrimitiveKind::Text,
1684 },
1685 constraints: Some(vec![
1686 (
1687 TypeConstraintCommand::Option,
1688 vec![CommandArg::Text("active".to_string())],
1689 ),
1690 (
1691 TypeConstraintCommand::Option,
1692 vec![CommandArg::Text("inactive".to_string())],
1693 ),
1694 ]),
1695 from: None,
1696 };
1697 assert_eq!(
1698 format!("{}", AsLemmaSource(&fv)),
1699 "[text -> option \"active\" -> option \"inactive\"]"
1700 );
1701 }
1702
1703 #[test]
1704 fn as_lemma_source_scale_unit_not_quoted() {
1705 let fv = FactValue::TypeDeclaration {
1706 base: ParentType::Primitive {
1707 primitive: PrimitiveKind::Scale,
1708 },
1709 constraints: Some(vec![
1710 (
1711 TypeConstraintCommand::Unit,
1712 vec![
1713 CommandArg::Label("eur".to_string()),
1714 CommandArg::Number("1.00".to_string()),
1715 ],
1716 ),
1717 (
1718 TypeConstraintCommand::Unit,
1719 vec![
1720 CommandArg::Label("usd".to_string()),
1721 CommandArg::Number("1.10".to_string()),
1722 ],
1723 ),
1724 ]),
1725 from: None,
1726 };
1727 assert_eq!(
1728 format!("{}", AsLemmaSource(&fv)),
1729 "[scale -> unit eur 1.00 -> unit usd 1.10]"
1730 );
1731 }
1732
1733 #[test]
1734 fn as_lemma_source_scale_minimum_with_unit() {
1735 let fv = FactValue::TypeDeclaration {
1736 base: ParentType::Primitive {
1737 primitive: PrimitiveKind::Scale,
1738 },
1739 constraints: Some(vec![(
1740 TypeConstraintCommand::Minimum,
1741 vec![
1742 CommandArg::Number("0".to_string()),
1743 CommandArg::Label("eur".to_string()),
1744 ],
1745 )]),
1746 from: None,
1747 };
1748 assert_eq!(
1749 format!("{}", AsLemmaSource(&fv)),
1750 "[scale -> minimum 0 eur]"
1751 );
1752 }
1753
1754 #[test]
1755 fn as_lemma_source_boolean_default() {
1756 let fv = FactValue::TypeDeclaration {
1757 base: ParentType::Primitive {
1758 primitive: PrimitiveKind::Boolean,
1759 },
1760 constraints: Some(vec![(
1761 TypeConstraintCommand::Default,
1762 vec![CommandArg::Boolean(BooleanValue::True)],
1763 )]),
1764 from: None,
1765 };
1766 assert_eq!(
1767 format!("{}", AsLemmaSource(&fv)),
1768 "[boolean -> default true]"
1769 );
1770 }
1771
1772 #[test]
1773 fn as_lemma_source_duration_default() {
1774 let fv = FactValue::TypeDeclaration {
1775 base: ParentType::Primitive {
1776 primitive: PrimitiveKind::Duration,
1777 },
1778 constraints: Some(vec![(
1779 TypeConstraintCommand::Default,
1780 vec![
1781 CommandArg::Number("40".to_string()),
1782 CommandArg::Label("hours".to_string()),
1783 ],
1784 )]),
1785 from: None,
1786 };
1787 assert_eq!(
1788 format!("{}", AsLemmaSource(&fv)),
1789 "[duration -> default 40 hours]"
1790 );
1791 }
1792
1793 #[test]
1794 fn as_lemma_source_named_type_default_quoted() {
1795 let fv = FactValue::TypeDeclaration {
1798 base: ParentType::Custom {
1799 name: "filing_status_type".to_string(),
1800 },
1801 constraints: Some(vec![(
1802 TypeConstraintCommand::Default,
1803 vec![CommandArg::Text("single".to_string())],
1804 )]),
1805 from: None,
1806 };
1807 assert_eq!(
1808 format!("{}", AsLemmaSource(&fv)),
1809 "[filing_status_type -> default \"single\"]"
1810 );
1811 }
1812
1813 #[test]
1814 fn as_lemma_source_help_escapes_quotes() {
1815 let fv = FactValue::TypeDeclaration {
1816 base: ParentType::Primitive {
1817 primitive: PrimitiveKind::Text,
1818 },
1819 constraints: Some(vec![(
1820 TypeConstraintCommand::Help,
1821 vec![CommandArg::Text("say \"hello\"".to_string())],
1822 )]),
1823 from: None,
1824 };
1825 assert_eq!(
1826 format!("{}", AsLemmaSource(&fv)),
1827 "[text -> help \"say \\\"hello\\\"\"]"
1828 );
1829 }
1830
1831 #[test]
1832 fn as_lemma_source_typedef_regular_options_quoted() {
1833 let td = TypeDef::Regular {
1834 source_location: Source::new(
1835 "test",
1836 Span {
1837 start: 0,
1838 end: 0,
1839 line: 1,
1840 col: 0,
1841 },
1842 ),
1843 name: "status".to_string(),
1844 parent: ParentType::Primitive {
1845 primitive: PrimitiveKind::Text,
1846 },
1847 constraints: Some(vec![
1848 (
1849 TypeConstraintCommand::Option,
1850 vec![CommandArg::Text("active".to_string())],
1851 ),
1852 (
1853 TypeConstraintCommand::Option,
1854 vec![CommandArg::Text("inactive".to_string())],
1855 ),
1856 ]),
1857 };
1858 let output = format!("{}", AsLemmaSource(&td));
1859 assert!(output.contains("option \"active\""), "got: {}", output);
1860 assert!(output.contains("option \"inactive\""), "got: {}", output);
1861 }
1862
1863 #[test]
1864 fn as_lemma_source_typedef_scale_units_not_quoted() {
1865 let td = TypeDef::Regular {
1866 source_location: Source::new(
1867 "test",
1868 Span {
1869 start: 0,
1870 end: 0,
1871 line: 1,
1872 col: 0,
1873 },
1874 ),
1875 name: "money".to_string(),
1876 parent: ParentType::Primitive {
1877 primitive: PrimitiveKind::Scale,
1878 },
1879 constraints: Some(vec![
1880 (
1881 TypeConstraintCommand::Unit,
1882 vec![
1883 CommandArg::Label("eur".to_string()),
1884 CommandArg::Number("1.00".to_string()),
1885 ],
1886 ),
1887 (
1888 TypeConstraintCommand::Decimals,
1889 vec![CommandArg::Number("2".to_string())],
1890 ),
1891 (
1892 TypeConstraintCommand::Minimum,
1893 vec![CommandArg::Number("0".to_string())],
1894 ),
1895 ]),
1896 };
1897 let output = format!("{}", AsLemmaSource(&td));
1898 assert!(output.contains("unit eur 1.00"), "got: {}", output);
1899 assert!(output.contains("decimals 2"), "got: {}", output);
1900 assert!(output.contains("minimum 0"), "got: {}", output);
1901 }
1902}