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!(
38                f,
39                "Required data '{data}' was not provided. Did you mean '{suggestion}'?"
40            ),
41            VetoType::MissingData {
42                data,
43                suggestion: None,
44            } => write!(f, "Required data '{data}' was not provided."),
45            VetoType::MissingRowData { key } => match split_numbered_row_field_key(key) {
46                Some((list, number, field)) => write!(
47                    f,
48                    "Required field '{field}' was not provided on row {number} of list '{list}'."
49                ),
50                None => write!(f, "Required data '{key}' was not provided."),
51            },
52            VetoType::UserDefined { message: Some(msg) } => write!(f, "{msg}"),
53            VetoType::UserDefined { message: None } => write!(f, "This rule was vetoed."),
54            VetoType::Computation { message } => write!(f, "{message}"),
55        }
56    }
57}
58
59/// `list.number.field` from [`crate::planning::semantics::row_field_input_key`].
60fn split_numbered_row_field_key(key: &str) -> Option<(&str, &str, &str)> {
61    let (list_and_number, field) = key.rsplit_once('.')?;
62    let (list, number) = list_and_number.rsplit_once('.')?;
63    if number.is_empty() || !number.bytes().all(|b| b.is_ascii_digit()) {
64        return None;
65    }
66    if list.is_empty() || field.is_empty() {
67        return None;
68    }
69    Some((list, number, field))
70}
71
72impl VetoType {
73    #[must_use]
74    pub fn computation(message: impl Into<String>) -> Self {
75        VetoType::Computation {
76            message: message.into(),
77        }
78    }
79
80    #[must_use]
81    pub fn missing_data(data: DataPath, suggestion: Option<String>) -> Self {
82        VetoType::MissingData { data, suggestion }
83    }
84}
85
86impl Serialize for VetoType {
87    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
88    where
89        S: serde::Serializer,
90    {
91        serializer.serialize_str(&self.to_string())
92    }
93}
94
95/// Result of evaluating a rule, expression, or data leaf.
96#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
97#[serde(rename_all = "snake_case")]
98pub enum OperationResult {
99    /// Operation produced a value (canonical magnitude + optional written unit)
100    Value(BoundValueKind),
101    /// Operation was vetoed (valid result, no value)
102    Veto(VetoType),
103}
104
105impl OperationResult {
106    /// Wrap a typeless literal with no written-unit binding.
107    pub fn from_literal(value: LiteralValue) -> Self {
108        Self::Value(BoundValueKind::unbound(value.value))
109    }
110
111    pub fn from_bound(bound: BoundValueKind) -> Self {
112        Self::Value(bound)
113    }
114
115    pub fn vetoed(&self) -> bool {
116        matches!(self, OperationResult::Veto(_))
117    }
118
119    /// True when this result is a [`VetoType::MissingData`] or [`VetoType::MissingRowData`] veto.
120    #[must_use]
121    pub fn is_missing_data(&self) -> bool {
122        matches!(
123            self,
124            OperationResult::Veto(VetoType::MissingData { .. } | VetoType::MissingRowData { .. })
125        )
126    }
127
128    #[must_use]
129    pub fn value(&self) -> Option<&BoundValueKind> {
130        match self {
131            OperationResult::Value(value) => Some(value),
132            OperationResult::Veto(_) => None,
133        }
134    }
135
136    /// Typeless payload (written unit dropped). Prefer [`Self::value`] when the binding matters.
137    #[must_use]
138    pub fn literal_value(&self) -> Option<LiteralValue> {
139        self.value().map(BoundValueKind::to_literal)
140    }
141
142    pub fn number(number: rust_decimal::Decimal) -> Self {
143        Self::from_literal(LiteralValue::number_from_decimal(number))
144    }
145
146    pub fn measure(
147        value: rust_decimal::Decimal,
148        unit: impl Into<String>,
149        lemma_type: Option<LemmaType>,
150    ) -> Self {
151        use crate::computation::rational::checked_mul;
152        let lemma_type = std::sync::Arc::new(
153            lemma_type.unwrap_or_else(|| LemmaType::primitive(TypeSpecification::measure())),
154        );
155        let unit_name = unit.into();
156        let rational = crate::literals::rational_from_parsed_decimal(value)
157            .expect("BUG: operation result measure must lift at boundary");
158        let factor = if let TypeSpecification::Measure { units, .. } = &lemma_type.specifications {
159            units
160                .get(&unit_name)
161                .map(|u| u.factor.clone())
162                .unwrap_or_else(|_| {
163                    panic!(
164                        "BUG: OperationResult::measure unit '{}' not declared on type",
165                        unit_name
166                    )
167                })
168        } else {
169            crate::computation::rational::rational_one()
170        };
171        let canonical = checked_mul(&rational, &factor)
172            .expect("BUG: measure canonicalization overflow in OperationResult::measure");
173        Self::from_bound(BoundValueKind {
174            value: crate::planning::semantics::ValueKind::Measure(canonical),
175            unit: Some(std::sync::Arc::from(unit_name)),
176        })
177    }
178
179    pub fn text(text: impl Into<String>) -> Self {
180        Self::from_literal(LiteralValue::text(text.into()))
181    }
182
183    pub fn date(date: impl Into<SemanticDateTime>) -> Self {
184        Self::from_literal(LiteralValue::date(date.into()))
185    }
186
187    pub fn time(time: impl Into<SemanticTime>) -> Self {
188        Self::from_literal(LiteralValue::time(time.into()))
189    }
190
191    pub fn boolean(boolean: bool) -> Self {
192        Self::from_literal(LiteralValue::from_bool(boolean))
193    }
194
195    pub fn ratio(rational: rust_decimal::Decimal) -> Self {
196        Self::from_literal(LiteralValue::ratio_from_decimal(rational))
197    }
198
199    pub fn veto(veto: impl Into<String>) -> Self {
200        Self::Veto(VetoType::UserDefined {
201            message: Some(veto.into()),
202        })
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::VetoType;
209    use crate::planning::semantics::DataPath;
210
211    #[test]
212    fn veto_type_serializes_as_display_string() {
213        let v = VetoType::missing_data(DataPath::new(vec![], "product".to_string()), None);
214        let json = serde_json::to_string(&v).expect("serialize");
215        assert_eq!(json, "\"Required data 'product' was not provided.\"");
216    }
217
218    #[test]
219    fn missing_row_data_display_names_list_row_and_field() {
220        let v = VetoType::MissingRowData {
221            key: "menu.2.price".to_string(),
222        };
223        assert_eq!(
224            v.to_string(),
225            "Required field 'price' was not provided on row 2 of list 'menu'."
226        );
227    }
228}