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