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 kind = parser_value_to_value_kind(&parsed, type_spec).map_err(to_err)?;
180 let binding = match binding_unit_from_parser_value(&parsed) {
181 Some(written) => Some(
182 resolve_passed_unit(&written, lemma_type.as_ref(), unit_index, named_types)
183 .map_err(to_err)?,
184 ),
185 None => None,
186 };
187 (kind, binding)
188 }
189 (RunDataValue::Boolean(b), TypeSpecification::Boolean { .. }) => {
190 (ValueKind::Boolean(*b), None)
191 }
192 (RunDataValue::Boolean(_), _) => {
193 return Err(to_err(format!(
194 "boolean input is only valid for boolean data, not {}",
195 type_spec
196 )));
197 }
198 (RunDataValue::MeasureMap(map), TypeSpecification::Measure { .. }) => {
199 let (kind, binding) =
200 measure_from_unit_map(map, lemma_type.as_ref(), unit_index, named_types)
201 .map_err(to_err)?;
202 (kind, binding)
203 }
204 (
205 RunDataValue::MeasureMap(map) | RunDataValue::RatioMap(map),
206 TypeSpecification::Ratio { .. },
207 ) => {
208 let (kind, binding) =
209 ratio_from_unit_map(map, lemma_type.as_ref(), unit_index, named_types)
210 .map_err(to_err)?;
211 (kind, binding)
212 }
213 (RunDataValue::MeasureMap(_), _) => {
214 return Err(to_err(format!(
215 "measure unit map is only valid for measure data, not {}",
216 type_spec
217 )));
218 }
219 (RunDataValue::RatioMap(_), _) => {
220 return Err(to_err(format!(
221 "ratio unit map is only valid for ratio data, not {}",
222 type_spec
223 )));
224 }
225 };
226
227 let typed_type = match binding_unit {
228 Some(unit) => Arc::new(lemma_type.as_ref().clone().with_measure_binding_unit(unit)),
229 None => Arc::clone(lemma_type),
230 };
231 Ok(TypedLiteral {
232 value: kind,
233 lemma_type: typed_type,
234 })
235}
236
237fn binding_unit_from_parser_value(value: &crate::parsing::ast::Value) -> Option<String> {
238 use crate::parsing::ast::Value;
239 match value {
240 Value::NumberWithUnit(_, unit) => Some(unit.clone()),
241 Value::Range(left, right) => match (left.as_ref(), right.as_ref()) {
242 (Value::NumberWithUnit(_, left_unit), Value::NumberWithUnit(_, right_unit))
243 if left_unit == right_unit =>
244 {
245 Some(left_unit.clone())
246 }
247 _ => None,
248 },
249 _ => None,
250 }
251}
252
253fn resolve_passed_unit(
259 written: &str,
260 lemma_type: &LemmaType,
261 unit_index: &UnitIndex,
262 named_types: &IndexMap<String, Arc<LemmaType>>,
263) -> Result<String, String> {
264 match unit_index.resolve_with_named_types(written, named_types) {
265 Ok((bare, _)) => {
266 if unit_name_declared_on_type(lemma_type, &bare)
267 || range_element_declares_unit(lemma_type, &bare)
268 {
269 Ok(bare)
270 } else {
271 Err(format!(
272 "Unit '{written}' resolves to '{bare}', which is not declared on type {}",
273 lemma_type.name()
274 ))
275 }
276 }
277 Err(index_err) => {
278 if unit_name_declared_on_type(lemma_type, written)
279 || range_element_declares_unit(lemma_type, written)
280 {
281 Ok(written.to_string())
282 } else {
283 Err(index_err)
284 }
285 }
286 }
287}
288
289fn range_element_declares_unit(lemma_type: &LemmaType, unit_name: &str) -> bool {
290 let Some(element) = lemma_type.specifications.element_from_range() else {
291 return false;
292 };
293 unit_name_declared_on_type(&LemmaType::primitive(element), unit_name)
294}
295
296fn unit_name_declared_on_type(lemma_type: &LemmaType, unit_name: &str) -> bool {
297 match &lemma_type.specifications {
298 TypeSpecification::Measure { units, .. } => units.get(unit_name).is_ok(),
299 TypeSpecification::Ratio { units, .. } => units.get(unit_name).is_ok(),
300 _ => false,
301 }
302}
303
304fn measure_from_unit_map(
305 map: &BTreeMap<String, Decimal>,
306 lemma_type: &LemmaType,
307 unit_index: &UnitIndex,
308 named_types: &IndexMap<String, Arc<LemmaType>>,
309) -> Result<(ValueKind, Option<String>), String> {
310 if map.is_empty() {
311 return Err("measure input map must contain at least one unit key".to_string());
312 }
313 if lemma_type
314 .measure_unit_names()
315 .is_none_or(|names| names.is_empty())
316 {
317 unreachable!("BUG: measure type has no units at data input");
318 }
319
320 let mut kinds: Vec<ValueKind> = Vec::with_capacity(map.len());
321 let mut resolved_keys: Vec<String> = Vec::with_capacity(map.len());
322 for (unit_name, magnitude) in map {
323 let bare = resolve_passed_unit(unit_name, lemma_type, unit_index, named_types)?;
324 kinds.push(number_with_unit_to_value_kind(
325 *magnitude, &bare, lemma_type,
326 )?);
327 resolved_keys.push(bare);
328 }
329
330 let first = kinds.first().expect("BUG: map non-empty");
331 let ValueKind::Measure(first_magnitude) = first else {
332 return Err("expected measure value".to_string());
333 };
334 for kind in kinds.iter().skip(1) {
335 let ValueKind::Measure(magnitude) = kind else {
336 return Err("expected measure value".to_string());
337 };
338 if magnitude != first_magnitude {
339 return Err(
340 "measure unit map values disagree when converted to a common basis".to_string(),
341 );
342 }
343 }
344 let binding = (resolved_keys.len() == 1).then(|| {
345 resolved_keys
346 .into_iter()
347 .next()
348 .expect("BUG: single resolved key")
349 });
350 Ok((first.clone(), binding))
351}
352
353fn ratio_from_unit_map(
354 map: &BTreeMap<String, Decimal>,
355 lemma_type: &LemmaType,
356 unit_index: &UnitIndex,
357 named_types: &IndexMap<String, Arc<LemmaType>>,
358) -> Result<(ValueKind, Option<String>), String> {
359 if map.is_empty() {
360 return Err("ratio input map must contain at least one unit key".to_string());
361 }
362 match &lemma_type.specifications {
363 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {}
364 _ => unreachable!("BUG: ratio type has no units at data input"),
365 }
366
367 let mut kinds: Vec<ValueKind> = Vec::with_capacity(map.len());
368 let mut resolved_keys: Vec<String> = Vec::with_capacity(map.len());
369 for (unit_name, magnitude) in map {
370 let bare = resolve_passed_unit(unit_name, lemma_type, unit_index, named_types)?;
371 kinds.push(number_with_unit_to_value_kind(
372 *magnitude, &bare, lemma_type,
373 )?);
374 resolved_keys.push(bare);
375 }
376
377 let first = kinds.first().expect("BUG: map non-empty");
378 let ValueKind::Ratio(first_canonical) = first else {
379 return Err("expected ratio value".to_string());
380 };
381 for kind in kinds.iter().skip(1) {
382 let ValueKind::Ratio(canonical) = kind else {
383 return Err("expected ratio value".to_string());
384 };
385 if canonical != first_canonical {
386 return Err(
387 "ratio unit map values disagree when converted to a common basis".to_string(),
388 );
389 }
390 }
391 let binding = (resolved_keys.len() == 1).then(|| {
392 resolved_keys
393 .into_iter()
394 .next()
395 .expect("BUG: single resolved key")
396 });
397 Ok((ValueKind::Ratio(first_canonical.clone()), binding))
398}
399
400#[derive(Debug, Clone, Default)]
406pub struct RunData {
407 pub bindings: HashMap<DataPath, OperationResult>,
409 pub ignored_unknown: Vec<String>,
411}
412
413impl RunData {
414 pub fn resolve(
420 plan: &ExecutionPlan,
421 raw_values: HashMap<String, RunDataValue>,
422 limits: &ResourceLimits,
423 ) -> Result<Self, Error> {
424 let mut run_data = Self::default();
425 let mut seen_canonical = HashSet::with_capacity(raw_values.len());
426
427 for (name, raw_value) in raw_values {
428 let canonical = crate::parsing::ast::ascii_lowercase_logical_name(name.clone());
429 if !seen_canonical.insert(canonical.clone()) {
430 return Err(Error::request(
431 format!("Duplicate data key '{canonical}'"),
432 Some("Data keys are case-insensitive; remove the duplicate"),
433 ));
434 }
435
436 let Some(data_path) = plan.input_key_index.get(canonical.as_str()) else {
437 run_data.ignored_unknown.push(name);
438 continue;
439 };
440
441 let data_definition = plan
442 .data
443 .get(data_path)
444 .expect("BUG: data_path was just resolved from plan.input_key_index, must exist");
445
446 let data_source = data_definition.source();
447 let type_arc = match data_definition {
448 DataDefinition::TypeDeclaration { resolved_type, .. }
449 | DataDefinition::Reference { resolved_type, .. }
450 | DataDefinition::Value { resolved_type, .. } => Arc::clone(resolved_type),
451 DataDefinition::Import { .. } => {
452 run_data.ignored_unknown.push(name);
453 continue;
454 }
455 };
456
457 let input_key = canonical;
458
459 if raw_value.is_empty() && type_arc.empty_runtime_input_vetoes() {
460 run_data.bindings.insert(
461 data_path.clone(),
462 OperationResult::Veto(VetoType::computation(
463 type_arc.data_veto_message(&input_key, "cannot be empty."),
464 )),
465 );
466 continue;
467 }
468
469 let typed = match parse_data_value(
470 &raw_value,
471 &type_arc,
472 data_source,
473 plan.expression_unit_index(),
474 &plan.resolved_types.resolved,
475 ) {
476 Ok(value) => value,
477 Err(error) => {
478 run_data.bindings.insert(
479 data_path.clone(),
480 OperationResult::Veto(VetoType::computation(
481 type_arc.data_veto_message(&input_key, error.message()),
482 )),
483 );
484 continue;
485 }
486 };
487
488 let literal_value = LiteralValue {
489 value: typed.value.clone(),
490 };
491 let size = literal_value.byte_size();
492 if size > limits.max_data_value_bytes {
493 run_data.bindings.insert(
494 data_path.clone(),
495 OperationResult::Veto(VetoType::computation(
496 type_arc.data_veto_message(&input_key, "exceeds the size limit."),
497 )),
498 );
499 continue;
500 }
501
502 if let Err(message) = validate_value_against_type(
503 typed.lemma_type.as_ref(),
504 &literal_value,
505 plan.expression_unit_index(),
506 ) {
507 run_data.bindings.insert(
508 data_path.clone(),
509 OperationResult::Veto(VetoType::computation(
510 type_arc.data_veto_message(&input_key, &message),
511 )),
512 );
513 continue;
514 }
515
516 run_data.bindings.insert(
517 data_path.clone(),
518 OperationResult::from_bound(crate::planning::semantics::BoundValueKind {
519 value: typed.value.clone(),
520 measure_binding_unit: typed
521 .lemma_type
522 .measure_binding_unit
523 .as_deref()
524 .map(std::sync::Arc::from),
525 }),
526 );
527 }
528
529 Ok(run_data)
530 }
531}
532
533#[cfg(test)]
534mod tests {
535 use super::*;
536 use crate::computation::rational::{decimal_to_rational, rational_new, rational_one};
537 use crate::planning::semantics::{
538 primitive_number_arc, MeasureUnit, MeasureUnits, RatioUnit, RatioUnits, TypeExtends,
539 };
540
541 fn dummy_source() -> Source {
542 Source::new(
543 crate::parsing::source::SourceType::Volatile,
544 crate::parsing::ast::Span {
545 start: 0,
546 end: 0,
547 line: 1,
548 col: 1,
549 },
550 )
551 }
552
553 fn mass_measure_type() -> Arc<LemmaType> {
554 Arc::new(LemmaType::new(
555 "Mass".to_string(),
556 TypeSpecification::Measure {
557 minimum: None,
558 maximum: None,
559 decimals: None,
560 units: MeasureUnits::from(vec![
561 MeasureUnit {
562 name: "kilogram".to_string(),
563 factor: rational_one(),
564 derived_measure_factors: Vec::new(),
565 decomposition: crate::literals::BaseMeasureVector::new(),
566 minimum: None,
567 maximum: None,
568 suggestion_magnitude: None,
569 },
570 MeasureUnit {
571 name: "gram".to_string(),
572 factor: decimal_to_rational(Decimal::new(1, 3)).expect("factor"),
573 derived_measure_factors: Vec::new(),
574 decomposition: crate::literals::BaseMeasureVector::new(),
575 minimum: None,
576 maximum: None,
577 suggestion_magnitude: None,
578 },
579 ]),
580 traits: Vec::new(),
581 decomposition: None,
582 help: String::new(),
583 },
584 TypeExtends::Primitive,
585 ))
586 }
587
588 fn ratio_with_percent_type() -> Arc<LemmaType> {
589 Arc::new(LemmaType::new(
590 "Rate".to_string(),
591 TypeSpecification::Ratio {
592 minimum: None,
593 maximum: None,
594 decimals: None,
595 units: RatioUnits::from(vec![
596 RatioUnit {
597 name: "percent".to_string(),
598 value: decimal_to_rational(Decimal::new(100, 0)).expect("factor"),
599 minimum: None,
600 maximum: None,
601 suggestion_magnitude: None,
602 },
603 RatioUnit {
604 name: "fraction".to_string(),
605 value: rational_one(),
606 minimum: None,
607 maximum: None,
608 suggestion_magnitude: None,
609 },
610 ]),
611 help: String::new(),
612 },
613 TypeExtends::Primitive,
614 ))
615 }
616
617 #[test]
618 fn string_input_parsed_against_type() {
619 let ty = primitive_number_arc();
620 let lit = parse_data_value(
621 &RunDataValue::String("42".to_string()),
622 ty,
623 &dummy_source(),
624 &UnitIndex::new(),
625 &IndexMap::new(),
626 )
627 .unwrap();
628 assert!(matches!(lit.value, ValueKind::Number(_)));
629 }
630
631 #[test]
632 fn measure_map_agreeing_units_canonicalize() {
633 let ty = mass_measure_type();
634 let mut map = BTreeMap::new();
635 map.insert("kilogram".to_string(), Decimal::from(2));
636 map.insert("gram".to_string(), Decimal::from(2000));
637 let lit = parse_data_value(
638 &RunDataValue::MeasureMap(map),
639 &ty,
640 &dummy_source(),
641 &UnitIndex::new(),
642 &IndexMap::new(),
643 )
644 .unwrap();
645 let ValueKind::Measure(magnitude) = &lit.value else {
646 panic!("expected measure");
647 };
648 assert_eq!(magnitude, &rational_new(2, 1));
649 let signature = ty.measure_runtime_signature();
650 assert_eq!(signature.len(), 1);
651 assert_eq!(signature[0].1, 1);
652 }
653
654 #[test]
655 fn measure_map_disagreeing_units_rejected() {
656 let ty = mass_measure_type();
657 let mut map = BTreeMap::new();
658 map.insert("kilogram".to_string(), Decimal::from(2));
659 map.insert("gram".to_string(), Decimal::from(3000));
660 let err = parse_data_value(
661 &RunDataValue::MeasureMap(map),
662 &ty,
663 &dummy_source(),
664 &UnitIndex::new(),
665 &IndexMap::new(),
666 )
667 .unwrap_err();
668 assert!(err.message().contains("disagree"));
669 }
670
671 #[test]
672 fn ratio_map_percent_and_fraction_agree() {
673 let ty = ratio_with_percent_type();
674 let mut map = BTreeMap::new();
675 map.insert("percent".to_string(), Decimal::from(10));
676 map.insert("fraction".to_string(), Decimal::new(1, 1));
677 let lit = parse_data_value(
678 &RunDataValue::RatioMap(map),
679 &ty,
680 &dummy_source(),
681 &UnitIndex::new(),
682 &IndexMap::new(),
683 )
684 .unwrap();
685 let ValueKind::Ratio(canonical) = &lit.value else {
686 panic!("expected ratio");
687 };
688 assert_eq!(
689 *canonical,
690 decimal_to_rational(Decimal::new(1, 1)).expect("canonical")
691 );
692 assert!(ty.ratio_primary_unit().is_some());
693 }
694
695 #[test]
696 fn run_data_value_from_json_null_is_omitted() {
697 assert_eq!(
698 run_data_value_from_json_value(serde_json::Value::Null).expect("null is omit"),
699 None
700 );
701 }
702
703 #[test]
704 fn parse_run_data_object_null_field_is_omitted() {
705 let only_null = Some(serde_json::json!({ "x": null }));
706 let empty = parse_run_data_object(&only_null).expect("null field omits");
707 assert!(empty.is_empty(), "got: {empty:?}");
708
709 let mixed = Some(serde_json::json!({ "x": null, "y": "1" }));
710 let map = parse_run_data_object(&mixed).expect("null field omits");
711 assert_eq!(map.len(), 1);
712 assert_eq!(map.get("y").map(String::as_str), Some("1"));
713 assert!(!map.contains_key("x"));
714 }
715}