Skip to main content

lemma/evaluation/
data_input.rs

1use crate::evaluation::operations::{OperationResult, VetoType};
2use crate::planning::execution_plan::{validate_value_against_type, ExecutionPlan};
3use crate::planning::semantics::{
4    number_with_unit_to_value_kind, parse_value_from_string, parser_value_to_value_kind,
5    DataDefinition, DataPath, LemmaType, LiteralValue, Source, TypeSpecification, ValueKind,
6};
7use crate::Error;
8use crate::ResourceLimits;
9use rust_decimal::Decimal;
10use std::collections::{BTreeMap, HashMap, HashSet};
11use std::str::FromStr;
12use std::sync::Arc;
13
14/// Typed data value from a client (CLI/WASM). JSON parsing stays outside [`parse_data_value`].
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum DataValueInput {
17    Convenience(String),
18    Boolean(bool),
19    MeasureMap(BTreeMap<String, String>),
20    RatioMap(BTreeMap<String, String>),
21}
22
23impl DataValueInput {
24    pub fn convenience(value: impl Into<String>) -> Self {
25        Self::Convenience(value.into())
26    }
27}
28
29pub fn parse_data_value(
30    input: &DataValueInput,
31    lemma_type: &Arc<LemmaType>,
32    source: &Source,
33) -> Result<LiteralValue, Error> {
34    let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
35    let type_spec = &lemma_type.specifications;
36
37    let kind = match (input, type_spec) {
38        (DataValueInput::Convenience(s), _) => {
39            let parsed = parse_value_from_string(s, type_spec, source)?;
40            parser_value_to_value_kind(&parsed, type_spec).map_err(to_err)?
41        }
42        (DataValueInput::Boolean(b), TypeSpecification::Boolean { .. }) => ValueKind::Boolean(*b),
43        (DataValueInput::Boolean(_), _) => {
44            return Err(to_err(format!(
45                "boolean input is only valid for boolean data, not {}",
46                value_kind_tag_for_type(type_spec)
47            )));
48        }
49        (DataValueInput::MeasureMap(map), TypeSpecification::Measure { .. }) => {
50            measure_from_unit_map(map, lemma_type.as_ref()).map_err(to_err)?
51        }
52        (
53            DataValueInput::MeasureMap(map) | DataValueInput::RatioMap(map),
54            TypeSpecification::Ratio { .. },
55        ) => ratio_from_unit_map(map, lemma_type.as_ref()).map_err(to_err)?,
56        (DataValueInput::MeasureMap(_), _) => {
57            return Err(to_err(format!(
58                "measure unit map is only valid for measure data, not {}",
59                value_kind_tag_for_type(type_spec)
60            )));
61        }
62        (DataValueInput::RatioMap(_), _) => {
63            return Err(to_err(format!(
64                "ratio unit map is only valid for ratio data, not {}",
65                value_kind_tag_for_type(type_spec)
66            )));
67        }
68    };
69
70    Ok(LiteralValue {
71        value: kind,
72        lemma_type: Arc::clone(lemma_type),
73    })
74}
75
76fn measure_from_unit_map(
77    map: &BTreeMap<String, String>,
78    lemma_type: &LemmaType,
79) -> Result<ValueKind, String> {
80    if map.is_empty() {
81        return Err("measure input map must contain at least one unit key".to_string());
82    }
83    if lemma_type
84        .measure_unit_names()
85        .is_none_or(|names| names.is_empty())
86    {
87        unreachable!("BUG: measure type has no units at data input");
88    }
89
90    let mut kinds: Vec<ValueKind> = Vec::with_capacity(map.len());
91    for (unit_name, mag_str) in map {
92        let magnitude = Decimal::from_str(mag_str.trim())
93            .map_err(|error| format!("invalid decimal '{mag_str}': {error}"))?;
94        kinds.push(number_with_unit_to_value_kind(
95            magnitude, unit_name, lemma_type,
96        )?);
97    }
98
99    let first = kinds.first().expect("BUG: map non-empty");
100    let ValueKind::Measure(first_magnitude, first_signature) = first else {
101        return Err("expected measure value".to_string());
102    };
103    if first_signature.len() != 1 || first_signature[0].1 != 1 {
104        return Err(
105            "measure map produced a compound signature; use a convenience string instead"
106                .to_string(),
107        );
108    }
109    for kind in kinds.iter().skip(1) {
110        let ValueKind::Measure(magnitude, signature) = kind else {
111            return Err("expected measure value".to_string());
112        };
113        if signature.len() != 1 || signature[0].1 != 1 {
114            return Err(
115                "measure map produced a compound signature; use a convenience string instead"
116                    .to_string(),
117            );
118        }
119        if magnitude != first_magnitude {
120            return Err(
121                "measure unit map values disagree when converted to a common basis".to_string(),
122            );
123        }
124    }
125    Ok(first.clone())
126}
127
128fn ratio_from_unit_map(
129    map: &BTreeMap<String, String>,
130    lemma_type: &LemmaType,
131) -> Result<ValueKind, String> {
132    if map.is_empty() {
133        return Err("ratio input map must contain at least one unit key".to_string());
134    }
135    match &lemma_type.specifications {
136        TypeSpecification::Ratio { units, .. } if !units.is_empty() => {}
137        _ => unreachable!("BUG: ratio type has no units at data input"),
138    }
139
140    let mut kinds: Vec<ValueKind> = Vec::with_capacity(map.len());
141    for (unit_name, mag_str) in map {
142        let magnitude = Decimal::from_str(mag_str.trim())
143            .map_err(|error| format!("invalid decimal '{mag_str}': {error}"))?;
144        kinds.push(number_with_unit_to_value_kind(
145            magnitude, unit_name, lemma_type,
146        )?);
147    }
148
149    let first = kinds.first().expect("BUG: map non-empty");
150    let ValueKind::Ratio(first_canonical, first_unit) = first else {
151        return Err("expected ratio value".to_string());
152    };
153    for kind in kinds.iter().skip(1) {
154        let ValueKind::Ratio(canonical, _) = kind else {
155            return Err("expected ratio value".to_string());
156        };
157        if canonical != first_canonical {
158            return Err(
159                "ratio unit map values disagree when converted to a common basis".to_string(),
160            );
161        }
162    }
163    Ok(ValueKind::Ratio(
164        first_canonical.clone(),
165        first_unit.clone(),
166    ))
167}
168
169fn value_kind_tag_for_type(spec: &TypeSpecification) -> &'static str {
170    match spec {
171        TypeSpecification::Boolean { .. } => "boolean",
172        TypeSpecification::Measure { .. } => "measure",
173        TypeSpecification::Number { .. } => "number",
174        TypeSpecification::NumberRange { .. }
175        | TypeSpecification::MeasureRange { .. }
176        | TypeSpecification::DateRange { .. }
177        | TypeSpecification::TimeRange { .. }
178        | TypeSpecification::RatioRange { .. } => "range",
179        TypeSpecification::Ratio { .. } => "ratio",
180        TypeSpecification::Text { .. } => "text",
181        TypeSpecification::Date { .. } => "date",
182        TypeSpecification::Time { .. } => "time",
183        TypeSpecification::Veto { .. } => "veto",
184        TypeSpecification::Undetermined => "undetermined",
185    }
186}
187
188/// User-provided data values resolved against a plan's type declarations.
189///
190/// Lightweight and cheap to construct — no plan cloning required. The
191/// [`ExecutionPlan`] stays immutable; callers pass `(&ExecutionPlan, &DataOverlay)`
192/// to evaluation and show paths.
193#[derive(Debug, Clone, Default)]
194pub struct DataOverlay {
195    /// Caller bindings: successful literals or Veto (bad override) per Data.
196    pub bindings: HashMap<DataPath, OperationResult>,
197    /// Input keys that did not match any plan Data (including Import aliases).
198    pub ignored_unknown: Vec<String>,
199}
200
201impl DataOverlay {
202    /// Parse and validate caller-supplied values against the plan's data declarations.
203    ///
204    /// Unknown keys and Import aliases are ignored (recorded in [`Self::ignored_unknown`]).
205    /// Parse, constraint, options, decimals, and input-oversize failures bind that Data
206    /// as [`OperationResult::Veto`]; evaluation still runs. Duplicate canonical keys Error.
207    pub fn resolve(
208        plan: &ExecutionPlan,
209        raw_values: HashMap<String, DataValueInput>,
210        limits: &ResourceLimits,
211    ) -> Result<Self, Error> {
212        let mut overlay = Self::default();
213        let mut seen_canonical = HashSet::with_capacity(raw_values.len());
214        let mut by_input_key: HashMap<String, &DataPath> = HashMap::with_capacity(plan.data.len());
215        for path in plan.data.keys() {
216            by_input_key.insert(path.input_key(), path);
217        }
218
219        for (name, raw_value) in raw_values {
220            let canonical = crate::parsing::ast::ascii_lowercase_logical_name(name.clone());
221            if !seen_canonical.insert(canonical.clone()) {
222                return Err(Error::request(
223                    format!("Duplicate data key '{canonical}'"),
224                    Some("Data keys are case-insensitive; remove the duplicate"),
225                ));
226            }
227
228            let Some(data_path) = by_input_key.get(canonical.as_str()) else {
229                overlay.ignored_unknown.push(name);
230                continue;
231            };
232            let data_path = (*data_path).clone();
233
234            let data_definition = plan
235                .data
236                .get(&data_path)
237                .expect("BUG: data_path was just resolved from plan.data, must exist");
238
239            let data_source = data_definition.source().clone();
240            let type_arc = match data_definition {
241                DataDefinition::TypeDeclaration { resolved_type, .. }
242                | DataDefinition::Reference { resolved_type, .. } => Arc::clone(resolved_type),
243                DataDefinition::Value { value, .. } => Arc::clone(&value.lemma_type),
244                DataDefinition::Import { .. } => {
245                    overlay.ignored_unknown.push(name);
246                    continue;
247                }
248            };
249
250            let literal_value = match parse_data_value(&raw_value, &type_arc, &data_source) {
251                Ok(value) => value,
252                Err(error) => {
253                    overlay.bindings.insert(
254                        data_path,
255                        OperationResult::Veto(VetoType::computation(error.message().to_string())),
256                    );
257                    continue;
258                }
259            };
260
261            let size = literal_value.byte_size();
262            if size > limits.max_data_value_bytes {
263                overlay.bindings.insert(
264                    data_path,
265                    OperationResult::Veto(VetoType::computation(format!(
266                        "max_data_value_bytes (limit: {}, actual: {})",
267                        limits.max_data_value_bytes, size
268                    ))),
269                );
270                continue;
271            }
272
273            if let Err(message) = validate_value_against_type(
274                type_arc.as_ref(),
275                &literal_value,
276                plan.expression_unit_index(),
277            ) {
278                overlay.bindings.insert(
279                    data_path,
280                    OperationResult::Veto(VetoType::computation(message)),
281                );
282                continue;
283            }
284
285            overlay
286                .bindings
287                .insert(data_path, OperationResult::from_literal(literal_value));
288        }
289
290        Ok(overlay)
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use crate::computation::rational::{decimal_to_rational, rational_new, rational_one};
298    use crate::planning::semantics::{
299        primitive_number_arc, MeasureUnit, MeasureUnits, RatioUnit, RatioUnits, TypeExtends,
300    };
301
302    fn dummy_source() -> Source {
303        Source::new(
304            crate::parsing::source::SourceType::Volatile,
305            crate::planning::semantics::Span {
306                start: 0,
307                end: 0,
308                line: 1,
309                col: 1,
310            },
311        )
312    }
313
314    fn mass_measure_type() -> Arc<LemmaType> {
315        Arc::new(LemmaType::new(
316            "Mass".to_string(),
317            TypeSpecification::Measure {
318                minimum: None,
319                maximum: None,
320                decimals: None,
321                units: MeasureUnits::from(vec![
322                    MeasureUnit {
323                        name: "kilogram".to_string(),
324                        factor: rational_one(),
325                        derived_measure_factors: Vec::new(),
326                        decomposition: crate::literals::BaseMeasureVector::new(),
327                        minimum: None,
328                        maximum: None,
329                        suggestion_magnitude: None,
330                    },
331                    MeasureUnit {
332                        name: "gram".to_string(),
333                        factor: decimal_to_rational(Decimal::new(1, 3)).expect("factor"),
334                        derived_measure_factors: Vec::new(),
335                        decomposition: crate::literals::BaseMeasureVector::new(),
336                        minimum: None,
337                        maximum: None,
338                        suggestion_magnitude: None,
339                    },
340                ]),
341                traits: Vec::new(),
342                decomposition: None,
343                help: String::new(),
344            },
345            TypeExtends::Primitive,
346        ))
347    }
348
349    fn ratio_with_percent_type() -> Arc<LemmaType> {
350        Arc::new(LemmaType::new(
351            "Rate".to_string(),
352            TypeSpecification::Ratio {
353                minimum: None,
354                maximum: None,
355                decimals: None,
356                units: RatioUnits::from(vec![
357                    RatioUnit {
358                        name: "percent".to_string(),
359                        value: decimal_to_rational(Decimal::new(100, 0)).expect("factor"),
360                        minimum: None,
361                        maximum: None,
362                        suggestion_magnitude: None,
363                    },
364                    RatioUnit {
365                        name: "fraction".to_string(),
366                        value: rational_one(),
367                        minimum: None,
368                        maximum: None,
369                        suggestion_magnitude: None,
370                    },
371                ]),
372                help: String::new(),
373            },
374            TypeExtends::Primitive,
375        ))
376    }
377
378    #[test]
379    fn convenience_string_still_works() {
380        let ty = primitive_number_arc();
381        let lit = parse_data_value(
382            &DataValueInput::Convenience("42".to_string()),
383            ty,
384            &dummy_source(),
385        )
386        .unwrap();
387        assert!(matches!(lit.value, ValueKind::Number(_)));
388    }
389
390    #[test]
391    fn measure_map_agreeing_units_canonicalize() {
392        let ty = mass_measure_type();
393        let mut map = BTreeMap::new();
394        map.insert("kilogram".to_string(), "2".to_string());
395        map.insert("gram".to_string(), "2000".to_string());
396        let lit = parse_data_value(&DataValueInput::MeasureMap(map), &ty, &dummy_source()).unwrap();
397        let ValueKind::Measure(magnitude, signature) = &lit.value else {
398            panic!("expected measure");
399        };
400        assert_eq!(magnitude, &rational_new(2, 1));
401        assert_eq!(signature.len(), 1);
402        assert_eq!(signature[0].1, 1);
403    }
404
405    #[test]
406    fn measure_map_disagreeing_units_rejected() {
407        let ty = mass_measure_type();
408        let mut map = BTreeMap::new();
409        map.insert("kilogram".to_string(), "2".to_string());
410        map.insert("gram".to_string(), "3000".to_string());
411        let err =
412            parse_data_value(&DataValueInput::MeasureMap(map), &ty, &dummy_source()).unwrap_err();
413        assert!(err.message().contains("disagree"));
414    }
415
416    #[test]
417    fn ratio_map_percent_and_fraction_agree() {
418        let ty = ratio_with_percent_type();
419        let mut map = BTreeMap::new();
420        map.insert("percent".to_string(), "10".to_string());
421        map.insert("fraction".to_string(), "0.1".to_string());
422        let lit = parse_data_value(&DataValueInput::RatioMap(map), &ty, &dummy_source()).unwrap();
423        let ValueKind::Ratio(canonical, unit) = &lit.value else {
424            panic!("expected ratio");
425        };
426        assert_eq!(
427            *canonical,
428            decimal_to_rational(Decimal::new(1, 1)).expect("canonical")
429        );
430        assert!(unit.is_some());
431    }
432}