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