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