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