Skip to main content

lemma/computation/
operation_result.rs

1//! Result envelope for computation operations: a produced value or a domain veto.
2
3use std::fmt;
4
5use crate::planning::semantics::{
6    DataPath, LemmaType, LiteralValue, SemanticDateTime, SemanticTime, TypeSpecification,
7};
8use serde::Serialize;
9
10/// Why an operation yielded no value (domain veto).
11///
12/// JSON serialization is a single string (see [`fmt::Display`]). There is intentionally no
13/// `Deserialize` implementation: veto payloads are engine output only.
14#[derive(Debug, Clone, PartialEq)]
15pub enum VetoType {
16    /// Evaluation needed a data that was not provided
17    MissingData {
18        data: DataPath,
19        suggestion: Option<String>,
20    },
21    /// Explicit `veto "reason"` in Lemma source
22    UserDefined { message: Option<String> },
23    /// Runtime domain failure (division by zero, date overflow, bad Data override, etc.)
24    Computation { message: String },
25}
26
27impl fmt::Display for VetoType {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        match self {
30            VetoType::MissingData {
31                data,
32                suggestion: Some(suggestion),
33            } => write!(f, "Missing data: {data} (did you mean '{suggestion}'?)"),
34            VetoType::MissingData {
35                data,
36                suggestion: None,
37            } => write!(f, "Missing data: {data}"),
38            VetoType::UserDefined { message: Some(msg) } => write!(f, "{msg}"),
39            VetoType::UserDefined { message: None } => write!(f, "Vetoed"),
40            VetoType::Computation { message } => write!(f, "{message}"),
41        }
42    }
43}
44
45impl VetoType {
46    #[must_use]
47    pub fn computation(message: impl Into<String>) -> Self {
48        VetoType::Computation {
49            message: message.into(),
50        }
51    }
52
53    #[must_use]
54    pub fn missing_data(data: DataPath, suggestion: Option<String>) -> Self {
55        VetoType::MissingData { data, suggestion }
56    }
57}
58
59impl Serialize for VetoType {
60    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
61    where
62        S: serde::Serializer,
63    {
64        serializer.serialize_str(&self.to_string())
65    }
66}
67
68/// Result of evaluating a rule, expression, or data leaf.
69#[derive(Debug, Clone, PartialEq, Serialize)]
70#[serde(rename_all = "snake_case")]
71pub enum OperationResult {
72    /// Operation produced a value
73    Value(LiteralValue),
74    /// Operation was vetoed (valid result, no value)
75    Veto(VetoType),
76}
77
78impl OperationResult {
79    pub fn from_literal(value: LiteralValue) -> Self {
80        Self::Value(value)
81    }
82
83    pub fn vetoed(&self) -> bool {
84        matches!(self, OperationResult::Veto(_))
85    }
86
87    /// True when this result is a [`VetoType::MissingData`] veto.
88    #[must_use]
89    pub fn is_missing_data(&self) -> bool {
90        matches!(self, OperationResult::Veto(VetoType::MissingData { .. }))
91    }
92
93    #[must_use]
94    pub fn value(&self) -> Option<&LiteralValue> {
95        match self {
96            OperationResult::Value(value) => Some(value),
97            OperationResult::Veto(_) => None,
98        }
99    }
100
101    pub fn number(number: rust_decimal::Decimal) -> Self {
102        Self::from_literal(LiteralValue::number_from_decimal(number))
103    }
104
105    pub fn measure(
106        value: rust_decimal::Decimal,
107        unit: impl Into<String>,
108        lemma_type: Option<LemmaType>,
109    ) -> Self {
110        use crate::computation::rational::checked_mul;
111        let lemma_type = std::sync::Arc::new(
112            lemma_type.unwrap_or_else(|| LemmaType::primitive(TypeSpecification::measure())),
113        );
114        let unit_name = unit.into();
115        let rational = crate::literals::rational_from_parsed_decimal(value)
116            .expect("BUG: operation result measure must lift at boundary");
117        let factor = if let TypeSpecification::Measure { units, .. } = &lemma_type.specifications {
118            units
119                .get(&unit_name)
120                .map(|u| u.factor.clone())
121                .unwrap_or_else(|_| {
122                    panic!(
123                        "BUG: OperationResult::measure unit '{}' not declared on type",
124                        unit_name
125                    )
126                })
127        } else {
128            crate::computation::rational::rational_one()
129        };
130        let canonical = checked_mul(&rational, &factor)
131            .expect("BUG: measure canonicalization overflow in OperationResult::measure");
132        Self::from_literal(LiteralValue::measure_with_bound_unit(
133            canonical, unit_name, lemma_type,
134        ))
135    }
136
137    pub fn text(text: impl Into<String>) -> Self {
138        Self::from_literal(LiteralValue::text(text.into()))
139    }
140
141    pub fn date(date: impl Into<SemanticDateTime>) -> Self {
142        Self::from_literal(LiteralValue::date(date.into()))
143    }
144
145    pub fn time(time: impl Into<SemanticTime>) -> Self {
146        Self::from_literal(LiteralValue::time(time.into()))
147    }
148
149    pub fn boolean(boolean: bool) -> Self {
150        Self::from_literal(LiteralValue::from_bool(boolean))
151    }
152
153    pub fn ratio(rational: rust_decimal::Decimal) -> Self {
154        Self::from_literal(LiteralValue::ratio_from_decimal(rational))
155    }
156
157    pub fn veto(veto: impl Into<String>) -> Self {
158        Self::Veto(VetoType::UserDefined {
159            message: Some(veto.into()),
160        })
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::VetoType;
167    use crate::planning::semantics::DataPath;
168
169    #[test]
170    fn veto_type_serializes_as_display_string() {
171        let v = VetoType::missing_data(DataPath::new(vec![], "product".to_string()), None);
172        let json = serde_json::to_string(&v).expect("serialize");
173        assert_eq!(json, "\"Missing data: product\"");
174    }
175}