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#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum RunDataValue {
18 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
39pub 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
82pub 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
119pub 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#[derive(Debug, Clone, Default)]
322pub struct RunData {
323 pub bindings: HashMap<DataPath, OperationResult>,
325 pub overlay_types: HashMap<DataPath, Arc<LemmaType>>,
327 pub ignored_unknown: Vec<String>,
329}
330
331impl RunData {
332 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}