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