1use crate::api::types::LemmaType;
5use crate::api::value::RuleResultValue;
6use crate::parsing::ast::{
7 ArithmeticComputation, CalendarPeriodUnit, ComparisonComputation, ConversionTypeKind,
8 DateCalendarKind, DateRelativeKind, MathematicalComputation, RangeBound, RoundingMode,
9};
10use crate::planning::execution_plan::{
11 ExecutionPlan, Show as DomainShow, ShowBranch as DomainShowBranch,
12 ShowConversionTarget as DomainConversionTarget, ShowData as DomainShowData,
13 ShowExpression as DomainShowExpression, ShowRule as DomainShowRule,
14};
15pub use crate::planning::execution_plan::{ShowListCommand, SortDirection};
16use crate::planning::semantics::{
17 LemmaType as DomainLemmaType, PathSegment as DomainPathSegment, SpecIdentity,
18 TypeSpecification as DomainTypeSpecification,
19};
20use crate::planning::show_expression::take_operand;
21use crate::result_value::{
22 type_scoped_result_value_from_literal, RuleResultValue as DomainRuleResultValue,
23};
24use crate::Engine;
25use indexmap::IndexMap;
26use serde::{Deserialize, Serialize};
27use serde_json::{json, Map, Value as JsonValue};
28use std::collections::{BTreeMap, BTreeSet};
29use std::sync::Arc;
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct PathSegment {
35 pub uses: String,
36 #[serde(skip_serializing_if = "Option::is_none", default)]
38 pub row: Option<usize>,
39 #[serde(skip_serializing_if = "Option::is_none", default)]
40 pub repository: Option<String>,
41 pub spec: String,
42}
43
44impl From<&DomainPathSegment> for PathSegment {
45 fn from(segment: &DomainPathSegment) -> Self {
46 Self {
47 uses: segment.uses.to_string(),
48 row: segment.row,
49 repository: segment.repository.as_ref().map(|arc| arc.to_string()),
50 spec: segment.spec.to_string(),
51 }
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "snake_case")]
57pub enum ShowConversionTarget {
58 Type(ConversionTypeKind),
59 Unit { unit_name: String },
60}
61
62impl From<&DomainConversionTarget> for ShowConversionTarget {
63 fn from(target: &DomainConversionTarget) -> Self {
64 match target {
65 DomainConversionTarget::Type(kind) => Self::Type(*kind),
66 DomainConversionTarget::Unit { unit_name } => Self::Unit {
67 unit_name: unit_name.clone(),
68 },
69 }
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(tag = "type", rename_all = "snake_case")]
76pub enum ShowExpression {
77 Literal {
78 #[serde(flatten)]
79 value: Box<RuleResultValue>,
80 },
81 Data {
82 name: String,
83 },
84 Rule {
85 name: String,
86 },
87 And {
88 operands: Vec<ShowExpression>,
89 },
90 Or {
91 operands: Vec<ShowExpression>,
92 },
93 Not {
94 operand: Box<ShowExpression>,
95 },
96 Arithmetic {
97 first: Box<ShowExpression>,
98 steps: Vec<ShowArithmeticStep>,
99 },
100 Comparison {
101 op: ComparisonComputation,
102 left: Box<ShowExpression>,
103 right: Box<ShowExpression>,
104 },
105 UnitConversion {
106 operand: Box<ShowExpression>,
107 targets: Vec<ShowConversionTarget>,
108 },
109 Math {
110 op: MathematicalComputation,
111 operand: Box<ShowExpression>,
112 },
113 StepRounding {
114 mode: RoundingMode,
115 operand: Box<ShowExpression>,
116 #[serde(skip_serializing_if = "Option::is_none", default)]
117 step: Option<Box<ShowExpression>>,
118 },
119 Veto {
120 #[serde(skip_serializing_if = "Option::is_none", default)]
121 message: Option<String>,
122 },
123 Now,
124 DateRelative {
125 kind: DateRelativeKind,
126 operand: Box<ShowExpression>,
127 },
128 DateCalendar {
129 kind: DateCalendarKind,
130 unit: CalendarPeriodUnit,
131 operand: Box<ShowExpression>,
132 },
133 RangeLiteral {
134 from: Box<ShowExpression>,
135 to: Box<ShowExpression>,
136 },
137 PastFutureRange {
138 kind: DateRelativeKind,
139 operand: Box<ShowExpression>,
140 },
141 RangeContainment {
142 value: Box<ShowExpression>,
143 range: Box<ShowExpression>,
144 },
145 RangeBound {
146 bound: RangeBound,
147 operand: Box<ShowExpression>,
148 },
149 IsVeto {
150 operand: Box<ShowExpression>,
151 },
152 SpecMember {
153 base: String,
154 member: String,
155 },
156 Member {
157 base: Box<ShowExpression>,
158 member: String,
159 },
160 RowField {
161 field: String,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
163 qualifier: Option<Vec<String>>,
164 },
165 RowValue,
166 RowFieldValue {
167 field: String,
168 value: Box<ShowExpression>,
169 },
170 ListCommand {
171 list: Box<ShowExpression>,
172 command: ShowListCommand,
173 #[serde(skip_serializing_if = "Option::is_none", default)]
174 direction: Option<SortDirection>,
175 args: Vec<ShowExpression>,
176 },
177}
178
179#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181pub struct ShowArithmeticStep {
182 pub op: ArithmeticComputation,
183 pub operand: ShowExpression,
184}
185
186impl From<&DomainShowExpression> for ShowExpression {
187 fn from(expression: &DomainShowExpression) -> Self {
188 expression.fold(convert_show_node)
189 }
190}
191
192fn convert_show_node(node: &DomainShowExpression, operands: Vec<ShowExpression>) -> ShowExpression {
195 let mut operands = operands.into_iter();
196 match node {
197 DomainShowExpression::Literal(typed) => {
198 let domain_value = type_scoped_result_value_from_literal(
199 &typed.to_literal(),
200 typed.lemma_type.as_ref(),
201 )
202 .unwrap_or_else(|failure| {
203 panic!(
204 "BUG: show branch literal failed type_scoped_result_value_from_literal: {}",
205 crate::result_value::rule_result_value_failure_message(failure)
206 )
207 });
208 ShowExpression::Literal {
209 value: Box::new(RuleResultValue::from(&domain_value)),
210 }
211 }
212 DomainShowExpression::Data { name } => ShowExpression::Data { name: name.clone() },
213 DomainShowExpression::Rule { name } => ShowExpression::Rule { name: name.clone() },
214 DomainShowExpression::And { .. } => ShowExpression::And {
215 operands: operands.collect(),
216 },
217 DomainShowExpression::Or { .. } => ShowExpression::Or {
218 operands: operands.collect(),
219 },
220 DomainShowExpression::Not { .. } => ShowExpression::Not {
221 operand: Box::new(take_operand(&mut operands)),
222 },
223 DomainShowExpression::Arithmetic { steps, .. } => {
224 let first = Box::new(take_operand(&mut operands));
225 let steps = steps
226 .iter()
227 .map(|step| ShowArithmeticStep {
228 op: step.op.clone(),
229 operand: take_operand(&mut operands),
230 })
231 .collect();
232 ShowExpression::Arithmetic { first, steps }
233 }
234 DomainShowExpression::Comparison { op, .. } => {
235 let left = Box::new(take_operand(&mut operands));
236 let right = Box::new(take_operand(&mut operands));
237 ShowExpression::Comparison {
238 op: op.clone(),
239 left,
240 right,
241 }
242 }
243 DomainShowExpression::UnitConversion { targets, .. } => ShowExpression::UnitConversion {
244 operand: Box::new(take_operand(&mut operands)),
245 targets: targets.iter().map(ShowConversionTarget::from).collect(),
246 },
247 DomainShowExpression::Math { op, .. } => ShowExpression::Math {
248 op: op.clone(),
249 operand: Box::new(take_operand(&mut operands)),
250 },
251 DomainShowExpression::StepRounding { mode, step, .. } => {
252 let operand = Box::new(take_operand(&mut operands));
253 ShowExpression::StepRounding {
254 mode: *mode,
255 operand,
256 step: step.as_ref().map(|_| Box::new(take_operand(&mut operands))),
257 }
258 }
259 DomainShowExpression::Veto { message } => ShowExpression::Veto {
260 message: message.clone(),
261 },
262 DomainShowExpression::Now => ShowExpression::Now,
263 DomainShowExpression::DateRelative { kind, .. } => ShowExpression::DateRelative {
264 kind: *kind,
265 operand: Box::new(take_operand(&mut operands)),
266 },
267 DomainShowExpression::DateCalendar { kind, unit, .. } => ShowExpression::DateCalendar {
268 kind: *kind,
269 unit: *unit,
270 operand: Box::new(take_operand(&mut operands)),
271 },
272 DomainShowExpression::RangeLiteral { .. } => {
273 let from = Box::new(take_operand(&mut operands));
274 let to = Box::new(take_operand(&mut operands));
275 ShowExpression::RangeLiteral { from, to }
276 }
277 DomainShowExpression::PastFutureRange { kind, .. } => ShowExpression::PastFutureRange {
278 kind: *kind,
279 operand: Box::new(take_operand(&mut operands)),
280 },
281 DomainShowExpression::RangeContainment { .. } => {
282 let value = Box::new(take_operand(&mut operands));
283 let range = Box::new(take_operand(&mut operands));
284 ShowExpression::RangeContainment { value, range }
285 }
286 DomainShowExpression::RangeBound { bound, .. } => ShowExpression::RangeBound {
287 bound: *bound,
288 operand: Box::new(take_operand(&mut operands)),
289 },
290 DomainShowExpression::IsVeto { .. } => ShowExpression::IsVeto {
291 operand: Box::new(take_operand(&mut operands)),
292 },
293 DomainShowExpression::SpecMember { base, member } => ShowExpression::SpecMember {
294 base: base.clone(),
295 member: member.clone(),
296 },
297 DomainShowExpression::Member { member, .. } => ShowExpression::Member {
298 base: Box::new(take_operand(&mut operands)),
299 member: member.clone(),
300 },
301 DomainShowExpression::RowField { field, qualifier } => ShowExpression::RowField {
302 field: field.clone(),
303 qualifier: qualifier.clone(),
304 },
305 DomainShowExpression::RowValue => ShowExpression::RowValue,
306 DomainShowExpression::RowFieldValue { field, .. } => ShowExpression::RowFieldValue {
307 field: field.clone(),
308 value: Box::new(take_operand(&mut operands)),
309 },
310 DomainShowExpression::ListCommand {
311 command, direction, ..
312 } => {
313 let list = Box::new(take_operand(&mut operands));
314 ShowExpression::ListCommand {
315 list,
316 command: *command,
317 direction: *direction,
318 args: operands.collect(),
319 }
320 }
321 }
322}
323
324#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
325pub struct ShowBranch {
326 #[serde(skip_serializing_if = "Option::is_none", default)]
327 pub condition: Option<ShowExpression>,
328 pub result: ShowExpression,
329}
330
331impl From<&DomainShowBranch> for ShowBranch {
332 fn from(branch: &DomainShowBranch) -> Self {
333 Self {
334 condition: branch.condition.as_ref().map(ShowExpression::from),
335 result: ShowExpression::from(&branch.result),
336 }
337 }
338}
339
340#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
341pub struct ShowRule {
342 #[serde(rename = "type")]
343 pub lemma_type: LemmaType,
344 pub path: Vec<PathSegment>,
345 pub branches: Vec<ShowBranch>,
346 pub depends_on_rules: Vec<String>,
347}
348
349impl From<&DomainShowRule> for ShowRule {
350 fn from(rule: &DomainShowRule) -> Self {
351 Self {
352 lemma_type: LemmaType::from(&rule.lemma_type),
353 path: rule.path.iter().map(PathSegment::from).collect(),
354 branches: rule.branches.iter().map(ShowBranch::from).collect(),
355 depends_on_rules: rule
356 .depends_on_rules
357 .iter()
358 .map(|path| path.input_key())
359 .collect(),
360 }
361 }
362}
363
364const JSON_SCHEMA_DIALECT: &str = "https://json-schema.org/draft/2020-12/schema";
365
366const BOOLEAN_TEXT_PATTERN: &str =
368 r"^\s*(?:[Tt][Rr][Uu][Ee]|[Ff][Aa][Ll][Ss][Ee]|[Yy][Ee][Ss]|[Nn][Oo])\s*$";
369
370pub fn show_document(engine: &Engine, show: &DomainShow) -> JsonValue {
380 let consumer = match engine.plan_matching_show(show) {
381 Some(plan) => Some(plan),
382 None if !show_has_instance_list(show) => None,
383 None => panic!(
384 "BUG: Show '{}' has instance-list inputs but no matching execution plan",
385 show.spec
386 ),
387 };
388 let mut rows = RowSchemas::new(engine, consumer);
389 let mut document = Map::new();
390 document.insert("$schema".to_string(), json!(JSON_SCHEMA_DIALECT));
391 document.insert("title".to_string(), json!(show.spec));
392 if let Some(commentary) = &show.commentary {
393 document.insert("description".to_string(), json!(commentary));
394 }
395 document.extend(input_object_schema(show, &mut rows));
396 let (defs, row_field_needed_by_rules) = rows.into_defs();
397 if !defs.is_empty() {
398 document.insert("$defs".to_string(), JsonValue::Object(defs));
399 }
400 if !row_field_needed_by_rules.is_empty() {
401 document.insert(
405 "x-lemma-row-field-needed-by-rules".to_string(),
406 JsonValue::Object(row_field_needed_by_rules),
407 );
408 }
409 if let Some(repository) = &show.repository {
410 document.insert("x-lemma-repository".to_string(), json!(repository));
411 }
412 if let Some(from) = &show.effective_from {
413 document.insert("x-lemma-effective-from".to_string(), to_json(from));
414 }
415 if let Some(to) = &show.effective_to {
416 document.insert("x-lemma-effective-to".to_string(), to_json(to));
417 }
418 document.insert("x-lemma-start-line".to_string(), json!(show.start_line));
419 if let Some(source_type) = &show.source_type {
420 document.insert("x-lemma-source-type".to_string(), to_json(source_type));
421 }
422 document.insert("x-lemma-meta".to_string(), to_json(&show.meta));
423 let types: IndexMap<&String, LemmaType> = show
424 .types
425 .iter()
426 .map(|(name, lemma_type)| (name, LemmaType::from(lemma_type)))
427 .collect();
428 document.insert("x-lemma-types".to_string(), to_json(&types));
429 let rules: IndexMap<&String, ShowRule> = show
430 .rules
431 .iter()
432 .map(|(name, rule)| (name, ShowRule::from(rule)))
433 .collect();
434 document.insert("x-lemma-rules".to_string(), to_json(&rules));
435 JsonValue::Object(document)
436}
437
438fn to_json<T: Serialize>(value: &T) -> JsonValue {
439 serde_json::to_value(value).expect("BUG: Show document parts serialize to JSON")
440}
441
442fn show_has_instance_list(show: &DomainShow) -> bool {
443 show.data.values().any(|data| {
444 matches!(
445 &data.lemma_type.specifications,
446 DomainTypeSpecification::List { element, .. }
447 if matches!(&element.specifications, DomainTypeSpecification::Spec { .. })
448 )
449 })
450}
451
452fn input_object_schema(show: &DomainShow, rows: &mut RowSchemas) -> Map<String, JsonValue> {
456 let mut properties = Map::new();
457 let mut required = Vec::new();
458 for (name, data) in &show.data {
459 properties.insert(
460 name.clone(),
461 property_schema(data, rows, &data.needed_by_rules),
462 );
463 let is_list = matches!(
464 data.lemma_type.specifications,
465 DomainTypeSpecification::List { .. }
466 );
467 if data.value.is_none() && !data.needed_by_rules.is_empty() && !is_list {
468 required.push(name.clone());
469 }
470 }
471 required.sort();
472 let mut schema = Map::new();
473 schema.insert("type".to_string(), json!("object"));
474 schema.insert("additionalProperties".to_string(), json!(true));
475 schema.insert("properties".to_string(), JsonValue::Object(properties));
476 if !required.is_empty() {
477 schema.insert("required".to_string(), json!(required));
478 }
479 schema
480}
481
482fn row_object_schema(show: &DomainShow, rows: &mut RowSchemas) -> Map<String, JsonValue> {
490 let mut properties = Map::new();
491 let mut required = Vec::new();
492 for (name, lemma_type) in &show.types {
493 let stub;
494 let entry = match show.data.get(name) {
495 Some(data) => data,
496 None => {
497 stub = DomainShowData {
498 lemma_type: lemma_type.clone(),
499 path: Vec::new(),
500 value: None,
501 suggestion: None,
502 needed_by_rules: Vec::new(),
503 };
504 &stub
505 }
506 };
507 let is_list = matches!(
508 entry.lemma_type.specifications,
509 DomainTypeSpecification::List { .. }
510 );
511 if entry.value.is_none() && !is_list {
512 required.push(name.clone());
513 }
514 properties.insert(name.clone(), property_schema(entry, rows, &[]));
515 }
516 for (name, data) in &show.data {
517 if properties.contains_key(name) {
518 continue;
519 }
520 let is_list = matches!(
521 data.lemma_type.specifications,
522 DomainTypeSpecification::List { .. }
523 );
524 if data.value.is_none() && !is_list {
525 required.push(name.clone());
526 }
527 properties.insert(name.clone(), property_schema(data, rows, &[]));
528 }
529 required.sort();
530 let mut schema = Map::new();
531 schema.insert("type".to_string(), json!("object"));
532 schema.insert("additionalProperties".to_string(), json!(true));
533 schema.insert("properties".to_string(), JsonValue::Object(properties));
534 if !required.is_empty() {
535 schema.insert("required".to_string(), json!(required));
536 }
537 schema
538}
539
540fn row_property_names(show: &DomainShow) -> BTreeSet<String> {
542 let mut names: BTreeSet<String> = show.types.keys().cloned().collect();
543 names.extend(show.data.keys().cloned());
544 names
545}
546
547fn property_schema(
548 data: &DomainShowData,
549 rows: &mut RowSchemas,
550 needed_by_rules: &[String],
551) -> JsonValue {
552 let mut schema = type_schema(&data.lemma_type, rows);
553 let help = specification_help(&data.lemma_type.specifications);
554 if !help.is_empty() {
555 schema.insert("description".to_string(), json!(help));
556 }
557 if let Some(value) = &data.value {
558 if !rule_result_value_has_reference(value) {
561 schema.insert(
562 "default".to_string(),
563 input_literal(value, &data.lemma_type, rows),
564 );
565 }
566 }
567 if let Some(suggestion) = &data.suggestion {
568 schema.insert(
569 "examples".to_string(),
570 json!([input_literal(suggestion, &data.lemma_type, rows)]),
571 );
572 }
573 schema.insert(
574 "x-lemma-type".to_string(),
575 lemma_type_without_standard_keywords(&data.lemma_type),
576 );
577 let path: Vec<PathSegment> = data.path.iter().map(PathSegment::from).collect();
578 schema.insert("x-lemma-path".to_string(), to_json(&path));
579 schema.insert(
580 "x-lemma-needed-by-rules".to_string(),
581 json!(needed_by_rules),
582 );
583 if let Some(value) = &data.value {
584 schema.insert(
585 "x-lemma-value".to_string(),
586 to_json(&RuleResultValue::from(value)),
587 );
588 }
589 if let Some(suggestion) = &data.suggestion {
590 schema.insert(
591 "x-lemma-suggestion".to_string(),
592 to_json(&RuleResultValue::from(suggestion)),
593 );
594 }
595 JsonValue::Object(schema)
596}
597
598fn type_schema(lemma_type: &DomainLemmaType, rows: &mut RowSchemas) -> Map<String, JsonValue> {
601 let DomainTypeSpecification::List { element, .. } = &lemma_type.specifications else {
602 return scalar_data_schema(lemma_type);
603 };
604 let mut schema = Map::new();
605 let items = match &element.specifications {
606 DomainTypeSpecification::Spec { spec } => rows.reference(spec),
607 DomainTypeSpecification::List { .. } => {
608 unreachable!("BUG: lists do not nest; planning rejects a list element list")
609 }
610 _ => JsonValue::Object(scalar_data_schema(element)),
611 };
612 schema.insert("type".to_string(), json!("array"));
613 schema.insert("items".to_string(), items);
614 schema.insert(
615 "maxItems".to_string(),
616 json!(rows.engine.limits().max_list_rows),
617 );
618 schema
619}
620
621fn scalar_data_schema(lemma_type: &DomainLemmaType) -> Map<String, JsonValue> {
623 let specification = &lemma_type.specifications;
624 let names: Vec<&str> = match specification {
625 DomainTypeSpecification::Measure { units, .. } => {
626 units.iter().map(|unit| unit.name.as_str()).collect()
627 }
628 DomainTypeSpecification::Ratio { units, .. } => {
629 units.iter().map(|unit| unit.name.as_str()).collect()
630 }
631 _ => return scalar_type_schema(lemma_type),
632 };
633 let unit_map = json!({
634 "type": "object",
635 "minProperties": 1,
636 "maxProperties": 1,
637 "propertyNames": {"enum": names},
638 "additionalProperties": {"type": "string"}
639 });
640 let mut schema = Map::new();
641 schema.insert(
642 "oneOf".to_string(),
643 json!([JsonValue::Object(scalar_type_schema(lemma_type)), unit_map]),
644 );
645 publish_option_enum(&mut schema, lemma_type);
646 schema
647}
648
649fn scalar_type_schema(lemma_type: &DomainLemmaType) -> Map<String, JsonValue> {
650 let specification = &lemma_type.specifications;
651 let mut schema = Map::new();
652 match specification {
653 DomainTypeSpecification::Boolean { .. } => {
654 schema.insert(
655 "oneOf".to_string(),
656 json!([
657 {"type": "boolean"},
658 {"type": "string", "pattern": BOOLEAN_TEXT_PATTERN}
659 ]),
660 );
661 }
662 DomainTypeSpecification::Text { length, .. } => {
663 schema.insert("type".to_string(), json!("string"));
664 if let Some(length) = length {
665 schema.insert("minLength".to_string(), json!(length));
666 schema.insert("maxLength".to_string(), json!(length));
667 }
668 }
669 DomainTypeSpecification::Number { .. }
670 | DomainTypeSpecification::Measure { .. }
671 | DomainTypeSpecification::Ratio { .. }
672 | DomainTypeSpecification::NumberRange { .. }
673 | DomainTypeSpecification::MeasureRange { .. }
674 | DomainTypeSpecification::RatioRange { .. }
675 | DomainTypeSpecification::DateRange { .. }
676 | DomainTypeSpecification::TimeRange { .. } => {
677 schema.insert("type".to_string(), json!("string"));
678 }
679 DomainTypeSpecification::Date { .. } => {
680 schema.insert(
681 "oneOf".to_string(),
682 json!([
683 {"type": "string", "format": "date"},
684 {"type": "string", "format": "date-time"}
685 ]),
686 );
687 }
688 DomainTypeSpecification::Time { .. } => {
689 schema.insert("type".to_string(), json!("string"));
690 schema.insert("format".to_string(), json!("time"));
691 }
692 DomainTypeSpecification::List { .. } => {
693 unreachable!("BUG: list schemas are built by type_schema")
694 }
695 DomainTypeSpecification::Spec { spec } => {
696 unreachable!("BUG: spec '{spec}' is a uses alias, never a Show data entry")
697 }
698 DomainTypeSpecification::Veto { .. } => {
699 unreachable!("BUG: veto sentinel type on a Show data entry")
700 }
701 DomainTypeSpecification::Undetermined => {
702 unreachable!("BUG: undetermined sentinel type on a Show data entry")
703 }
704 }
705 publish_option_enum(&mut schema, lemma_type);
706 schema
707}
708
709fn publish_option_enum(schema: &mut Map<String, JsonValue>, lemma_type: &DomainLemmaType) {
710 if lemma_type.options.values.is_empty() {
711 return;
712 }
713 schema.insert(
714 "enum".to_string(),
715 json!(lemma_type
716 .options
717 .values
718 .iter()
719 .map(|option| lemma_type.display_bound(&option.value))
720 .collect::<Vec<_>>()),
721 );
722}
723
724fn specification_help(specification: &DomainTypeSpecification) -> String {
725 let mut leaf = specification;
726 while let DomainTypeSpecification::List { element, .. } = leaf {
727 leaf = &element.specifications;
728 }
729 match leaf {
730 DomainTypeSpecification::Boolean { help }
731 | DomainTypeSpecification::Measure { help, .. }
732 | DomainTypeSpecification::MeasureRange { help, .. }
733 | DomainTypeSpecification::Number { help, .. }
734 | DomainTypeSpecification::NumberRange { help, .. }
735 | DomainTypeSpecification::Ratio { help, .. }
736 | DomainTypeSpecification::RatioRange { help, .. }
737 | DomainTypeSpecification::Text { help, .. }
738 | DomainTypeSpecification::Date { help, .. }
739 | DomainTypeSpecification::DateRange { help, .. }
740 | DomainTypeSpecification::Time { help, .. }
741 | DomainTypeSpecification::TimeRange { help, .. } => help.clone(),
742 DomainTypeSpecification::Spec { spec } => spec.to_string(),
743 DomainTypeSpecification::List { .. } => {
744 unreachable!("BUG: list chain is unwound before the leaf match")
745 }
746 DomainTypeSpecification::Veto { .. } | DomainTypeSpecification::Undetermined => {
747 unreachable!("BUG: sentinel type on a Show data entry")
748 }
749 }
750}
751
752fn rule_result_value_has_reference(value: &DomainRuleResultValue) -> bool {
756 if value.reference.is_some() {
757 return true;
758 }
759 if let Some(fields) = &value.fields {
760 if fields.values().any(rule_result_value_has_reference) {
761 return true;
762 }
763 }
764 if let Some(list) = &value.list {
765 if list.iter().any(rule_result_value_has_reference) {
766 return true;
767 }
768 }
769 false
770}
771
772enum InputLiteralTask<'a> {
773 Visit {
774 value: &'a DomainRuleResultValue,
775 lemma_type: Arc<DomainLemmaType>,
776 },
777 CollectArray {
778 len: usize,
779 },
780 CollectObject {
781 keys: Vec<String>,
782 },
783}
784
785fn input_literal(
786 value: &DomainRuleResultValue,
787 lemma_type: &DomainLemmaType,
788 rows: &RowSchemas,
789) -> JsonValue {
790 let mut tasks = vec![InputLiteralTask::Visit {
791 value,
792 lemma_type: Arc::new(lemma_type.clone()),
793 }];
794 let mut converted: Vec<JsonValue> = Vec::new();
795 while let Some(task) = tasks.pop() {
796 match task {
797 InputLiteralTask::Visit { value, lemma_type } => match &lemma_type.specifications {
798 DomainTypeSpecification::Boolean { .. } => converted.push(JsonValue::Bool(
799 value
800 .boolean
801 .expect("BUG: boolean-typed Show value without boolean"),
802 )),
803 DomainTypeSpecification::List { element, .. } => {
804 let items = value
805 .list
806 .as_ref()
807 .expect("BUG: list-typed Show value without list");
808 tasks.push(InputLiteralTask::CollectArray { len: items.len() });
809 match &element.specifications {
810 DomainTypeSpecification::Spec { spec } => {
811 let row_show = rows.engine.show_spec(spec);
812 for row in items.iter().rev() {
813 let fields = row
814 .fields
815 .as_ref()
816 .expect("BUG: instance list Show row without fields");
817 let mut keys = Vec::with_capacity(fields.len());
818 let mut children = Vec::with_capacity(fields.len());
819 for (name, field) in fields {
820 assert!(
821 field.reference.is_none(),
822 "BUG: input_literal on Show value with reference field '{name}'"
823 );
824 let field_type = row_show
825 .types
826 .get(name)
827 .or_else(|| row_show.data.get(name).map(|d| &d.lemma_type))
828 .unwrap_or_else(|| {
829 panic!("BUG: row field '{name}' missing from Show of '{spec}'")
830 });
831 keys.push(name.clone());
832 children.push((field, Arc::new(field_type.clone())));
833 }
834 tasks.push(InputLiteralTask::CollectObject { keys });
835 for (field, field_type) in children.into_iter().rev() {
836 tasks.push(InputLiteralTask::Visit {
837 value: field,
838 lemma_type: field_type,
839 });
840 }
841 }
842 }
843 _ => {
844 for item in items.iter().rev() {
845 tasks.push(InputLiteralTask::Visit {
846 value: item,
847 lemma_type: Arc::clone(element),
848 });
849 }
850 }
851 }
852 }
853 _ => converted.push(JsonValue::String(
854 value
855 .result
856 .clone()
857 .expect("BUG: scalar Show value without result"),
858 )),
859 },
860 InputLiteralTask::CollectArray { len } => {
861 let items = converted.split_off(converted.len() - len);
862 converted.push(JsonValue::Array(items));
863 }
864 InputLiteralTask::CollectObject { keys } => {
865 let children = converted.split_off(converted.len() - keys.len());
866 converted.push(JsonValue::Object(keys.into_iter().zip(children).collect()));
867 }
868 }
869 }
870 converted
871 .pop()
872 .expect("BUG: Show value conversion yields exactly one JSON value")
873}
874
875fn lemma_type_without_standard_keywords(lemma_type: &DomainLemmaType) -> JsonValue {
878 let mut value = to_json(&LemmaType::from(lemma_type));
879 strip_standard_keywords(&mut value);
880 value
881}
882
883fn strip_standard_keywords(value: &mut JsonValue) {
884 let mut pending = vec![value];
885 while let Some(value) = pending.pop() {
886 let object = value
887 .as_object_mut()
888 .expect("BUG: LemmaType serializes to a JSON object");
889 object.remove("help");
890 if object.get("kind") == Some(&json!("text")) {
891 object.remove("options");
892 object.remove("length");
893 }
894 if let Some(element) = object.get_mut("element") {
895 pending.push(element);
896 }
897 }
898}
899
900struct RowSchemas<'a> {
903 engine: &'a Engine,
904 consumer: Option<&'a ExecutionPlan>,
907 started: BTreeSet<SpecIdentity>,
908 pending: Vec<SpecIdentity>,
909 defs: BTreeMap<String, JsonValue>,
910}
911
912impl<'a> RowSchemas<'a> {
913 fn new(engine: &'a Engine, consumer: Option<&'a ExecutionPlan>) -> Self {
914 Self {
915 engine,
916 consumer,
917 started: BTreeSet::new(),
918 pending: Vec::new(),
919 defs: BTreeMap::new(),
920 }
921 }
922
923 fn into_defs(mut self) -> (Map<String, JsonValue>, Map<String, JsonValue>) {
927 let mut row_field_needed_by_rules = Map::new();
928 while let Some(identity) = self.pending.pop() {
929 let show = self.engine.show_spec(&identity);
930 let mut schema = Map::new();
931 schema.insert("title".to_string(), json!(format!("{identity} row")));
932 schema.extend(row_object_schema(&show, &mut self));
933 let def_name = row_def_name(&identity);
934 let mut field_map = Map::new();
935 for field in row_property_names(&show) {
936 let needed = self.row_field_needed_by_rules(&identity, &field);
937 if !needed.is_empty() {
938 field_map.insert(field, json!(needed));
939 }
940 }
941 if !field_map.is_empty() {
942 row_field_needed_by_rules.insert(def_name.clone(), JsonValue::Object(field_map));
943 }
944 self.defs.insert(def_name, JsonValue::Object(schema));
945 }
946 (self.defs.into_iter().collect(), row_field_needed_by_rules)
947 }
948
949 fn row_field_needed_by_rules(&self, identity: &SpecIdentity, field: &str) -> Vec<String> {
951 let Some(consumer) = self.consumer else {
952 return Vec::new();
953 };
954 consumer
955 .row_field_needed_by_rules(identity, field)
956 .into_iter()
957 .map(|rule_position| {
958 &consumer
959 .rules
960 .get_index(rule_position as usize)
961 .expect("BUG: needed_by_rules field consumer out of consumer.rules range")
962 .1
963 .path
964 })
965 .filter(|rule_path| consumer.show_rules.contains_key(*rule_path))
966 .map(|rule_path| rule_path.input_key())
967 .collect()
968 }
969
970 fn reference(&mut self, identity: &SpecIdentity) -> JsonValue {
972 let name = row_def_name(identity);
973 if self.started.insert(identity.clone()) {
974 self.pending.push(identity.clone());
975 }
976 json!({ "$ref": format!("#/$defs/{name}") })
977 }
978}
979
980pub fn row_def_name(identity: &SpecIdentity) -> String {
984 let mut name = String::from("LemmaRow_");
985 for c in identity.to_string().chars() {
986 if c.is_ascii_alphanumeric() || c == '-' {
987 name.push(c);
988 } else if c == '_' {
989 name.push_str("__");
990 } else {
991 name.push_str(&format!("_{:x}_", c as u32));
992 }
993 }
994 name
995}
996
997#[cfg(test)]
998mod tests {
999 use super::*;
1000 use crate::parsing::ast::Span;
1001 use crate::parsing::source::{Source, SourceType};
1002 use crate::planning::semantics::parse_value_from_string;
1003
1004 const INPUTS: &str = r#"
1005spec inputs
1006data money: measure -> unit eur: 1 -> unit cent: 0.01
1007data price: money -> suggest 10 eur
1008data deposit: 250 cent
1009data rate: ratio -> suggest 21%
1010data bps: ratio -> unit basis_points: 10000 -> unit percent: 100 -> suggest 500 basis_points
1011data fee: 3%
1012data quantity: number -> suggest 12.5
1013data offset: -3
1014data express: boolean -> suggest true
1015data code: text -> option "A" -> option "B" -> suggest "B"
1016data label: text -> length 3
1017data start: date -> suggest 2026-01-01
1018data at: time -> suggest 09:30:00
1019data window: number range -> suggest 1...5
1020data tags: number list
1021 -> with 5, 10
1022rule r_price: price + deposit
1023rule r_rate: rate
1024rule r_bps: bps
1025rule r_fee: fee
1026rule r_quantity: quantity + offset
1027rule r_express: express
1028rule r_code: code
1029rule r_label: label
1030rule r_start: start
1031rule r_at: at
1032rule r_window: window
1033rule r_tags: tags -> sum
1034"#;
1035
1036 fn engine() -> Engine {
1037 let mut engine = Engine::new();
1038 engine
1039 .load([(SourceType::Volatile, INPUTS.to_string())])
1040 .expect("load inputs");
1041 engine
1042 }
1043
1044 fn document() -> (Engine, DomainShow, JsonValue) {
1045 let engine = engine();
1046 let show = engine.show(None, "inputs", None).expect("show inputs");
1047 let document = show_document(&engine, &show);
1048 (engine, show, document)
1049 }
1050
1051 fn source() -> Source {
1052 Source::new(
1053 SourceType::Volatile,
1054 Span {
1055 start: 0,
1056 end: 0,
1057 line: 1,
1058 col: 1,
1059 },
1060 )
1061 }
1062
1063 fn decimal(text: &JsonValue) -> rust_decimal::Decimal {
1064 text.as_str()
1065 .expect("API decimal is a string")
1066 .parse()
1067 .expect("API decimal parses")
1068 }
1069
1070 fn assert_literal_denotes(
1072 name: &str,
1073 literal: &str,
1074 value: &JsonValue,
1075 specification: &DomainTypeSpecification,
1076 ) {
1077 let parsed = parse_value_from_string(literal, specification, &source())
1078 .unwrap_or_else(|error| panic!("{name}: '{literal}' must parse: {error}"));
1079 assert_parsed_denotes(name, literal, &parsed, value);
1080 }
1081
1082 fn assert_parsed_denotes(
1083 name: &str,
1084 literal: &str,
1085 parsed: &crate::parsing::ast::Value,
1086 value: &JsonValue,
1087 ) {
1088 use crate::parsing::ast::Value;
1089 match parsed {
1090 Value::Number(magnitude) => {
1091 assert_eq!(*magnitude, decimal(&value["number"]), "{name}: '{literal}'");
1092 }
1093 Value::NumberWithUnit(magnitude, unit) => {
1094 let units = value
1095 .get("measure")
1096 .or_else(|| value.get("ratio"))
1097 .unwrap_or_else(|| panic!("{name}: unit map in {value}"));
1098 assert_eq!(
1099 *magnitude,
1100 decimal(&units[unit]),
1101 "{name}: '{literal}' vs {value}"
1102 );
1103 }
1104 Value::Range(left, right) => {
1105 assert_parsed_denotes(name, literal, left, &value["range"]["from"]);
1106 assert_parsed_denotes(name, literal, right, &value["range"]["to"]);
1107 }
1108 other => panic!("{name}: '{literal}' parsed to unexpected {other:?}"),
1109 }
1110 }
1111
1112 #[test]
1113 fn spec_level_keywords() {
1114 let (_, _, document) = document();
1115 assert_eq!(document["$schema"], JSON_SCHEMA_DIALECT);
1116 assert_eq!(document["title"], "inputs");
1117 assert_eq!(document["type"], "object");
1118 assert_eq!(document["additionalProperties"], true);
1119 assert!(document.get("$defs").is_none());
1120 assert_eq!(document["x-lemma-start-line"], 2);
1121 assert!(document["x-lemma-rules"]["r_price"].is_object());
1122 }
1123
1124 #[test]
1125 fn required_is_sorted_needed_inputs_without_committed_value() {
1126 let (_, _, document) = document();
1127 assert_eq!(
1128 document["required"],
1129 json!([
1130 "at", "bps", "code", "express", "label", "price", "quantity", "rate", "start",
1131 "window"
1132 ])
1133 );
1134 }
1135
1136 #[test]
1137 fn standard_keywords_per_type() {
1138 let (_, _, document) = document();
1139 let properties = &document["properties"];
1140 assert_eq!(properties["express"]["oneOf"][0]["type"], "boolean");
1141 assert_eq!(properties["express"]["examples"], json!([true]));
1142 assert_eq!(properties["code"]["type"], "string");
1143 assert_eq!(properties["code"]["enum"], json!(["A", "B"]));
1144 assert_eq!(properties["label"]["minLength"], 3);
1145 assert_eq!(properties["label"]["maxLength"], 3);
1146 assert_eq!(properties["quantity"]["type"], "string");
1147 assert!(properties["quantity"].get("pattern").is_none());
1148 assert_eq!(
1149 properties["price"]["oneOf"][1]["propertyNames"]["enum"],
1150 json!(["eur", "cent"])
1151 );
1152 assert_eq!(
1153 properties["price"]["oneOf"][1]["additionalProperties"],
1154 json!({ "type": "string" })
1155 );
1156 assert!(properties["price"]["oneOf"][0].get("pattern").is_none());
1157 assert_eq!(
1158 properties["rate"]["oneOf"][1]["propertyNames"]["enum"],
1159 json!(["percent", "permille"])
1160 );
1161 assert_eq!(
1162 properties["bps"]["oneOf"][1]["propertyNames"]["enum"],
1163 json!(["percent", "permille", "basis_points"])
1164 );
1165 assert_eq!(
1166 properties["start"]["oneOf"],
1167 json!([
1168 {"type": "string", "format": "date"},
1169 {"type": "string", "format": "date-time"}
1170 ])
1171 );
1172 assert_eq!(properties["at"]["type"], "string");
1173 assert_eq!(properties["at"]["format"], "time");
1174 assert_eq!(properties["window"]["type"], "string");
1175 assert!(properties["window"].get("pattern").is_none());
1176 assert!(properties["window"].get("format").is_none());
1177 let tags = &properties["tags"];
1178 assert_eq!(tags["type"], "array");
1179 assert_eq!(tags["items"], json!({ "type": "string" }));
1180 assert_eq!(
1181 tags["maxItems"],
1182 crate::ResourceLimits::default().max_list_rows
1183 );
1184 assert_eq!(tags["default"], json!(["5", "10"]));
1185 }
1186
1187 #[test]
1188 fn x_lemma_type_omits_what_standard_keywords_state() {
1189 let (_, _, document) = document();
1190 for (name, property) in document["properties"].as_object().expect("properties") {
1191 let lemma_type = &property["x-lemma-type"];
1192 assert!(lemma_type.get("help").is_none(), "{name}: {lemma_type}");
1193 assert!(lemma_type.get("options").is_none(), "{name}: {lemma_type}");
1194 assert!(lemma_type.get("length").is_none(), "{name}: {lemma_type}");
1195 if let Some(element) = lemma_type.get("element") {
1196 assert!(element.get("help").is_none(), "{name}: {element}");
1197 }
1198 assert!(property.get("x-lemma-path").is_some(), "{name}");
1199 assert!(property.get("x-lemma-needed-by-rules").is_some(), "{name}");
1200 }
1201 assert_eq!(
1202 document["properties"]["price"]["x-lemma-type"]["kind"],
1203 "measure"
1204 );
1205 assert!(document["properties"]["price"]["x-lemma-type"]["units"].is_array());
1206 }
1207
1208 fn value_json_has_reference(value: &JsonValue) -> bool {
1209 if value.get("reference").is_some() {
1210 return true;
1211 }
1212 if let Some(fields) = value.get("fields").and_then(|v| v.as_object()) {
1213 if fields.values().any(value_json_has_reference) {
1214 return true;
1215 }
1216 }
1217 if let Some(list) = value.get("list").and_then(|v| v.as_array()) {
1218 if list.iter().any(value_json_has_reference) {
1219 return true;
1220 }
1221 }
1222 false
1223 }
1224
1225 fn evaluate_data_json_of(value: &JsonValue) -> JsonValue {
1227 if let Some(boolean) = value.get("boolean") {
1228 return boolean.clone();
1229 }
1230 if let Some(items) = value.get("list") {
1231 return JsonValue::Array(
1232 items
1233 .as_array()
1234 .expect("list")
1235 .iter()
1236 .map(evaluate_data_json_of)
1237 .collect(),
1238 );
1239 }
1240 if let Some(fields) = value.get("fields") {
1241 return JsonValue::Object(
1242 fields
1243 .as_object()
1244 .expect("fields")
1245 .iter()
1246 .filter(|(_, field)| field.get("reference").is_none())
1247 .map(|(name, field)| (name.clone(), evaluate_data_json_of(field)))
1248 .collect(),
1249 );
1250 }
1251 if value.get("reference").is_some() {
1252 panic!("BUG: reference field is not a run-data literal");
1253 }
1254 value["result"].clone()
1255 }
1256
1257 #[test]
1258 fn instance_list_with_references_has_value_but_no_default() {
1259 let mut engine = Engine::new();
1260 engine
1261 .load([(
1262 SourceType::Volatile,
1263 r#"
1264spec processing
1265data type: text
1266data weight: number
1267data enabled: boolean
1268spec line_item
1269data weight: number
1270data finish_a: boolean
1271uses step: processing
1272data processing: step list
1273 -> with weight: weight, type: "finish_a", enabled: finish_a
1274rule cost: processing -> where enabled -> count
1275"#
1276 .to_string(),
1277 )])
1278 .expect("load");
1279 let show = engine.show(None, "line_item", None).expect("show");
1280 let processing = show.data.get("processing").expect("processing");
1281 let value = processing.value.as_ref().expect("value");
1282 assert!(
1283 rule_result_value_has_reference(value),
1284 "expected reference in value: {value:?}"
1285 );
1286 let document = show_document(&engine, &show);
1287 let property = &document["properties"]["processing"];
1288 assert!(property.get("x-lemma-value").is_some(), "{property}");
1289 assert!(
1290 property.get("default").is_none(),
1291 "default must be omitted when fields reference data: {property}"
1292 );
1293 }
1294
1295 #[test]
1296 fn default_and_examples_agree_with_typed_value_and_suggestion() {
1297 let (_, _, document) = document();
1298 let mut checked = 0;
1299 for (name, property) in document["properties"].as_object().expect("properties") {
1300 match (property.get("default"), property.get("x-lemma-value")) {
1301 (Some(default), Some(value)) => {
1302 assert_eq!(default, &evaluate_data_json_of(value), "{name}");
1303 checked += 1;
1304 }
1305 (None, Some(value)) => {
1306 assert!(
1307 value_json_has_reference(value),
1308 "{name}: value without default must carry a reference field"
1309 );
1310 }
1311 (None, None) => {}
1312 other => {
1313 panic!("{name}: default and x-lemma-value disagree on presence: {other:?}")
1314 }
1315 }
1316 match (property.get("examples"), property.get("x-lemma-suggestion")) {
1317 (Some(examples), Some(suggestion)) => {
1318 assert_eq!(
1319 examples,
1320 &json!([evaluate_data_json_of(suggestion)]),
1321 "{name}"
1322 );
1323 checked += 1;
1324 }
1325 (None, None) => {}
1326 other => panic!(
1327 "{name}: examples and x-lemma-suggestion disagree on presence: {other:?}"
1328 ),
1329 }
1330 }
1331 assert_eq!(checked, 13);
1332 }
1333
1334 fn is_numeric_literal_specification(specification: &DomainTypeSpecification) -> bool {
1335 matches!(
1336 specification,
1337 DomainTypeSpecification::Number { .. }
1338 | DomainTypeSpecification::Measure { .. }
1339 | DomainTypeSpecification::Ratio { .. }
1340 | DomainTypeSpecification::NumberRange { .. }
1341 | DomainTypeSpecification::MeasureRange { .. }
1342 | DomainTypeSpecification::RatioRange { .. }
1343 )
1344 }
1345
1346 #[test]
1347 fn emitted_string_literals_denote_their_value() {
1348 let (_, show, document) = document();
1349 let mut checked = 0;
1350 for (name, property) in document["properties"].as_object().expect("properties") {
1351 let specification = &show.data[name].lemma_type.specifications;
1352 if !is_numeric_literal_specification(specification) {
1353 continue;
1354 }
1355 let mut literals: Vec<(&str, &JsonValue)> = Vec::new();
1356 if let Some(literal) = property.get("default").and_then(|value| value.as_str()) {
1357 literals.push((literal, &property["x-lemma-value"]));
1358 }
1359 if let Some(literal) = property
1360 .get("examples")
1361 .and_then(|examples| examples[0].as_str())
1362 {
1363 literals.push((literal, &property["x-lemma-suggestion"]));
1364 }
1365 for (literal, value) in literals {
1366 assert_literal_denotes(name, literal, value, specification);
1367 checked += 1;
1368 }
1369 }
1370 assert_eq!(checked, 8);
1371 }
1372
1373 fn identity(repository: Option<&str>, spec: &str) -> SpecIdentity {
1374 SpecIdentity {
1375 repository: repository.map(str::to_string),
1376 spec: spec.to_string(),
1377 effective: crate::parsing::ast::EffectiveDate::Origin,
1378 slice: crate::parsing::ast::EffectiveDate::Origin,
1379 }
1380 }
1381
1382 #[test]
1383 fn row_def_name_is_injective_and_component_safe() {
1384 assert_eq!(row_def_name(&identity(None, "line")), "LemmaRow_line");
1385 assert_eq!(
1386 row_def_name(&identity(None, "tax_bracket")),
1387 "LemmaRow_tax__bracket"
1388 );
1389 let names = [
1390 row_def_name(&identity(Some("@org/repo"), "line")),
1391 row_def_name(&identity(None, "_org_repo_line")),
1392 row_def_name(&identity(None, "line")),
1393 ];
1394 for name in &names {
1395 assert!(
1396 name.chars()
1397 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-'),
1398 "{name}"
1399 );
1400 }
1401 assert_ne!(names[0], names[1]);
1402 assert_ne!(names[0], names[2]);
1403 }
1404}