Skip to main content

lemma/api/
value.rs

1//! ValueKind / LiteralValue / RuleResultValue JSON shapes.
2
3use crate::planning::semantics::{
4    LiteralValue as DomainLiteralValue, SemanticDateTime, SemanticTime,
5    ValueKind as DomainValueKind,
6};
7use crate::result_value::{
8    CalendarResult as DomainCalendarResult, RangeResult as DomainRangeResult,
9    RuleResultValue as DomainRuleResultValue,
10};
11use rust_decimal::Decimal;
12use serde::{Deserialize, Serialize};
13use std::collections::BTreeMap;
14
15/// Externally tagged value payload matching today's `ValueKind` Serialize shape.
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "lowercase")]
18pub enum ValueKind {
19    Number(Decimal),
20    Measure {
21        value: Decimal,
22    },
23    Ratio {
24        value: Decimal,
25    },
26    Text(String),
27    Date(SemanticDateTime),
28    Time(SemanticTime),
29    Boolean(bool),
30    Range {
31        from: Box<ValueKind>,
32        to: Box<ValueKind>,
33    },
34}
35
36impl From<&DomainValueKind> for ValueKind {
37    fn from(value: &DomainValueKind) -> Self {
38        match value {
39            DomainValueKind::Number(rational) => Self::Number(
40                rational
41                    .try_to_decimal()
42                    .expect("BUG: planned number must convert to decimal"),
43            ),
44            DomainValueKind::Measure(rational) => Self::Measure {
45                value: rational
46                    .try_to_decimal()
47                    .expect("BUG: planned measure must convert to decimal"),
48            },
49            DomainValueKind::Ratio(rational) => Self::Ratio {
50                value: rational
51                    .try_to_decimal()
52                    .expect("BUG: planned ratio must convert to decimal"),
53            },
54            DomainValueKind::Text(text) => Self::Text(text.clone()),
55            DomainValueKind::Date(date) => Self::Date(date.clone()),
56            DomainValueKind::Time(time) => Self::Time(time.clone()),
57            DomainValueKind::Boolean(flag) => Self::Boolean(*flag),
58            DomainValueKind::Range(from, to) => Self::Range {
59                from: Box::new(ValueKind::from(&from.value)),
60                to: Box::new(ValueKind::from(&to.value)),
61            },
62            DomainValueKind::Spec(_) => {
63                unreachable!("BUG: spec values are rule results, not nested ValueKind payloads")
64            }
65        }
66    }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct LiteralValue {
71    pub value: ValueKind,
72}
73
74impl From<&DomainLiteralValue> for LiteralValue {
75    fn from(literal: &DomainLiteralValue) -> Self {
76        Self {
77            value: ValueKind::from(&literal.value),
78        }
79    }
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
83pub struct CalendarResult {
84    pub value: Decimal,
85    pub unit: String,
86}
87
88impl From<&DomainCalendarResult> for CalendarResult {
89    fn from(calendar: &DomainCalendarResult) -> Self {
90        Self {
91            value: calendar.value,
92            unit: calendar.unit.clone(),
93        }
94    }
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
98pub struct RangeResult {
99    pub from: RuleResultValue,
100    pub to: RuleResultValue,
101}
102
103impl From<&DomainRangeResult> for RangeResult {
104    fn from(range: &DomainRangeResult) -> Self {
105        Self {
106            from: RuleResultValue::from(&range.from),
107            to: RuleResultValue::from(&range.to),
108        }
109    }
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
113pub struct RuleResultValue {
114    #[serde(default, skip_serializing_if = "Option::is_none")]
115    pub result: Option<String>,
116    #[serde(default, skip_serializing_if = "Option::is_none")]
117    pub measure: Option<BTreeMap<String, Decimal>>,
118    #[serde(default, skip_serializing_if = "Option::is_none")]
119    pub ratio: Option<BTreeMap<String, Decimal>>,
120    #[serde(default, skip_serializing_if = "Option::is_none")]
121    pub number: Option<Decimal>,
122    #[serde(default, skip_serializing_if = "Option::is_none")]
123    pub boolean: Option<bool>,
124    #[serde(default, skip_serializing_if = "Option::is_none")]
125    pub text: Option<String>,
126    #[serde(default, skip_serializing_if = "Option::is_none")]
127    pub date: Option<SemanticDateTime>,
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub time: Option<SemanticTime>,
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    pub calendar: Option<CalendarResult>,
132    #[serde(default, skip_serializing_if = "Option::is_none")]
133    pub unit: Option<String>,
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub range: Option<Box<RangeResult>>,
136    #[serde(default, skip_serializing_if = "Option::is_none")]
137    pub spec: Option<Box<crate::result_value::SpecResult>>,
138}
139
140impl From<&DomainRuleResultValue> for RuleResultValue {
141    fn from(value: &DomainRuleResultValue) -> Self {
142        Self {
143            result: value.result.clone(),
144            measure: value.measure.clone(),
145            ratio: value.ratio.clone(),
146            number: value.number,
147            boolean: value.boolean,
148            text: value.text.clone(),
149            date: value.date.clone(),
150            time: value.time.clone(),
151            calendar: value.calendar.as_ref().map(CalendarResult::from),
152            unit: value.unit.clone(),
153            range: value
154                .range
155                .as_ref()
156                .map(|range| Box::new(RangeResult::from(range.as_ref()))),
157            spec: value.spec.clone(),
158        }
159    }
160}