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