1use crate::evaluation::operations::{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 DataValueInput {
17 Convenience(String),
18 Boolean(bool),
19 MeasureMap(BTreeMap<String, String>),
20 RatioMap(BTreeMap<String, String>),
21}
22
23impl DataValueInput {
24 pub fn convenience(value: impl Into<String>) -> Self {
25 Self::Convenience(value.into())
26 }
27}
28
29pub fn parse_data_value(
30 input: &DataValueInput,
31 lemma_type: &Arc<LemmaType>,
32 source: &Source,
33) -> Result<LiteralValue, Error> {
34 let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
35 let type_spec = &lemma_type.specifications;
36
37 let kind = match (input, type_spec) {
38 (DataValueInput::Convenience(s), _) => {
39 let parsed = parse_value_from_string(s, type_spec, source)?;
40 parser_value_to_value_kind(&parsed, type_spec).map_err(to_err)?
41 }
42 (DataValueInput::Boolean(b), TypeSpecification::Boolean { .. }) => ValueKind::Boolean(*b),
43 (DataValueInput::Boolean(_), _) => {
44 return Err(to_err(format!(
45 "boolean input is only valid for boolean data, not {}",
46 value_kind_tag_for_type(type_spec)
47 )));
48 }
49 (DataValueInput::MeasureMap(map), TypeSpecification::Measure { .. }) => {
50 measure_from_unit_map(map, lemma_type.as_ref()).map_err(to_err)?
51 }
52 (
53 DataValueInput::MeasureMap(map) | DataValueInput::RatioMap(map),
54 TypeSpecification::Ratio { .. },
55 ) => ratio_from_unit_map(map, lemma_type.as_ref()).map_err(to_err)?,
56 (DataValueInput::MeasureMap(_), _) => {
57 return Err(to_err(format!(
58 "measure unit map is only valid for measure data, not {}",
59 value_kind_tag_for_type(type_spec)
60 )));
61 }
62 (DataValueInput::RatioMap(_), _) => {
63 return Err(to_err(format!(
64 "ratio unit map is only valid for ratio data, not {}",
65 value_kind_tag_for_type(type_spec)
66 )));
67 }
68 };
69
70 Ok(LiteralValue {
71 value: kind,
72 lemma_type: Arc::clone(lemma_type),
73 })
74}
75
76fn measure_from_unit_map(
77 map: &BTreeMap<String, String>,
78 lemma_type: &LemmaType,
79) -> Result<ValueKind, String> {
80 if map.is_empty() {
81 return Err("measure input map must contain at least one unit key".to_string());
82 }
83 if lemma_type
84 .measure_unit_names()
85 .is_none_or(|names| names.is_empty())
86 {
87 unreachable!("BUG: measure type has no units at data input");
88 }
89
90 let mut kinds: Vec<ValueKind> = Vec::with_capacity(map.len());
91 for (unit_name, mag_str) in map {
92 let magnitude = Decimal::from_str(mag_str.trim())
93 .map_err(|error| format!("invalid decimal '{mag_str}': {error}"))?;
94 kinds.push(number_with_unit_to_value_kind(
95 magnitude, unit_name, lemma_type,
96 )?);
97 }
98
99 let first = kinds.first().expect("BUG: map non-empty");
100 let ValueKind::Measure(first_magnitude, first_signature) = first else {
101 return Err("expected measure value".to_string());
102 };
103 if first_signature.len() != 1 || first_signature[0].1 != 1 {
104 return Err(
105 "measure map produced a compound signature; use a convenience string instead"
106 .to_string(),
107 );
108 }
109 for kind in kinds.iter().skip(1) {
110 let ValueKind::Measure(magnitude, signature) = kind else {
111 return Err("expected measure value".to_string());
112 };
113 if signature.len() != 1 || signature[0].1 != 1 {
114 return Err(
115 "measure map produced a compound signature; use a convenience string instead"
116 .to_string(),
117 );
118 }
119 if magnitude != first_magnitude {
120 return Err(
121 "measure unit map values disagree when converted to a common basis".to_string(),
122 );
123 }
124 }
125 Ok(first.clone())
126}
127
128fn ratio_from_unit_map(
129 map: &BTreeMap<String, String>,
130 lemma_type: &LemmaType,
131) -> Result<ValueKind, String> {
132 if map.is_empty() {
133 return Err("ratio input map must contain at least one unit key".to_string());
134 }
135 match &lemma_type.specifications {
136 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {}
137 _ => unreachable!("BUG: ratio type has no units at data input"),
138 }
139
140 let mut kinds: Vec<ValueKind> = Vec::with_capacity(map.len());
141 for (unit_name, mag_str) in map {
142 let magnitude = Decimal::from_str(mag_str.trim())
143 .map_err(|error| format!("invalid decimal '{mag_str}': {error}"))?;
144 kinds.push(number_with_unit_to_value_kind(
145 magnitude, unit_name, lemma_type,
146 )?);
147 }
148
149 let first = kinds.first().expect("BUG: map non-empty");
150 let ValueKind::Ratio(first_canonical, first_unit) = first else {
151 return Err("expected ratio value".to_string());
152 };
153 for kind in kinds.iter().skip(1) {
154 let ValueKind::Ratio(canonical, _) = kind else {
155 return Err("expected ratio value".to_string());
156 };
157 if canonical != first_canonical {
158 return Err(
159 "ratio unit map values disagree when converted to a common basis".to_string(),
160 );
161 }
162 }
163 Ok(ValueKind::Ratio(
164 first_canonical.clone(),
165 first_unit.clone(),
166 ))
167}
168
169fn value_kind_tag_for_type(spec: &TypeSpecification) -> &'static str {
170 match spec {
171 TypeSpecification::Boolean { .. } => "boolean",
172 TypeSpecification::Measure { .. } => "measure",
173 TypeSpecification::Number { .. } => "number",
174 TypeSpecification::NumberRange { .. }
175 | TypeSpecification::MeasureRange { .. }
176 | TypeSpecification::DateRange { .. }
177 | TypeSpecification::TimeRange { .. }
178 | TypeSpecification::RatioRange { .. } => "range",
179 TypeSpecification::Ratio { .. } => "ratio",
180 TypeSpecification::Text { .. } => "text",
181 TypeSpecification::Date { .. } => "date",
182 TypeSpecification::Time { .. } => "time",
183 TypeSpecification::Veto { .. } => "veto",
184 TypeSpecification::Undetermined => "undetermined",
185 }
186}
187
188#[derive(Debug, Clone, Default)]
194pub struct DataOverlay {
195 pub bindings: HashMap<DataPath, OperationResult>,
197 pub ignored_unknown: Vec<String>,
199}
200
201impl DataOverlay {
202 pub fn resolve(
208 plan: &ExecutionPlan,
209 raw_values: HashMap<String, DataValueInput>,
210 limits: &ResourceLimits,
211 ) -> Result<Self, Error> {
212 let mut overlay = Self::default();
213 let mut seen_canonical = HashSet::with_capacity(raw_values.len());
214 let mut by_input_key: HashMap<String, &DataPath> = HashMap::with_capacity(plan.data.len());
215 for path in plan.data.keys() {
216 by_input_key.insert(path.input_key(), path);
217 }
218
219 for (name, raw_value) in raw_values {
220 let canonical = crate::parsing::ast::ascii_lowercase_logical_name(name.clone());
221 if !seen_canonical.insert(canonical.clone()) {
222 return Err(Error::request(
223 format!("Duplicate data key '{canonical}'"),
224 Some("Data keys are case-insensitive; remove the duplicate"),
225 ));
226 }
227
228 let Some(data_path) = by_input_key.get(canonical.as_str()) else {
229 overlay.ignored_unknown.push(name);
230 continue;
231 };
232 let data_path = (*data_path).clone();
233
234 let data_definition = plan
235 .data
236 .get(&data_path)
237 .expect("BUG: data_path was just resolved from plan.data, must exist");
238
239 let data_source = data_definition.source().clone();
240 let type_arc = match data_definition {
241 DataDefinition::TypeDeclaration { resolved_type, .. }
242 | DataDefinition::Reference { resolved_type, .. } => Arc::clone(resolved_type),
243 DataDefinition::Value { value, .. } => Arc::clone(&value.lemma_type),
244 DataDefinition::Import { .. } => {
245 overlay.ignored_unknown.push(name);
246 continue;
247 }
248 };
249
250 let literal_value = match parse_data_value(&raw_value, &type_arc, &data_source) {
251 Ok(value) => value,
252 Err(error) => {
253 overlay.bindings.insert(
254 data_path,
255 OperationResult::Veto(VetoType::computation(error.message().to_string())),
256 );
257 continue;
258 }
259 };
260
261 let size = literal_value.byte_size();
262 if size > limits.max_data_value_bytes {
263 overlay.bindings.insert(
264 data_path,
265 OperationResult::Veto(VetoType::computation(format!(
266 "max_data_value_bytes (limit: {}, actual: {})",
267 limits.max_data_value_bytes, size
268 ))),
269 );
270 continue;
271 }
272
273 if let Err(message) = validate_value_against_type(
274 type_arc.as_ref(),
275 &literal_value,
276 plan.expression_unit_index(),
277 ) {
278 overlay.bindings.insert(
279 data_path,
280 OperationResult::Veto(VetoType::computation(message)),
281 );
282 continue;
283 }
284
285 overlay
286 .bindings
287 .insert(data_path, OperationResult::from_literal(literal_value));
288 }
289
290 Ok(overlay)
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297 use crate::computation::rational::{decimal_to_rational, rational_new, rational_one};
298 use crate::planning::semantics::{
299 primitive_number_arc, MeasureUnit, MeasureUnits, RatioUnit, RatioUnits, TypeExtends,
300 };
301
302 fn dummy_source() -> Source {
303 Source::new(
304 crate::parsing::source::SourceType::Volatile,
305 crate::planning::semantics::Span {
306 start: 0,
307 end: 0,
308 line: 1,
309 col: 1,
310 },
311 )
312 }
313
314 fn mass_measure_type() -> Arc<LemmaType> {
315 Arc::new(LemmaType::new(
316 "Mass".to_string(),
317 TypeSpecification::Measure {
318 minimum: None,
319 maximum: None,
320 decimals: None,
321 units: MeasureUnits::from(vec![
322 MeasureUnit {
323 name: "kilogram".to_string(),
324 factor: rational_one(),
325 derived_measure_factors: Vec::new(),
326 decomposition: crate::literals::BaseMeasureVector::new(),
327 minimum: None,
328 maximum: None,
329 suggestion_magnitude: None,
330 },
331 MeasureUnit {
332 name: "gram".to_string(),
333 factor: decimal_to_rational(Decimal::new(1, 3)).expect("factor"),
334 derived_measure_factors: Vec::new(),
335 decomposition: crate::literals::BaseMeasureVector::new(),
336 minimum: None,
337 maximum: None,
338 suggestion_magnitude: None,
339 },
340 ]),
341 traits: Vec::new(),
342 decomposition: None,
343 help: String::new(),
344 },
345 TypeExtends::Primitive,
346 ))
347 }
348
349 fn ratio_with_percent_type() -> Arc<LemmaType> {
350 Arc::new(LemmaType::new(
351 "Rate".to_string(),
352 TypeSpecification::Ratio {
353 minimum: None,
354 maximum: None,
355 decimals: None,
356 units: RatioUnits::from(vec![
357 RatioUnit {
358 name: "percent".to_string(),
359 value: decimal_to_rational(Decimal::new(100, 0)).expect("factor"),
360 minimum: None,
361 maximum: None,
362 suggestion_magnitude: None,
363 },
364 RatioUnit {
365 name: "fraction".to_string(),
366 value: rational_one(),
367 minimum: None,
368 maximum: None,
369 suggestion_magnitude: None,
370 },
371 ]),
372 help: String::new(),
373 },
374 TypeExtends::Primitive,
375 ))
376 }
377
378 #[test]
379 fn convenience_string_still_works() {
380 let ty = primitive_number_arc();
381 let lit = parse_data_value(
382 &DataValueInput::Convenience("42".to_string()),
383 ty,
384 &dummy_source(),
385 )
386 .unwrap();
387 assert!(matches!(lit.value, ValueKind::Number(_)));
388 }
389
390 #[test]
391 fn measure_map_agreeing_units_canonicalize() {
392 let ty = mass_measure_type();
393 let mut map = BTreeMap::new();
394 map.insert("kilogram".to_string(), "2".to_string());
395 map.insert("gram".to_string(), "2000".to_string());
396 let lit = parse_data_value(&DataValueInput::MeasureMap(map), &ty, &dummy_source()).unwrap();
397 let ValueKind::Measure(magnitude, signature) = &lit.value else {
398 panic!("expected measure");
399 };
400 assert_eq!(magnitude, &rational_new(2, 1));
401 assert_eq!(signature.len(), 1);
402 assert_eq!(signature[0].1, 1);
403 }
404
405 #[test]
406 fn measure_map_disagreeing_units_rejected() {
407 let ty = mass_measure_type();
408 let mut map = BTreeMap::new();
409 map.insert("kilogram".to_string(), "2".to_string());
410 map.insert("gram".to_string(), "3000".to_string());
411 let err =
412 parse_data_value(&DataValueInput::MeasureMap(map), &ty, &dummy_source()).unwrap_err();
413 assert!(err.message().contains("disagree"));
414 }
415
416 #[test]
417 fn ratio_map_percent_and_fraction_agree() {
418 let ty = ratio_with_percent_type();
419 let mut map = BTreeMap::new();
420 map.insert("percent".to_string(), "10".to_string());
421 map.insert("fraction".to_string(), "0.1".to_string());
422 let lit = parse_data_value(&DataValueInput::RatioMap(map), &ty, &dummy_source()).unwrap();
423 let ValueKind::Ratio(canonical, unit) = &lit.value else {
424 panic!("expected ratio");
425 };
426 assert_eq!(
427 *canonical,
428 decimal_to_rational(Decimal::new(1, 1)).expect("canonical")
429 );
430 assert!(unit.is_some());
431 }
432}