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#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum RunDataValue {
17 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
38pub 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
81pub 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
118pub 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#[derive(Debug, Clone, Default)]
296pub struct RunData {
297 pub bindings: HashMap<DataPath, OperationResult>,
299 pub ignored_unknown: Vec<String>,
301}
302
303impl RunData {
304 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}