Skip to main content

lemma/evaluation/
run_data.rs

1use crate::computation::{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 RunDataValue {
17    /// Raw string, parsed against the target type's specification by [`parse_value_from_string`].
18    String(String),
19    Boolean(bool),
20    MeasureMap(BTreeMap<String, String>),
21    RatioMap(BTreeMap<String, String>),
22}
23
24impl RunDataValue {
25    pub fn string(value: impl Into<String>) -> Self {
26        Self::String(value.into())
27    }
28
29    pub(crate) fn is_empty(&self) -> bool {
30        match self {
31            Self::String(s) => s.trim().is_empty(),
32            Self::MeasureMap(map) | Self::RatioMap(map) => map.is_empty(),
33            Self::Boolean(_) => false,
34        }
35    }
36}
37
38/// Parse one JSON value into a [`RunDataValue`] (SDK / MCP / WASM wire shape).
39pub fn run_data_value_from_json_value(value: serde_json::Value) -> Result<RunDataValue, String> {
40    match value {
41        serde_json::Value::String(s) => Ok(RunDataValue::String(s)),
42        serde_json::Value::Bool(b) => Ok(RunDataValue::Boolean(b)),
43        serde_json::Value::Number(n) => {
44            if n.is_i64() || n.is_u64() {
45                Ok(RunDataValue::String(n.to_string()))
46            } else {
47                Err("decimal values must be passed as strings to preserve exactness".to_string())
48            }
49        }
50        serde_json::Value::Object(obj) => {
51            if obj.is_empty() {
52                return Err("data value object must not be empty".to_string());
53            }
54            if obj.len() == 2 && obj.contains_key("value") && obj.contains_key("unit") {
55                return Err(
56                    "the {value, unit} object shape is not supported; use a unit map like {\"eur\": \"84\"}"
57                        .to_string(),
58                );
59            }
60            if obj.values().all(|v| v.is_string()) {
61                let map: BTreeMap<String, String> = obj
62                    .into_iter()
63                    .map(|(k, v)| {
64                        (
65                            k,
66                            v.as_str()
67                                .expect("BUG: object values checked as strings")
68                                .to_string(),
69                        )
70                    })
71                    .collect();
72                return Ok(RunDataValue::MeasureMap(map));
73            }
74            Err("data value object must be a unit map with string magnitudes".to_string())
75        }
76        serde_json::Value::Null => Err("data value must not be null".to_string()),
77        serde_json::Value::Array(_) => Err("data value must not be an array".to_string()),
78    }
79}
80
81/// Convert SDK/MCP/WASM `data` object to string map for [`crate::Engine::run`].
82///
83/// Single-entry unit maps become convenience strings (`"84 eur"`). Multi-key maps
84/// are rejected until `Engine::run` accepts typed [`RunDataValue`] directly.
85pub fn parse_run_data_object(
86    data: &Option<serde_json::Value>,
87) -> Result<HashMap<String, String>, String> {
88    let Some(value) = data else {
89        return Ok(HashMap::new());
90    };
91    if value.is_null() {
92        return Ok(HashMap::new());
93    }
94    let map: HashMap<String, serde_json::Value> = serde_json::from_value(value.clone())
95        .map_err(|e| format!("data must be a plain object: {e}"))?;
96    map.into_iter()
97        .filter(|(_, v)| !v.is_null())
98        .map(|(k, v)| {
99            let input = run_data_value_from_json_value(v)?;
100            match input {
101                RunDataValue::String(s) => Ok((k, s)),
102                RunDataValue::Boolean(b) => Ok((k, b.to_string())),
103                RunDataValue::MeasureMap(m) | RunDataValue::RatioMap(m) => {
104                    if m.len() == 1 {
105                        let (unit, mag) = m.into_iter().next().expect("BUG: single entry map");
106                        Ok((k, format!("{mag} {unit}")))
107                    } else {
108                        Err(format!(
109                            "data value '{k}' must be a convenience string for run"
110                        ))
111                    }
112                }
113            }
114        })
115        .collect()
116}
117
118/// Resolve SDK/MCP/WASM `rules` (string or string array) for [`crate::Engine::run`].
119pub fn resolve_run_rules(rules: &Option<serde_json::Value>) -> Result<Option<Vec<String>>, String> {
120    let Some(value) = rules else {
121        return Ok(None);
122    };
123    if value.is_null() {
124        return Ok(None);
125    }
126    if let Some(s) = value.as_str() {
127        let trimmed = s.trim();
128        if trimmed.is_empty() {
129            return Err("rules must not be empty".to_string());
130        }
131        return Ok(Some(vec![trimmed.to_string()]));
132    }
133    if let Some(arr) = value.as_array() {
134        if arr.is_empty() {
135            return Err("rules must not be empty".to_string());
136        }
137        let names: Vec<String> = arr
138            .iter()
139            .map(|v| {
140                v.as_str()
141                    .map(|s| s.to_string())
142                    .ok_or_else(|| "rules must be an array of strings".to_string())
143            })
144            .collect::<Result<_, _>>()?;
145        return Ok(Some(names));
146    }
147    Err("rules must be a string or array of strings".to_string())
148}
149
150pub fn parse_data_value(
151    input: &RunDataValue,
152    lemma_type: &Arc<LemmaType>,
153    source: &Source,
154) -> Result<LiteralValue, Error> {
155    let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
156    let type_spec = &lemma_type.specifications;
157
158    let kind = match (input, type_spec) {
159        (RunDataValue::String(s), _) => {
160            let parsed = parse_value_from_string(s, type_spec, source)?;
161            parser_value_to_value_kind(&parsed, type_spec).map_err(to_err)?
162        }
163        (RunDataValue::Boolean(b), TypeSpecification::Boolean { .. }) => ValueKind::Boolean(*b),
164        (RunDataValue::Boolean(_), _) => {
165            return Err(to_err(format!(
166                "boolean input is only valid for boolean data, not {}",
167                type_spec
168            )));
169        }
170        (RunDataValue::MeasureMap(map), TypeSpecification::Measure { .. }) => {
171            measure_from_unit_map(map, lemma_type.as_ref()).map_err(to_err)?
172        }
173        (
174            RunDataValue::MeasureMap(map) | RunDataValue::RatioMap(map),
175            TypeSpecification::Ratio { .. },
176        ) => ratio_from_unit_map(map, lemma_type.as_ref()).map_err(to_err)?,
177        (RunDataValue::MeasureMap(_), _) => {
178            return Err(to_err(format!(
179                "measure unit map is only valid for measure data, not {}",
180                type_spec
181            )));
182        }
183        (RunDataValue::RatioMap(_), _) => {
184            return Err(to_err(format!(
185                "ratio unit map is only valid for ratio data, not {}",
186                type_spec
187            )));
188        }
189    };
190
191    Ok(LiteralValue {
192        value: kind,
193        lemma_type: Arc::clone(lemma_type),
194    })
195}
196
197fn measure_from_unit_map(
198    map: &BTreeMap<String, String>,
199    lemma_type: &LemmaType,
200) -> Result<ValueKind, String> {
201    if map.is_empty() {
202        return Err("measure input map must contain at least one unit key".to_string());
203    }
204    if lemma_type
205        .measure_unit_names()
206        .is_none_or(|names| names.is_empty())
207    {
208        unreachable!("BUG: measure type has no units at data input");
209    }
210
211    let mut kinds: Vec<ValueKind> = Vec::with_capacity(map.len());
212    for (unit_name, mag_str) in map {
213        let magnitude = Decimal::from_str(mag_str.trim())
214            .map_err(|error| format!("invalid decimal '{mag_str}': {error}"))?;
215        kinds.push(number_with_unit_to_value_kind(
216            magnitude, unit_name, lemma_type,
217        )?);
218    }
219
220    let first = kinds.first().expect("BUG: map non-empty");
221    let ValueKind::Measure(first_magnitude, first_signature) = first else {
222        return Err("expected measure value".to_string());
223    };
224    if first_signature.len() != 1 || first_signature[0].1 != 1 {
225        return Err(
226            "measure map produced a compound signature; use a convenience string instead"
227                .to_string(),
228        );
229    }
230    for kind in kinds.iter().skip(1) {
231        let ValueKind::Measure(magnitude, signature) = kind else {
232            return Err("expected measure value".to_string());
233        };
234        if signature.len() != 1 || signature[0].1 != 1 {
235            return Err(
236                "measure map produced a compound signature; use a convenience string instead"
237                    .to_string(),
238            );
239        }
240        if magnitude != first_magnitude {
241            return Err(
242                "measure unit map values disagree when converted to a common basis".to_string(),
243            );
244        }
245    }
246    Ok(first.clone())
247}
248
249fn ratio_from_unit_map(
250    map: &BTreeMap<String, String>,
251    lemma_type: &LemmaType,
252) -> Result<ValueKind, String> {
253    if map.is_empty() {
254        return Err("ratio input map must contain at least one unit key".to_string());
255    }
256    match &lemma_type.specifications {
257        TypeSpecification::Ratio { units, .. } if !units.is_empty() => {}
258        _ => unreachable!("BUG: ratio type has no units at data input"),
259    }
260
261    let mut kinds: Vec<ValueKind> = Vec::with_capacity(map.len());
262    for (unit_name, mag_str) in map {
263        let magnitude = Decimal::from_str(mag_str.trim())
264            .map_err(|error| format!("invalid decimal '{mag_str}': {error}"))?;
265        kinds.push(number_with_unit_to_value_kind(
266            magnitude, unit_name, lemma_type,
267        )?);
268    }
269
270    let first = kinds.first().expect("BUG: map non-empty");
271    let ValueKind::Ratio(first_canonical, first_unit) = first else {
272        return Err("expected ratio value".to_string());
273    };
274    for kind in kinds.iter().skip(1) {
275        let ValueKind::Ratio(canonical, _) = kind else {
276            return Err("expected ratio value".to_string());
277        };
278        if canonical != first_canonical {
279            return Err(
280                "ratio unit map values disagree when converted to a common basis".to_string(),
281            );
282        }
283    }
284    Ok(ValueKind::Ratio(
285        first_canonical.clone(),
286        first_unit.clone(),
287    ))
288}
289
290/// User-provided data values resolved against a plan's type declarations.
291///
292/// Lightweight and cheap to construct — no plan cloning required. The
293/// [`ExecutionPlan`] stays immutable; callers pass `(&ExecutionPlan, &RunData)`
294/// to evaluation and show paths.
295#[derive(Debug, Clone, Default)]
296pub struct RunData {
297    /// Caller bindings: successful literals or Veto (bad override) per Data.
298    pub bindings: HashMap<DataPath, OperationResult>,
299    /// Input keys that did not match any plan Data (including Import aliases).
300    pub ignored_unknown: Vec<String>,
301}
302
303impl RunData {
304    /// Parse and validate caller-supplied values against the plan's data declarations.
305    ///
306    /// Unknown keys and Import aliases are ignored (recorded in [`Self::ignored_unknown`]).
307    /// Parse, constraint, options, decimals, and input-oversize failures bind that Data
308    /// as [`OperationResult::Veto`]; evaluation still runs. Duplicate canonical keys Error.
309    pub fn resolve(
310        plan: &ExecutionPlan,
311        raw_values: HashMap<String, RunDataValue>,
312        limits: &ResourceLimits,
313    ) -> Result<Self, Error> {
314        let mut run_data = Self::default();
315        let mut seen_canonical = HashSet::with_capacity(raw_values.len());
316        let mut by_input_key: HashMap<String, &DataPath> = HashMap::with_capacity(plan.data.len());
317        for path in plan.data.keys() {
318            by_input_key.insert(path.input_key(), path);
319        }
320
321        for (name, raw_value) in raw_values {
322            let canonical = crate::parsing::ast::ascii_lowercase_logical_name(name.clone());
323            if !seen_canonical.insert(canonical.clone()) {
324                return Err(Error::request(
325                    format!("Duplicate data key '{canonical}'"),
326                    Some("Data keys are case-insensitive; remove the duplicate"),
327                ));
328            }
329
330            let Some(data_path) = by_input_key.get(canonical.as_str()) else {
331                run_data.ignored_unknown.push(name);
332                continue;
333            };
334            let data_path = (*data_path).clone();
335
336            let data_definition = plan
337                .data
338                .get(&data_path)
339                .expect("BUG: data_path was just resolved from plan.data, must exist");
340
341            let data_source = data_definition.source().clone();
342            let type_arc = match data_definition {
343                DataDefinition::TypeDeclaration { resolved_type, .. }
344                | DataDefinition::Reference { resolved_type, .. } => Arc::clone(resolved_type),
345                DataDefinition::Value { value, .. } => Arc::clone(&value.lemma_type),
346                DataDefinition::Import { .. } => {
347                    run_data.ignored_unknown.push(name);
348                    continue;
349                }
350            };
351
352            let input_key = data_path.input_key();
353
354            if raw_value.is_empty() && type_arc.empty_runtime_input_vetoes() {
355                run_data.bindings.insert(
356                    data_path,
357                    OperationResult::Veto(VetoType::computation(
358                        type_arc.data_veto_message(&input_key, "cannot be empty."),
359                    )),
360                );
361                continue;
362            }
363
364            let literal_value = match parse_data_value(&raw_value, &type_arc, &data_source) {
365                Ok(value) => value,
366                Err(error) => {
367                    run_data.bindings.insert(
368                        data_path,
369                        OperationResult::Veto(VetoType::computation(
370                            type_arc.data_veto_message(&input_key, error.message()),
371                        )),
372                    );
373                    continue;
374                }
375            };
376
377            let size = literal_value.byte_size();
378            if size > limits.max_data_value_bytes {
379                run_data.bindings.insert(
380                    data_path,
381                    OperationResult::Veto(VetoType::computation(
382                        type_arc.data_veto_message(&input_key, "exceeds the size limit."),
383                    )),
384                );
385                continue;
386            }
387
388            if let Err(message) = validate_value_against_type(
389                type_arc.as_ref(),
390                &literal_value,
391                plan.expression_unit_index(),
392            ) {
393                run_data.bindings.insert(
394                    data_path,
395                    OperationResult::Veto(VetoType::computation(
396                        type_arc.data_veto_message(&input_key, &message),
397                    )),
398                );
399                continue;
400            }
401
402            run_data
403                .bindings
404                .insert(data_path, OperationResult::from_literal(literal_value));
405        }
406
407        Ok(run_data)
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414    use crate::computation::rational::{decimal_to_rational, rational_new, rational_one};
415    use crate::planning::semantics::{
416        primitive_number_arc, MeasureUnit, MeasureUnits, RatioUnit, RatioUnits, TypeExtends,
417    };
418
419    fn dummy_source() -> Source {
420        Source::new(
421            crate::parsing::source::SourceType::Volatile,
422            crate::parsing::ast::Span {
423                start: 0,
424                end: 0,
425                line: 1,
426                col: 1,
427            },
428        )
429    }
430
431    fn mass_measure_type() -> Arc<LemmaType> {
432        Arc::new(LemmaType::new(
433            "Mass".to_string(),
434            TypeSpecification::Measure {
435                minimum: None,
436                maximum: None,
437                decimals: None,
438                units: MeasureUnits::from(vec![
439                    MeasureUnit {
440                        name: "kilogram".to_string(),
441                        factor: rational_one(),
442                        derived_measure_factors: Vec::new(),
443                        decomposition: crate::literals::BaseMeasureVector::new(),
444                        minimum: None,
445                        maximum: None,
446                        suggestion_magnitude: None,
447                    },
448                    MeasureUnit {
449                        name: "gram".to_string(),
450                        factor: decimal_to_rational(Decimal::new(1, 3)).expect("factor"),
451                        derived_measure_factors: Vec::new(),
452                        decomposition: crate::literals::BaseMeasureVector::new(),
453                        minimum: None,
454                        maximum: None,
455                        suggestion_magnitude: None,
456                    },
457                ]),
458                traits: Vec::new(),
459                decomposition: None,
460                help: String::new(),
461            },
462            TypeExtends::Primitive,
463        ))
464    }
465
466    fn ratio_with_percent_type() -> Arc<LemmaType> {
467        Arc::new(LemmaType::new(
468            "Rate".to_string(),
469            TypeSpecification::Ratio {
470                minimum: None,
471                maximum: None,
472                decimals: None,
473                units: RatioUnits::from(vec![
474                    RatioUnit {
475                        name: "percent".to_string(),
476                        value: decimal_to_rational(Decimal::new(100, 0)).expect("factor"),
477                        minimum: None,
478                        maximum: None,
479                        suggestion_magnitude: None,
480                    },
481                    RatioUnit {
482                        name: "fraction".to_string(),
483                        value: rational_one(),
484                        minimum: None,
485                        maximum: None,
486                        suggestion_magnitude: None,
487                    },
488                ]),
489                help: String::new(),
490            },
491            TypeExtends::Primitive,
492        ))
493    }
494
495    #[test]
496    fn string_input_parsed_against_type() {
497        let ty = primitive_number_arc();
498        let lit =
499            parse_data_value(&RunDataValue::String("42".to_string()), ty, &dummy_source()).unwrap();
500        assert!(matches!(lit.value, ValueKind::Number(_)));
501    }
502
503    #[test]
504    fn measure_map_agreeing_units_canonicalize() {
505        let ty = mass_measure_type();
506        let mut map = BTreeMap::new();
507        map.insert("kilogram".to_string(), "2".to_string());
508        map.insert("gram".to_string(), "2000".to_string());
509        let lit = parse_data_value(&RunDataValue::MeasureMap(map), &ty, &dummy_source()).unwrap();
510        let ValueKind::Measure(magnitude, signature) = &lit.value else {
511            panic!("expected measure");
512        };
513        assert_eq!(magnitude, &rational_new(2, 1));
514        assert_eq!(signature.len(), 1);
515        assert_eq!(signature[0].1, 1);
516    }
517
518    #[test]
519    fn measure_map_disagreeing_units_rejected() {
520        let ty = mass_measure_type();
521        let mut map = BTreeMap::new();
522        map.insert("kilogram".to_string(), "2".to_string());
523        map.insert("gram".to_string(), "3000".to_string());
524        let err =
525            parse_data_value(&RunDataValue::MeasureMap(map), &ty, &dummy_source()).unwrap_err();
526        assert!(err.message().contains("disagree"));
527    }
528
529    #[test]
530    fn ratio_map_percent_and_fraction_agree() {
531        let ty = ratio_with_percent_type();
532        let mut map = BTreeMap::new();
533        map.insert("percent".to_string(), "10".to_string());
534        map.insert("fraction".to_string(), "0.1".to_string());
535        let lit = parse_data_value(&RunDataValue::RatioMap(map), &ty, &dummy_source()).unwrap();
536        let ValueKind::Ratio(canonical, unit) = &lit.value else {
537            panic!("expected ratio");
538        };
539        assert_eq!(
540            *canonical,
541            decimal_to_rational(Decimal::new(1, 1)).expect("canonical")
542        );
543        assert!(unit.is_some());
544    }
545}