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::planning::unit_index::UnitIndex;
9use crate::Error;
10use crate::ResourceLimits;
11use indexmap::IndexMap;
12use rust_decimal::Decimal;
13use std::collections::{BTreeMap, HashMap, HashSet};
14use std::str::FromStr;
15use std::sync::Arc;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum RunDataValue {
20 String(String),
22 Boolean(bool),
23 MeasureMap(BTreeMap<String, Decimal>),
24 RatioMap(BTreeMap<String, Decimal>),
25}
26
27impl RunDataValue {
28 pub fn string(value: impl Into<String>) -> Self {
29 Self::String(value.into())
30 }
31
32 pub(crate) fn is_empty(&self) -> bool {
33 match self {
34 Self::String(s) => s.trim().is_empty(),
35 Self::MeasureMap(map) | Self::RatioMap(map) => map.is_empty(),
36 Self::Boolean(_) => false,
37 }
38 }
39}
40
41impl From<String> for RunDataValue {
42 fn from(value: String) -> Self {
43 Self::String(value)
44 }
45}
46
47impl From<&str> for RunDataValue {
48 fn from(value: &str) -> Self {
49 Self::String(value.to_string())
50 }
51}
52
53pub fn run_data_value_from_json_value(
57 value: serde_json::Value,
58) -> Result<Option<RunDataValue>, String> {
59 match value {
60 serde_json::Value::Null => Ok(None),
61 serde_json::Value::String(s) => Ok(Some(RunDataValue::String(s))),
62 serde_json::Value::Bool(b) => Ok(Some(RunDataValue::Boolean(b))),
63 serde_json::Value::Number(n) => {
64 if n.is_i64() || n.is_u64() {
65 Ok(Some(RunDataValue::String(n.to_string())))
66 } else {
67 Err("decimal values must be passed as strings to preserve exactness".to_string())
68 }
69 }
70 serde_json::Value::Object(obj) => {
71 if obj.is_empty() {
72 return Err("data value object must not be empty".to_string());
73 }
74 if obj.len() == 2 && obj.contains_key("value") && obj.contains_key("unit") {
75 return Err(
76 "the {value, unit} object shape is not supported; use a unit map like {\"eur\": \"84\"}"
77 .to_string(),
78 );
79 }
80 if obj.values().all(|v| v.is_string()) {
81 let mut map = BTreeMap::new();
82 for (k, v) in obj {
83 let text = v.as_str().expect("BUG: object values checked as strings");
84 let decimal = Decimal::from_str(text.trim())
85 .map_err(|error| format!("invalid decimal '{text}': {error}"))?;
86 map.insert(k, decimal);
87 }
88 return Ok(Some(RunDataValue::MeasureMap(map)));
89 }
90 Err("data value object must be a unit map with string magnitudes".to_string())
91 }
92 serde_json::Value::Array(_) => Err("data value must not be an array".to_string()),
93 }
94}
95
96pub fn parse_run_data_object(
102 data: &Option<serde_json::Value>,
103) -> Result<HashMap<String, String>, String> {
104 let Some(value) = data else {
105 return Ok(HashMap::new());
106 };
107 if value.is_null() {
108 return Ok(HashMap::new());
109 }
110 let map: HashMap<String, serde_json::Value> = serde_json::from_value(value.clone())
111 .map_err(|e| format!("data must be a plain object: {e}"))?;
112 map.into_iter()
113 .filter_map(|(k, v)| match run_data_value_from_json_value(v) {
114 Ok(None) => None,
115 Ok(Some(input)) => Some(match input {
116 RunDataValue::String(s) => Ok((k, s)),
117 RunDataValue::Boolean(b) => Ok((k, b.to_string())),
118 RunDataValue::MeasureMap(m) | RunDataValue::RatioMap(m) => {
119 if m.len() == 1 {
120 let (unit, mag) = m.into_iter().next().expect("BUG: single entry map");
121 Ok((k, format!("{mag} {unit}")))
122 } else {
123 Err(format!(
124 "data value '{k}' must be a convenience string for run"
125 ))
126 }
127 }
128 }),
129 Err(e) => Some(Err(e)),
130 })
131 .collect()
132}
133
134pub fn resolve_run_rules(rules: &Option<serde_json::Value>) -> Result<Option<Vec<String>>, String> {
136 let Some(value) = rules else {
137 return Ok(None);
138 };
139 if value.is_null() {
140 return Ok(None);
141 }
142 if let Some(s) = value.as_str() {
143 let trimmed = s.trim();
144 if trimmed.is_empty() {
145 return Err("rules must not be empty".to_string());
146 }
147 return Ok(Some(vec![trimmed.to_string()]));
148 }
149 if let Some(arr) = value.as_array() {
150 if arr.is_empty() {
151 return Err("rules must not be empty".to_string());
152 }
153 let names: Vec<String> = arr
154 .iter()
155 .map(|v| {
156 v.as_str()
157 .map(|s| s.to_string())
158 .ok_or_else(|| "rules must be an array of strings".to_string())
159 })
160 .collect::<Result<_, _>>()?;
161 return Ok(Some(names));
162 }
163 Err("rules must be a string or array of strings".to_string())
164}
165
166pub fn parse_data_value(
167 input: &RunDataValue,
168 lemma_type: &Arc<LemmaType>,
169 source: &Source,
170 unit_index: &UnitIndex,
171 named_types: &IndexMap<String, Arc<LemmaType>>,
172) -> Result<TypedLiteral, Error> {
173 let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
174 let type_spec = &lemma_type.specifications;
175
176 let (kind, binding_unit) = match (input, type_spec) {
177 (RunDataValue::String(s), _) => {
178 let parsed = parse_value_from_string(s, type_spec, source)?;
179 let resolve = |unit: &str| {
180 resolve_passed_unit(unit, lemma_type.as_ref(), unit_index, named_types)
181 };
182 let kind =
183 parser_value_to_value_kind(&parsed, type_spec, Some(&resolve)).map_err(to_err)?;
184 let binding = match binding_unit_from_parser_value(&parsed) {
185 Some(written) => Some(
186 resolve_passed_unit(&written, lemma_type.as_ref(), unit_index, named_types)
187 .map_err(to_err)?,
188 ),
189 None => None,
190 };
191 (kind, binding)
192 }
193 (RunDataValue::Boolean(b), TypeSpecification::Boolean { .. }) => {
194 (ValueKind::Boolean(*b), None)
195 }
196 (RunDataValue::Boolean(_), _) => {
197 return Err(to_err(format!(
198 "boolean input is only valid for boolean data, not {}",
199 type_spec
200 )));
201 }
202 (RunDataValue::MeasureMap(map), TypeSpecification::Measure { .. }) => {
203 let (kind, binding) =
204 measure_from_unit_map(map, lemma_type.as_ref(), unit_index, named_types)
205 .map_err(to_err)?;
206 (kind, binding)
207 }
208 (
209 RunDataValue::MeasureMap(map) | RunDataValue::RatioMap(map),
210 TypeSpecification::Ratio { .. },
211 ) => {
212 let (kind, binding) =
213 ratio_from_unit_map(map, lemma_type.as_ref(), unit_index, named_types)
214 .map_err(to_err)?;
215 (kind, binding)
216 }
217 (RunDataValue::MeasureMap(_), _) => {
218 return Err(to_err(format!(
219 "measure unit map is only valid for measure data, not {}",
220 type_spec
221 )));
222 }
223 (RunDataValue::RatioMap(_), _) => {
224 return Err(to_err(format!(
225 "ratio unit map is only valid for ratio data, not {}",
226 type_spec
227 )));
228 }
229 };
230
231 let typed_type = match binding_unit {
232 Some(unit)
233 if matches!(
234 &lemma_type.specifications,
235 TypeSpecification::Measure { .. } | TypeSpecification::Ratio { .. }
236 ) =>
237 {
238 Arc::new(lemma_type.as_ref().clone().with_unit(unit))
239 }
240 Some(_) | None => Arc::clone(lemma_type),
241 };
242 Ok(TypedLiteral {
243 value: kind,
244 lemma_type: typed_type,
245 })
246}
247
248fn binding_unit_from_parser_value(value: &crate::parsing::ast::Value) -> Option<String> {
249 use crate::parsing::ast::Value;
250 match value {
251 Value::NumberWithUnit(_, unit) => Some(unit.clone()),
252 Value::Range(left, right) => match (left.as_ref(), right.as_ref()) {
253 (Value::NumberWithUnit(_, left_unit), Value::NumberWithUnit(_, right_unit))
254 if left_unit == right_unit =>
255 {
256 Some(left_unit.clone())
257 }
258 _ => None,
259 },
260 _ => None,
261 }
262}
263
264fn resolve_passed_unit(
270 written: &str,
271 lemma_type: &LemmaType,
272 unit_index: &UnitIndex,
273 named_types: &IndexMap<String, Arc<LemmaType>>,
274) -> Result<String, String> {
275 match unit_index.resolve_with_named_types(written, named_types) {
276 Ok((bare, _)) => {
277 if unit_name_declared_on_type(lemma_type, &bare)
278 || range_element_declares_unit(lemma_type, &bare)
279 {
280 Ok(bare)
281 } else {
282 Err(format!(
283 "Unit '{written}' resolves to '{bare}', which is not declared on type {}",
284 lemma_type.name()
285 ))
286 }
287 }
288 Err(index_err) => {
289 if unit_name_declared_on_type(lemma_type, written)
290 || range_element_declares_unit(lemma_type, written)
291 {
292 Ok(written.to_string())
293 } else {
294 Err(index_err)
295 }
296 }
297 }
298}
299
300fn range_element_declares_unit(lemma_type: &LemmaType, unit_name: &str) -> bool {
301 let Some(element) = lemma_type.specifications.element_from_range() else {
302 return false;
303 };
304 unit_name_declared_on_type(&LemmaType::primitive(element), unit_name)
305}
306
307fn unit_name_declared_on_type(lemma_type: &LemmaType, unit_name: &str) -> bool {
308 match &lemma_type.specifications {
309 TypeSpecification::Measure { units, .. } => units.get(unit_name).is_ok(),
310 TypeSpecification::Ratio { units, .. } => units.get(unit_name).is_ok(),
311 _ => false,
312 }
313}
314
315fn measure_from_unit_map(
316 map: &BTreeMap<String, Decimal>,
317 lemma_type: &LemmaType,
318 unit_index: &UnitIndex,
319 named_types: &IndexMap<String, Arc<LemmaType>>,
320) -> Result<(ValueKind, Option<String>), String> {
321 if map.is_empty() {
322 return Err("measure input map must contain at least one unit key".to_string());
323 }
324 if lemma_type
325 .measure_unit_names()
326 .is_none_or(|names| names.is_empty())
327 {
328 unreachable!("BUG: measure type has no units at data input");
329 }
330
331 let mut kinds: Vec<ValueKind> = Vec::with_capacity(map.len());
332 let mut resolved_keys: Vec<String> = Vec::with_capacity(map.len());
333 for (unit_name, magnitude) in map {
334 let bare = resolve_passed_unit(unit_name, lemma_type, unit_index, named_types)?;
335 kinds.push(number_with_unit_to_value_kind(
336 *magnitude, &bare, lemma_type,
337 )?);
338 resolved_keys.push(bare);
339 }
340
341 let first = kinds.first().expect("BUG: map non-empty");
342 let ValueKind::Measure(first_magnitude) = first else {
343 return Err("expected measure value".to_string());
344 };
345 for kind in kinds.iter().skip(1) {
346 let ValueKind::Measure(magnitude) = kind else {
347 return Err("expected measure value".to_string());
348 };
349 if magnitude != first_magnitude {
350 return Err(
351 "measure unit map values disagree when converted to a common basis".to_string(),
352 );
353 }
354 }
355 let binding = (resolved_keys.len() == 1).then(|| {
356 resolved_keys
357 .into_iter()
358 .next()
359 .expect("BUG: single resolved key")
360 });
361 Ok((first.clone(), binding))
362}
363
364fn ratio_from_unit_map(
365 map: &BTreeMap<String, Decimal>,
366 lemma_type: &LemmaType,
367 unit_index: &UnitIndex,
368 named_types: &IndexMap<String, Arc<LemmaType>>,
369) -> Result<(ValueKind, Option<String>), String> {
370 if map.is_empty() {
371 return Err("ratio input map must contain at least one unit key".to_string());
372 }
373 match &lemma_type.specifications {
374 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {}
375 _ => unreachable!("BUG: ratio type has no units at data input"),
376 }
377
378 let mut kinds: Vec<ValueKind> = Vec::with_capacity(map.len());
379 let mut resolved_keys: Vec<String> = Vec::with_capacity(map.len());
380 for (unit_name, magnitude) in map {
381 let bare = resolve_passed_unit(unit_name, lemma_type, unit_index, named_types)?;
382 kinds.push(number_with_unit_to_value_kind(
383 *magnitude, &bare, lemma_type,
384 )?);
385 resolved_keys.push(bare);
386 }
387
388 let first = kinds.first().expect("BUG: map non-empty");
389 let ValueKind::Ratio(first_canonical) = first else {
390 return Err("expected ratio value".to_string());
391 };
392 for kind in kinds.iter().skip(1) {
393 let ValueKind::Ratio(canonical) = kind else {
394 return Err("expected ratio value".to_string());
395 };
396 if canonical != first_canonical {
397 return Err(
398 "ratio unit map values disagree when converted to a common basis".to_string(),
399 );
400 }
401 }
402 let binding = (resolved_keys.len() == 1).then(|| {
403 resolved_keys
404 .into_iter()
405 .next()
406 .expect("BUG: single resolved key")
407 });
408 Ok((ValueKind::Ratio(first_canonical.clone()), binding))
409}
410
411#[derive(Debug, Clone, Default)]
417pub struct RunData {
418 pub bindings: HashMap<DataPath, OperationResult>,
420 pub ignored_unknown: Vec<String>,
422}
423
424impl RunData {
425 pub fn resolve(
431 plan: &ExecutionPlan,
432 raw_values: HashMap<String, RunDataValue>,
433 limits: &ResourceLimits,
434 ) -> Result<Self, Error> {
435 let mut run_data = Self::default();
436 let mut seen_canonical = HashSet::with_capacity(raw_values.len());
437
438 for (name, raw_value) in raw_values {
439 let canonical = crate::parsing::ast::ascii_lowercase_logical_name(name.clone());
440 if !seen_canonical.insert(canonical.clone()) {
441 return Err(Error::request(
442 format!("Duplicate data key '{canonical}'"),
443 Some("Data keys are case-insensitive; remove the duplicate"),
444 ));
445 }
446
447 let Some(data_path) = plan.input_key_index.get(canonical.as_str()) else {
448 run_data.ignored_unknown.push(name);
449 continue;
450 };
451
452 let data_definition = plan
453 .data
454 .get(data_path)
455 .expect("BUG: data_path was just resolved from plan.input_key_index, must exist");
456
457 let data_source = data_definition.source();
458 let type_arc = match data_definition {
459 DataDefinition::TypeDeclaration { resolved_type, .. }
460 | DataDefinition::Reference { resolved_type, .. }
461 | DataDefinition::Value { resolved_type, .. } => Arc::clone(resolved_type),
462 DataDefinition::Import { .. } => {
463 run_data.ignored_unknown.push(name);
464 continue;
465 }
466 };
467
468 let input_key = canonical;
469
470 if raw_value.is_empty() && type_arc.empty_runtime_input_vetoes() {
471 run_data.bindings.insert(
472 data_path.clone(),
473 OperationResult::Veto(VetoType::computation(
474 type_arc.data_veto_message(&input_key, "cannot be empty."),
475 )),
476 );
477 continue;
478 }
479
480 let typed = match parse_data_value(
481 &raw_value,
482 &type_arc,
483 data_source,
484 plan.expression_unit_index(),
485 &plan.resolved_types.resolved,
486 ) {
487 Ok(value) => value,
488 Err(error) => {
489 run_data.bindings.insert(
490 data_path.clone(),
491 OperationResult::Veto(VetoType::computation(
492 type_arc.data_veto_message(&input_key, error.message()),
493 )),
494 );
495 continue;
496 }
497 };
498
499 let literal_value = LiteralValue {
500 value: typed.value.clone(),
501 };
502 let size = literal_value.byte_size();
503 if size > limits.max_data_value_bytes {
504 run_data.bindings.insert(
505 data_path.clone(),
506 OperationResult::Veto(VetoType::computation(
507 type_arc.data_veto_message(&input_key, "exceeds the size limit."),
508 )),
509 );
510 continue;
511 }
512
513 if let Err(message) = validate_value_against_type(
514 typed.lemma_type.as_ref(),
515 &literal_value,
516 plan.expression_unit_index(),
517 ) {
518 run_data.bindings.insert(
519 data_path.clone(),
520 OperationResult::Veto(VetoType::computation(
521 type_arc.data_veto_message(&input_key, &message),
522 )),
523 );
524 continue;
525 }
526
527 run_data.bindings.insert(
528 data_path.clone(),
529 OperationResult::from_bound(crate::planning::semantics::BoundValueKind {
530 value: typed.value.clone(),
531 unit: typed.lemma_type.unit().map(std::sync::Arc::from),
532 }),
533 );
534 }
535
536 Ok(run_data)
537 }
538}
539
540#[cfg(test)]
541mod tests {
542 use super::*;
543 use crate::computation::rational::{decimal_to_rational, rational_new, rational_one};
544 use crate::planning::semantics::{
545 primitive_number_arc, MeasureUnit, MeasureUnits, RatioUnit, RatioUnits, TypeExtends,
546 };
547
548 fn dummy_source() -> Source {
549 Source::new(
550 crate::parsing::source::SourceType::Volatile,
551 crate::parsing::ast::Span {
552 start: 0,
553 end: 0,
554 line: 1,
555 col: 1,
556 },
557 )
558 }
559
560 fn mass_measure_type() -> Arc<LemmaType> {
561 Arc::new(LemmaType::new(
562 "Mass".to_string(),
563 TypeSpecification::Measure {
564 minimum: None,
565 maximum: None,
566 decimals: None,
567 units: MeasureUnits::from(vec![
568 MeasureUnit {
569 name: "kilogram".to_string(),
570 factor: rational_one(),
571 derived_measure_factors: Vec::new(),
572 decomposition: crate::literals::BaseMeasureVector::new(),
573 minimum: None,
574 maximum: None,
575 suggestion_magnitude: None,
576 },
577 MeasureUnit {
578 name: "gram".to_string(),
579 factor: decimal_to_rational(Decimal::new(1, 3)).expect("factor"),
580 derived_measure_factors: Vec::new(),
581 decomposition: crate::literals::BaseMeasureVector::new(),
582 minimum: None,
583 maximum: None,
584 suggestion_magnitude: None,
585 },
586 ]),
587 traits: Vec::new(),
588 decomposition: None,
589 unit: None,
590 help: String::new(),
591 },
592 TypeExtends::Primitive,
593 ))
594 }
595
596 fn ratio_with_percent_type() -> Arc<LemmaType> {
597 Arc::new(LemmaType::new(
598 "Rate".to_string(),
599 TypeSpecification::Ratio {
600 minimum: None,
601 maximum: None,
602 decimals: None,
603 units: RatioUnits::from(vec![
604 RatioUnit {
605 name: "percent".to_string(),
606 value: decimal_to_rational(Decimal::new(100, 0)).expect("factor"),
607 minimum: None,
608 maximum: None,
609 suggestion_magnitude: None,
610 },
611 RatioUnit {
612 name: "fraction".to_string(),
613 value: rational_one(),
614 minimum: None,
615 maximum: None,
616 suggestion_magnitude: None,
617 },
618 ]),
619 unit: None,
620 help: String::new(),
621 },
622 TypeExtends::Primitive,
623 ))
624 }
625
626 #[test]
627 fn string_input_parsed_against_type() {
628 let ty = primitive_number_arc();
629 let lit = parse_data_value(
630 &RunDataValue::String("42".to_string()),
631 ty,
632 &dummy_source(),
633 &UnitIndex::new(),
634 &IndexMap::new(),
635 )
636 .unwrap();
637 assert!(matches!(lit.value, ValueKind::Number(_)));
638 }
639
640 #[test]
641 fn measure_map_agreeing_units_canonicalize() {
642 let ty = mass_measure_type();
643 let mut map = BTreeMap::new();
644 map.insert("kilogram".to_string(), Decimal::from(2));
645 map.insert("gram".to_string(), Decimal::from(2000));
646 let lit = parse_data_value(
647 &RunDataValue::MeasureMap(map),
648 &ty,
649 &dummy_source(),
650 &UnitIndex::new(),
651 &IndexMap::new(),
652 )
653 .unwrap();
654 let ValueKind::Measure(magnitude) = &lit.value else {
655 panic!("expected measure");
656 };
657 assert_eq!(magnitude, &rational_new(2, 1));
658 let signature = ty.measure_runtime_signature();
659 assert_eq!(signature.len(), 1);
660 assert_eq!(signature[0].1, 1);
661 }
662
663 #[test]
664 fn measure_map_disagreeing_units_rejected() {
665 let ty = mass_measure_type();
666 let mut map = BTreeMap::new();
667 map.insert("kilogram".to_string(), Decimal::from(2));
668 map.insert("gram".to_string(), Decimal::from(3000));
669 let err = parse_data_value(
670 &RunDataValue::MeasureMap(map),
671 &ty,
672 &dummy_source(),
673 &UnitIndex::new(),
674 &IndexMap::new(),
675 )
676 .unwrap_err();
677 assert!(err.message().contains("disagree"));
678 }
679
680 #[test]
681 fn ratio_map_percent_and_fraction_agree() {
682 let ty = ratio_with_percent_type();
683 let mut map = BTreeMap::new();
684 map.insert("percent".to_string(), Decimal::from(10));
685 map.insert("fraction".to_string(), Decimal::new(1, 1));
686 let lit = parse_data_value(
687 &RunDataValue::RatioMap(map),
688 &ty,
689 &dummy_source(),
690 &UnitIndex::new(),
691 &IndexMap::new(),
692 )
693 .unwrap();
694 let ValueKind::Ratio(canonical) = &lit.value else {
695 panic!("expected ratio");
696 };
697 assert_eq!(
698 *canonical,
699 decimal_to_rational(Decimal::new(1, 1)).expect("canonical")
700 );
701 assert!(ty.ratio_primary_unit().is_some());
702 }
703
704 #[test]
705 fn run_data_value_from_json_null_is_omitted() {
706 assert_eq!(
707 run_data_value_from_json_value(serde_json::Value::Null).expect("null is omit"),
708 None
709 );
710 }
711
712 #[test]
713 fn parse_run_data_object_null_field_is_omitted() {
714 let only_null = Some(serde_json::json!({ "x": null }));
715 let empty = parse_run_data_object(&only_null).expect("null field omits");
716 assert!(empty.is_empty(), "got: {empty:?}");
717
718 let mixed = Some(serde_json::json!({ "x": null, "y": "1" }));
719 let map = parse_run_data_object(&mixed).expect("null field omits");
720 assert_eq!(map.len(), 1);
721 assert_eq!(map.get("y").map(String::as_str), Some("1"));
722 assert!(!map.contains_key("x"));
723 }
724}