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 TEXT_NOTE: &str = "Text values are limited to 1024 UTF-8 bytes.";
336const DATE_NOTE: &str = "Accepted forms: an RFC 3339 date-time with offset; a naive date-time `YYYY-MM-DDTHH:MM[:SS[.f]]`; a date `YYYY-MM-DD`; an ISO week `YYYY-Www` (week 1 to 53, 53 only in ISO years that have one); a year-month `YYYY-MM`; a year `YYYY` (1 to 9999). Exact separators follow the chrono parsers the engine uses.";
337const TIME_NOTE: &str = "Accepted forms: `HH:MM[:SS[.f]]`, optionally followed by `Z` or an offset `+HH:MM` / `-HH:MM`.";
338
339fn with_engine_note(help: &str, note: &str) -> String {
340 if help.is_empty() {
341 note.to_string()
342 } else {
343 format!("{help} {note}")
344 }
345}
346
347const BOOLEAN_TEXT_PATTERN: &str =
349 r"^\s*(?:[Tt][Rr][Uu][Ee]|[Ff][Aa][Ll][Ss][Ee]|[Yy][Ee][Ss]|[Nn][Oo])\s*$";
350
351pub fn show_document(engine: &Engine, show: &DomainShow) -> JsonValue {
358 let consumer = match engine.plan_matching_show(show) {
359 Some(plan) => Some(plan),
360 None if !show_has_instance_list(show) => None,
361 None => panic!(
362 "BUG: Show '{}' has instance-list inputs but no matching execution plan",
363 show.spec
364 ),
365 };
366 let mut rows = RowSchemas::new(engine, consumer);
367 let mut document = Map::new();
368 document.insert("$schema".to_string(), json!(JSON_SCHEMA_DIALECT));
369 document.insert("title".to_string(), json!(show.spec));
370 if let Some(commentary) = &show.commentary {
371 document.insert("description".to_string(), json!(commentary));
372 }
373 document.extend(input_object_schema(show, &mut rows));
374 let defs = rows.into_defs();
375 if !defs.is_empty() {
376 document.insert("$defs".to_string(), JsonValue::Object(defs));
377 }
378 if let Some(repository) = &show.repository {
379 document.insert("x-lemma-repository".to_string(), json!(repository));
380 }
381 if let Some(from) = &show.effective_from {
382 document.insert("x-lemma-effective-from".to_string(), to_json(from));
383 }
384 if let Some(to) = &show.effective_to {
385 document.insert("x-lemma-effective-to".to_string(), to_json(to));
386 }
387 document.insert("x-lemma-start-line".to_string(), json!(show.start_line));
388 if let Some(source_type) = &show.source_type {
389 document.insert("x-lemma-source-type".to_string(), to_json(source_type));
390 }
391 document.insert("x-lemma-meta".to_string(), to_json(&show.meta));
392 let types: IndexMap<&String, LemmaType> = show
393 .types
394 .iter()
395 .map(|(name, lemma_type)| (name, LemmaType::from(lemma_type)))
396 .collect();
397 document.insert("x-lemma-types".to_string(), to_json(&types));
398 let rules: IndexMap<&String, ShowRule> = show
399 .rules
400 .iter()
401 .map(|(name, rule)| (name, ShowRule::from(rule)))
402 .collect();
403 document.insert("x-lemma-rules".to_string(), to_json(&rules));
404 JsonValue::Object(document)
405}
406
407fn to_json<T: Serialize>(value: &T) -> JsonValue {
408 serde_json::to_value(value).expect("BUG: Show document parts serialize to JSON")
409}
410
411fn show_has_instance_list(show: &DomainShow) -> bool {
412 show.data.values().any(|data| {
413 matches!(
414 &data.lemma_type.specifications,
415 DomainTypeSpecification::List { element, .. }
416 if matches!(&element.specifications, DomainTypeSpecification::Spec { .. })
417 )
418 })
419}
420
421fn input_object_schema(show: &DomainShow, rows: &mut RowSchemas) -> Map<String, JsonValue> {
425 let mut properties = Map::new();
426 let mut required = Vec::new();
427 for (name, data) in &show.data {
428 properties.insert(
429 name.clone(),
430 property_schema(data, rows, &data.needed_by_rules),
431 );
432 let is_list = matches!(
433 data.lemma_type.specifications,
434 DomainTypeSpecification::List { .. }
435 );
436 if data.value.is_none() && !data.needed_by_rules.is_empty() && !is_list {
437 required.push(name.clone());
438 }
439 }
440 required.sort();
441 let mut schema = Map::new();
442 schema.insert("type".to_string(), json!("object"));
443 schema.insert("additionalProperties".to_string(), json!(true));
444 schema.insert("properties".to_string(), JsonValue::Object(properties));
445 if !required.is_empty() {
446 schema.insert("required".to_string(), json!(required));
447 }
448 schema
449}
450
451fn row_object_schema(
455 show: &DomainShow,
456 identity: &SpecIdentity,
457 rows: &mut RowSchemas,
458) -> Map<String, JsonValue> {
459 let mut properties = Map::new();
460 let mut required = Vec::new();
461 for (name, lemma_type) in &show.types {
462 let stub;
463 let entry = match show.data.get(name) {
464 Some(data) => data,
465 None => {
466 stub = DomainShowData {
467 lemma_type: lemma_type.clone(),
468 path: Vec::new(),
469 value: None,
470 suggestion: None,
471 needed_by_rules: Vec::new(),
472 };
473 &stub
474 }
475 };
476 let is_list = matches!(
477 entry.lemma_type.specifications,
478 DomainTypeSpecification::List { .. }
479 );
480 if entry.value.is_none() && !is_list {
481 required.push(name.clone());
482 }
483 let needed = rows.row_field_needed_by_rules(identity, name);
484 properties.insert(name.clone(), property_schema(entry, rows, &needed));
485 }
486 for (name, data) in &show.data {
487 if properties.contains_key(name) {
488 continue;
489 }
490 let is_list = matches!(
491 data.lemma_type.specifications,
492 DomainTypeSpecification::List { .. }
493 );
494 if data.value.is_none() && !is_list {
495 required.push(name.clone());
496 }
497 let needed = rows.row_field_needed_by_rules(identity, name);
498 properties.insert(name.clone(), property_schema(data, rows, &needed));
499 }
500 required.sort();
501 let mut schema = Map::new();
502 schema.insert("type".to_string(), json!("object"));
503 schema.insert("additionalProperties".to_string(), json!(true));
504 schema.insert("properties".to_string(), JsonValue::Object(properties));
505 if !required.is_empty() {
506 schema.insert("required".to_string(), json!(required));
507 }
508 schema
509}
510
511fn property_schema(
512 data: &DomainShowData,
513 rows: &mut RowSchemas,
514 needed_by_rules: &[String],
515) -> JsonValue {
516 let mut schema = type_schema(&data.lemma_type.specifications, rows);
517 let help = specification_help(&data.lemma_type.specifications);
518 if !help.is_empty() {
519 schema.insert("description".to_string(), json!(help));
520 }
521 if let Some(value) = &data.value {
522 if !rule_result_value_has_reference(value) {
525 schema.insert(
526 "default".to_string(),
527 input_literal(value, &data.lemma_type, rows),
528 );
529 }
530 }
531 if let Some(suggestion) = &data.suggestion {
532 schema.insert(
533 "examples".to_string(),
534 json!([input_literal(suggestion, &data.lemma_type, rows)]),
535 );
536 }
537 schema.insert(
538 "x-lemma-type".to_string(),
539 lemma_type_without_standard_keywords(&data.lemma_type),
540 );
541 let path: Vec<PathSegment> = data.path.iter().map(PathSegment::from).collect();
542 schema.insert("x-lemma-path".to_string(), to_json(&path));
543 schema.insert(
544 "x-lemma-needed-by-rules".to_string(),
545 json!(needed_by_rules),
546 );
547 if let Some(value) = &data.value {
548 schema.insert(
549 "x-lemma-value".to_string(),
550 to_json(&RuleResultValue::from(value)),
551 );
552 }
553 if let Some(suggestion) = &data.suggestion {
554 schema.insert(
555 "x-lemma-suggestion".to_string(),
556 to_json(&RuleResultValue::from(suggestion)),
557 );
558 }
559 JsonValue::Object(schema)
560}
561
562fn type_schema(
565 specification: &DomainTypeSpecification,
566 rows: &mut RowSchemas,
567) -> Map<String, JsonValue> {
568 let DomainTypeSpecification::List { element, .. } = specification else {
569 return scalar_data_schema(specification);
570 };
571 let mut schema = Map::new();
572 let items = match &element.specifications {
573 DomainTypeSpecification::Spec { spec } => rows.reference(spec),
574 DomainTypeSpecification::List { .. } => {
575 unreachable!("BUG: lists do not nest; planning rejects a list element list")
576 }
577 element => JsonValue::Object(scalar_data_schema(element)),
578 };
579 schema.insert("type".to_string(), json!("array"));
580 schema.insert("items".to_string(), items);
581 schema.insert(
582 "maxItems".to_string(),
583 json!(rows.engine.limits().max_list_rows),
584 );
585 schema
586}
587
588fn scalar_data_schema(specification: &DomainTypeSpecification) -> Map<String, JsonValue> {
590 let names: Vec<&str> = match specification {
591 DomainTypeSpecification::Measure { units, .. } => {
592 units.iter().map(|unit| unit.name.as_str()).collect()
593 }
594 DomainTypeSpecification::Ratio { units, .. } => {
595 units.iter().map(|unit| unit.name.as_str()).collect()
596 }
597 _ => return scalar_type_schema(specification),
598 };
599 let unit_map = json!({
600 "type": "object",
601 "minProperties": 1,
602 "maxProperties": 1,
603 "propertyNames": {"enum": names},
604 "additionalProperties": {"type": "string", "pattern": number_pattern()}
605 });
606 let mut schema = Map::new();
607 schema.insert(
608 "oneOf".to_string(),
609 json!([
610 JsonValue::Object(scalar_type_schema(specification)),
611 unit_map
612 ]),
613 );
614 schema
615}
616
617fn scalar_type_schema(specification: &DomainTypeSpecification) -> Map<String, JsonValue> {
618 let mut schema = Map::new();
619 match specification {
620 DomainTypeSpecification::Boolean { .. } => {
621 schema.insert(
622 "oneOf".to_string(),
623 json!([
624 {"type": "boolean"},
625 {"type": "string", "pattern": BOOLEAN_TEXT_PATTERN}
626 ]),
627 );
628 }
629 DomainTypeSpecification::Text {
630 length, options, ..
631 } => {
632 schema.insert("type".to_string(), json!("string"));
633 if !options.is_empty() {
634 schema.insert("enum".to_string(), json!(options));
635 }
636 if let Some(length) = length {
637 schema.insert("minLength".to_string(), json!(length));
638 schema.insert("maxLength".to_string(), json!(length));
639 }
640 }
641 DomainTypeSpecification::Number { .. } => {
642 schema.insert("type".to_string(), json!("string"));
643 schema.insert("pattern".to_string(), json!(number_pattern()));
644 }
645 DomainTypeSpecification::Measure { units, .. } => {
646 schema.insert("type".to_string(), json!("string"));
647 schema.insert("pattern".to_string(), json!(measure_pattern(units)));
648 }
649 DomainTypeSpecification::Ratio { units, .. } => {
650 schema.insert("type".to_string(), json!("string"));
651 schema.insert("pattern".to_string(), json!(ratio_pattern(units)));
652 }
653 DomainTypeSpecification::NumberRange { .. } => {
654 schema.insert("type".to_string(), json!("string"));
655 schema.insert("pattern".to_string(), json!(range_pattern(&number_body())));
656 }
657 DomainTypeSpecification::MeasureRange { units, .. } => {
658 schema.insert("type".to_string(), json!("string"));
659 schema.insert(
660 "pattern".to_string(),
661 json!(range_pattern(&measure_body(units))),
662 );
663 }
664 DomainTypeSpecification::RatioRange { units, .. } => {
665 schema.insert("type".to_string(), json!("string"));
666 schema.insert(
667 "pattern".to_string(),
668 json!(range_pattern(&ratio_body(units))),
669 );
670 }
671 DomainTypeSpecification::Date { .. }
672 | DomainTypeSpecification::Time { .. }
673 | DomainTypeSpecification::DateRange { .. }
674 | DomainTypeSpecification::TimeRange { .. } => {
675 schema.insert("type".to_string(), json!("string"));
676 }
677 DomainTypeSpecification::List { .. } => {
678 unreachable!("BUG: list schemas are built by type_schema")
679 }
680 DomainTypeSpecification::Spec { spec } => {
681 unreachable!("BUG: spec '{spec}' is a uses alias, never a Show data entry")
682 }
683 DomainTypeSpecification::Veto { .. } => {
684 unreachable!("BUG: veto sentinel type on a Show data entry")
685 }
686 DomainTypeSpecification::Undetermined => {
687 unreachable!("BUG: undetermined sentinel type on a Show data entry")
688 }
689 }
690 schema
691}
692
693fn specification_help(specification: &DomainTypeSpecification) -> String {
694 let mut leaf = specification;
695 while let DomainTypeSpecification::List { element, .. } = leaf {
696 leaf = &element.specifications;
697 }
698 match leaf {
699 DomainTypeSpecification::Boolean { help }
700 | DomainTypeSpecification::Measure { help, .. }
701 | DomainTypeSpecification::MeasureRange { help, .. }
702 | DomainTypeSpecification::Number { help, .. }
703 | DomainTypeSpecification::NumberRange { help, .. }
704 | DomainTypeSpecification::Ratio { help, .. }
705 | DomainTypeSpecification::RatioRange { help, .. } => help.clone(),
706 DomainTypeSpecification::Text { help, .. } => with_engine_note(help, TEXT_NOTE),
707 DomainTypeSpecification::Date { help, .. }
708 | DomainTypeSpecification::DateRange { help, .. } => with_engine_note(help, DATE_NOTE),
709 DomainTypeSpecification::Time { help, .. }
710 | DomainTypeSpecification::TimeRange { help, .. } => with_engine_note(help, TIME_NOTE),
711 DomainTypeSpecification::Spec { spec } => spec.to_string(),
712 DomainTypeSpecification::List { .. } => {
713 unreachable!("BUG: list chain is unwound before the leaf match")
714 }
715 DomainTypeSpecification::Veto { .. } | DomainTypeSpecification::Undetermined => {
716 unreachable!("BUG: sentinel type on a Show data entry")
717 }
718 }
719}
720
721fn number_body() -> String {
726 let digits = "[0-9](?:_?[0-9]){0,27}";
727 format!(r"[+-]?(?:{digits}(?:\.(?:{digits})?)?|\.{digits})(?:[eE][+-]?[0-9]{{1,2}})?")
728}
729
730fn measure_body(units: &MeasureUnits) -> String {
732 let names: Vec<&str> = units.iter().map(|unit| unit.name.as_str()).collect();
733 assert!(!names.is_empty(), "BUG: planned measure type has no units");
734 format!(r"{}\s+({})", number_body(), alternation(&names))
735}
736
737fn ratio_body(units: &RatioUnits) -> String {
741 let names: Vec<&str> = units.iter().map(|unit| unit.name.as_str()).collect();
742 assert!(!names.is_empty(), "BUG: planned ratio type has no units");
743 let mut alternatives = Vec::new();
744 if names.contains(&"permille") {
745 alternatives.push(format!("{}%%", number_body()));
746 }
747 if names.contains(&"percent") {
748 alternatives.push(format!("{}%", number_body()));
749 }
750 alternatives.push(format!(r"{}\s+({})", number_body(), alternation(&names)));
751 format!("(?:{})", alternatives.join("|"))
752}
753
754pub fn number_pattern() -> String {
756 format!(r"^\s*{}\s*$", number_body())
757}
758
759pub fn measure_pattern(units: &MeasureUnits) -> String {
761 format!(r"^\s*{}\s*$", measure_body(units))
762}
763
764pub fn ratio_pattern(units: &RatioUnits) -> String {
766 format!(r"^\s*{}\s*$", ratio_body(units))
767}
768
769fn range_pattern(endpoint: &str) -> String {
771 format!(r"^\s*{endpoint}\s*\.\.\.\s*{endpoint}\s*$")
772}
773
774fn alternation(names: &[&str]) -> String {
775 names
776 .iter()
777 .map(|name| regex_escape(name))
778 .collect::<Vec<_>>()
779 .join("|")
780}
781
782fn regex_escape(text: &str) -> String {
783 let mut escaped = String::with_capacity(text.len());
784 for c in text.chars() {
785 if "\\^$.|?*+()[]{}/-".contains(c) {
786 escaped.push('\\');
787 }
788 escaped.push(c);
789 }
790 escaped
791}
792
793fn rule_result_value_has_reference(value: &DomainRuleResultValue) -> bool {
797 if value.reference.is_some() {
798 return true;
799 }
800 if let Some(fields) = &value.fields {
801 if fields.values().any(rule_result_value_has_reference) {
802 return true;
803 }
804 }
805 if let Some(list) = &value.list {
806 if list.iter().any(rule_result_value_has_reference) {
807 return true;
808 }
809 }
810 false
811}
812
813enum InputLiteralTask<'a> {
814 Visit {
815 value: &'a DomainRuleResultValue,
816 lemma_type: Arc<DomainLemmaType>,
817 },
818 CollectArray {
819 len: usize,
820 },
821 CollectObject {
822 keys: Vec<String>,
823 },
824}
825
826fn input_literal(
827 value: &DomainRuleResultValue,
828 lemma_type: &DomainLemmaType,
829 rows: &RowSchemas,
830) -> JsonValue {
831 let mut tasks = vec![InputLiteralTask::Visit {
832 value,
833 lemma_type: Arc::new(lemma_type.clone()),
834 }];
835 let mut converted: Vec<JsonValue> = Vec::new();
836 while let Some(task) = tasks.pop() {
837 match task {
838 InputLiteralTask::Visit { value, lemma_type } => match &lemma_type.specifications {
839 DomainTypeSpecification::Boolean { .. } => converted.push(JsonValue::Bool(
840 value
841 .boolean
842 .expect("BUG: boolean-typed Show value without boolean"),
843 )),
844 DomainTypeSpecification::List { element, .. } => {
845 let items = value
846 .list
847 .as_ref()
848 .expect("BUG: list-typed Show value without list");
849 tasks.push(InputLiteralTask::CollectArray { len: items.len() });
850 match &element.specifications {
851 DomainTypeSpecification::Spec { spec } => {
852 let row_show = rows.engine.show_spec(spec);
853 for row in items.iter().rev() {
854 let fields = row
855 .fields
856 .as_ref()
857 .expect("BUG: instance list Show row without fields");
858 let mut keys = Vec::with_capacity(fields.len());
859 let mut children = Vec::with_capacity(fields.len());
860 for (name, field) in fields {
861 assert!(
862 field.reference.is_none(),
863 "BUG: input_literal on Show value with reference field '{name}'"
864 );
865 let field_type = row_show
866 .types
867 .get(name)
868 .or_else(|| row_show.data.get(name).map(|d| &d.lemma_type))
869 .unwrap_or_else(|| {
870 panic!("BUG: row field '{name}' missing from Show of '{spec}'")
871 });
872 keys.push(name.clone());
873 children.push((field, Arc::new(field_type.clone())));
874 }
875 tasks.push(InputLiteralTask::CollectObject { keys });
876 for (field, field_type) in children.into_iter().rev() {
877 tasks.push(InputLiteralTask::Visit {
878 value: field,
879 lemma_type: field_type,
880 });
881 }
882 }
883 }
884 _ => {
885 for item in items.iter().rev() {
886 tasks.push(InputLiteralTask::Visit {
887 value: item,
888 lemma_type: Arc::clone(element),
889 });
890 }
891 }
892 }
893 }
894 _ => converted.push(JsonValue::String(
895 value
896 .result
897 .clone()
898 .expect("BUG: scalar Show value without result"),
899 )),
900 },
901 InputLiteralTask::CollectArray { len } => {
902 let items = converted.split_off(converted.len() - len);
903 converted.push(JsonValue::Array(items));
904 }
905 InputLiteralTask::CollectObject { keys } => {
906 let children = converted.split_off(converted.len() - keys.len());
907 converted.push(JsonValue::Object(keys.into_iter().zip(children).collect()));
908 }
909 }
910 }
911 converted
912 .pop()
913 .expect("BUG: Show value conversion yields exactly one JSON value")
914}
915
916fn lemma_type_without_standard_keywords(lemma_type: &DomainLemmaType) -> JsonValue {
919 let mut value = to_json(&LemmaType::from(lemma_type));
920 strip_standard_keywords(&mut value);
921 value
922}
923
924fn strip_standard_keywords(value: &mut JsonValue) {
925 let mut pending = vec![value];
926 while let Some(value) = pending.pop() {
927 let object = value
928 .as_object_mut()
929 .expect("BUG: LemmaType serializes to a JSON object");
930 object.remove("help");
931 if object.get("kind") == Some(&json!("text")) {
932 object.remove("options");
933 object.remove("length");
934 }
935 if let Some(element) = object.get_mut("element") {
936 pending.push(element);
937 }
938 }
939}
940
941struct RowSchemas<'a> {
944 engine: &'a Engine,
945 consumer: Option<&'a ExecutionPlan>,
948 started: BTreeSet<SpecIdentity>,
949 pending: Vec<SpecIdentity>,
950 defs: BTreeMap<String, JsonValue>,
951}
952
953impl<'a> RowSchemas<'a> {
954 fn new(engine: &'a Engine, consumer: Option<&'a ExecutionPlan>) -> Self {
955 Self {
956 engine,
957 consumer,
958 started: BTreeSet::new(),
959 pending: Vec::new(),
960 defs: BTreeMap::new(),
961 }
962 }
963
964 fn into_defs(mut self) -> Map<String, JsonValue> {
967 while let Some(identity) = self.pending.pop() {
968 let show = self.engine.show_spec(&identity);
969 let mut schema = Map::new();
970 schema.insert("title".to_string(), json!(format!("{identity} row")));
971 schema.extend(row_object_schema(&show, &identity, &mut self));
972 self.defs
973 .insert(row_def_name(&identity), JsonValue::Object(schema));
974 }
975 self.defs.into_iter().collect()
976 }
977
978 fn row_field_needed_by_rules(&self, identity: &SpecIdentity, field: &str) -> Vec<String> {
980 let Some(consumer) = self.consumer else {
981 return Vec::new();
982 };
983 consumer
984 .row_field_needed_by_rules(identity, field)
985 .into_iter()
986 .map(|rule_position| {
987 consumer
988 .rules
989 .get_index(rule_position as usize)
990 .expect("BUG: needed_by_rules field consumer out of consumer.rules range")
991 .1
992 .path
993 .input_key()
994 })
995 .collect()
996 }
997
998 fn reference(&mut self, identity: &SpecIdentity) -> JsonValue {
1000 let name = row_def_name(identity);
1001 if self.started.insert(identity.clone()) {
1002 self.pending.push(identity.clone());
1003 }
1004 json!({ "$ref": format!("#/$defs/{name}") })
1005 }
1006}
1007
1008pub fn row_def_name(identity: &SpecIdentity) -> String {
1012 let mut name = String::from("LemmaRow_");
1013 for c in identity.to_string().chars() {
1014 if c.is_ascii_alphanumeric() || c == '-' {
1015 name.push(c);
1016 } else if c == '_' {
1017 name.push_str("__");
1018 } else {
1019 name.push_str(&format!("_{:x}_", c as u32));
1020 }
1021 }
1022 name
1023}
1024
1025#[cfg(test)]
1026mod tests {
1027 use super::*;
1028 use crate::parsing::ast::Span;
1029 use crate::parsing::source::{Source, SourceType};
1030 use crate::planning::semantics::parse_value_from_string;
1031
1032 const INPUTS: &str = r#"
1033spec inputs
1034data money: measure -> unit eur: 1 -> unit cent: 0.01
1035data price: money -> suggest 10 eur
1036data deposit: 250 cent
1037data rate: ratio -> suggest 21%
1038data bps: ratio -> unit basis_points: 10000 -> unit percent: 100 -> suggest 500 basis_points
1039data fee: 3%
1040data quantity: number -> suggest 12.5
1041data offset: -3
1042data express: boolean -> suggest true
1043data code: text -> option "A" -> option "B" -> suggest "B"
1044data label: text -> length 3
1045data start: date -> suggest 2026-01-01
1046data window: number range -> suggest 1...5
1047data tags: number list
1048 -> with 5, 10
1049rule r_price: price + deposit
1050rule r_rate: rate
1051rule r_bps: bps
1052rule r_fee: fee
1053rule r_quantity: quantity + offset
1054rule r_express: express
1055rule r_code: code
1056rule r_label: label
1057rule r_start: start
1058rule r_window: window
1059rule r_tags: tags -> sum
1060"#;
1061
1062 fn engine() -> Engine {
1063 let mut engine = Engine::new();
1064 engine
1065 .load([(SourceType::Volatile, INPUTS.to_string())])
1066 .expect("load inputs");
1067 engine
1068 }
1069
1070 fn document() -> (Engine, DomainShow, JsonValue) {
1071 let engine = engine();
1072 let show = engine.show(None, "inputs", None).expect("show inputs");
1073 let document = show_document(&engine, &show);
1074 (engine, show, document)
1075 }
1076
1077 fn source() -> Source {
1078 Source::new(
1079 SourceType::Volatile,
1080 Span {
1081 start: 0,
1082 end: 0,
1083 line: 1,
1084 col: 1,
1085 },
1086 )
1087 }
1088
1089 fn pattern_accepts(pattern: &str, literal: &str) -> bool {
1090 jsonschema::is_valid(
1091 &json!({ "type": "string", "pattern": pattern }),
1092 &json!(literal),
1093 )
1094 }
1095
1096 fn parses(literal: &str, specification: &DomainTypeSpecification) -> bool {
1097 parse_value_from_string(literal, specification, &source()).is_ok()
1098 }
1099
1100 fn decimal(text: &JsonValue) -> rust_decimal::Decimal {
1101 text.as_str()
1102 .expect("API decimal is a string")
1103 .parse()
1104 .expect("API decimal parses")
1105 }
1106
1107 fn assert_literal_denotes(
1109 name: &str,
1110 literal: &str,
1111 value: &JsonValue,
1112 specification: &DomainTypeSpecification,
1113 ) {
1114 let parsed = parse_value_from_string(literal, specification, &source())
1115 .unwrap_or_else(|error| panic!("{name}: '{literal}' must parse: {error}"));
1116 assert_parsed_denotes(name, literal, &parsed, value);
1117 }
1118
1119 fn assert_parsed_denotes(
1120 name: &str,
1121 literal: &str,
1122 parsed: &crate::parsing::ast::Value,
1123 value: &JsonValue,
1124 ) {
1125 use crate::parsing::ast::Value;
1126 match parsed {
1127 Value::Number(magnitude) => {
1128 assert_eq!(*magnitude, decimal(&value["number"]), "{name}: '{literal}'");
1129 }
1130 Value::NumberWithUnit(magnitude, unit) => {
1131 let units = value
1132 .get("measure")
1133 .or_else(|| value.get("ratio"))
1134 .unwrap_or_else(|| panic!("{name}: unit map in {value}"));
1135 assert_eq!(
1136 *magnitude,
1137 decimal(&units[unit]),
1138 "{name}: '{literal}' vs {value}"
1139 );
1140 }
1141 Value::Range(left, right) => {
1142 assert_parsed_denotes(name, literal, left, &value["range"]["from"]);
1143 assert_parsed_denotes(name, literal, right, &value["range"]["to"]);
1144 }
1145 other => panic!("{name}: '{literal}' parsed to unexpected {other:?}"),
1146 }
1147 }
1148
1149 #[test]
1150 fn spec_level_keywords() {
1151 let (_, _, document) = document();
1152 assert_eq!(document["$schema"], JSON_SCHEMA_DIALECT);
1153 assert_eq!(document["title"], "inputs");
1154 assert_eq!(document["type"], "object");
1155 assert_eq!(document["additionalProperties"], true);
1156 assert!(document.get("$defs").is_none());
1157 assert_eq!(document["x-lemma-start-line"], 2);
1158 assert!(document["x-lemma-rules"]["r_price"].is_object());
1159 }
1160
1161 #[test]
1162 fn required_is_sorted_needed_inputs_without_committed_value() {
1163 let (_, _, document) = document();
1164 assert_eq!(
1165 document["required"],
1166 json!([
1167 "bps", "code", "express", "label", "price", "quantity", "rate", "start", "window"
1168 ])
1169 );
1170 }
1171
1172 #[test]
1173 fn standard_keywords_per_type() {
1174 let (_, _, document) = document();
1175 let properties = &document["properties"];
1176 assert_eq!(properties["express"]["oneOf"][0]["type"], "boolean");
1177 assert_eq!(properties["express"]["examples"], json!([true]));
1178 assert_eq!(properties["code"]["type"], "string");
1179 assert_eq!(properties["code"]["enum"], json!(["A", "B"]));
1180 assert_eq!(properties["label"]["minLength"], 3);
1181 assert_eq!(properties["label"]["maxLength"], 3);
1182 assert_eq!(properties["quantity"]["pattern"], number_pattern());
1183 assert_eq!(
1184 properties["price"]["oneOf"][1]["propertyNames"]["enum"],
1185 json!(["eur", "cent"])
1186 );
1187 assert_eq!(
1188 properties["rate"]["oneOf"][1]["propertyNames"]["enum"],
1189 json!(["percent", "permille"])
1190 );
1191 assert_eq!(
1192 properties["bps"]["oneOf"][1]["propertyNames"]["enum"],
1193 json!(["percent", "permille", "basis_points"])
1194 );
1195 assert_eq!(properties["start"]["type"], "string");
1196 assert!(properties["start"].get("pattern").is_none());
1197 assert!(properties["start"].get("format").is_none());
1198 assert_eq!(properties["window"]["type"], "string");
1199 assert!(properties["window"]["pattern"].is_string());
1200 let tags = &properties["tags"];
1201 assert_eq!(tags["type"], "array");
1202 assert_eq!(
1203 tags["items"],
1204 json!({ "type": "string", "pattern": number_pattern() })
1205 );
1206 assert_eq!(
1207 tags["maxItems"],
1208 crate::ResourceLimits::default().max_list_rows
1209 );
1210 assert_eq!(tags["default"], json!(["5", "10"]));
1211 }
1212
1213 #[test]
1214 fn x_lemma_type_omits_what_standard_keywords_state() {
1215 let (_, _, document) = document();
1216 for (name, property) in document["properties"].as_object().expect("properties") {
1217 let lemma_type = &property["x-lemma-type"];
1218 assert!(lemma_type.get("help").is_none(), "{name}: {lemma_type}");
1219 assert!(lemma_type.get("options").is_none(), "{name}: {lemma_type}");
1220 assert!(lemma_type.get("length").is_none(), "{name}: {lemma_type}");
1221 if let Some(element) = lemma_type.get("element") {
1222 assert!(element.get("help").is_none(), "{name}: {element}");
1223 }
1224 assert!(property.get("x-lemma-path").is_some(), "{name}");
1225 assert!(property.get("x-lemma-needed-by-rules").is_some(), "{name}");
1226 }
1227 assert_eq!(
1228 document["properties"]["price"]["x-lemma-type"]["kind"],
1229 "measure"
1230 );
1231 assert!(document["properties"]["price"]["x-lemma-type"]["units"].is_array());
1232 }
1233
1234 fn value_json_has_reference(value: &JsonValue) -> bool {
1235 if value.get("reference").is_some() {
1236 return true;
1237 }
1238 if let Some(fields) = value.get("fields").and_then(|v| v.as_object()) {
1239 if fields.values().any(value_json_has_reference) {
1240 return true;
1241 }
1242 }
1243 if let Some(list) = value.get("list").and_then(|v| v.as_array()) {
1244 if list.iter().any(value_json_has_reference) {
1245 return true;
1246 }
1247 }
1248 false
1249 }
1250
1251 fn evaluate_data_json_of(value: &JsonValue) -> JsonValue {
1253 if let Some(boolean) = value.get("boolean") {
1254 return boolean.clone();
1255 }
1256 if let Some(items) = value.get("list") {
1257 return JsonValue::Array(
1258 items
1259 .as_array()
1260 .expect("list")
1261 .iter()
1262 .map(evaluate_data_json_of)
1263 .collect(),
1264 );
1265 }
1266 if let Some(fields) = value.get("fields") {
1267 return JsonValue::Object(
1268 fields
1269 .as_object()
1270 .expect("fields")
1271 .iter()
1272 .filter(|(_, field)| field.get("reference").is_none())
1273 .map(|(name, field)| (name.clone(), evaluate_data_json_of(field)))
1274 .collect(),
1275 );
1276 }
1277 if value.get("reference").is_some() {
1278 panic!("BUG: reference field is not a run-data literal");
1279 }
1280 value["result"].clone()
1281 }
1282
1283 #[test]
1284 fn instance_list_with_references_has_value_but_no_default() {
1285 let mut engine = Engine::new();
1286 engine
1287 .load([(
1288 SourceType::Volatile,
1289 r#"
1290spec processing
1291data type: text
1292data weight: number
1293data enabled: boolean
1294spec line_item
1295data weight: number
1296data roast: boolean
1297uses processing list
1298 -> with weight: weight, type: "roast", enabled: roast
1299rule cost: processing -> where enabled -> count
1300"#
1301 .to_string(),
1302 )])
1303 .expect("load");
1304 let show = engine.show(None, "line_item", None).expect("show");
1305 let processing = show.data.get("processing").expect("processing");
1306 let value = processing.value.as_ref().expect("value");
1307 assert!(
1308 rule_result_value_has_reference(value),
1309 "expected reference in value: {value:?}"
1310 );
1311 let document = show_document(&engine, &show);
1312 let property = &document["properties"]["processing"];
1313 assert!(property.get("x-lemma-value").is_some(), "{property}");
1314 assert!(
1315 property.get("default").is_none(),
1316 "default must be omitted when fields reference data: {property}"
1317 );
1318 }
1319
1320 #[test]
1321 fn default_and_examples_agree_with_typed_value_and_suggestion() {
1322 let (_, _, document) = document();
1323 let mut checked = 0;
1324 for (name, property) in document["properties"].as_object().expect("properties") {
1325 match (property.get("default"), property.get("x-lemma-value")) {
1326 (Some(default), Some(value)) => {
1327 assert_eq!(default, &evaluate_data_json_of(value), "{name}");
1328 checked += 1;
1329 }
1330 (None, Some(value)) => {
1331 assert!(
1332 value_json_has_reference(value),
1333 "{name}: value without default must carry a reference field"
1334 );
1335 }
1336 (None, None) => {}
1337 other => {
1338 panic!("{name}: default and x-lemma-value disagree on presence: {other:?}")
1339 }
1340 }
1341 match (property.get("examples"), property.get("x-lemma-suggestion")) {
1342 (Some(examples), Some(suggestion)) => {
1343 assert_eq!(
1344 examples,
1345 &json!([evaluate_data_json_of(suggestion)]),
1346 "{name}"
1347 );
1348 checked += 1;
1349 }
1350 (None, None) => {}
1351 other => panic!(
1352 "{name}: examples and x-lemma-suggestion disagree on presence: {other:?}"
1353 ),
1354 }
1355 }
1356 assert_eq!(checked, 12);
1357 }
1358
1359 #[test]
1360 fn emitted_literals_match_pattern_and_denote_their_value() {
1361 let (_, show, document) = document();
1362 let mut checked = 0;
1363 for (name, property) in document["properties"].as_object().expect("properties") {
1364 let Some(pattern) = property["pattern"]
1365 .as_str()
1366 .or_else(|| property["oneOf"][0]["pattern"].as_str())
1367 else {
1368 continue;
1369 };
1370 let specification = &show.data[name].lemma_type.specifications;
1371 let mut literals: Vec<(&str, &JsonValue)> = Vec::new();
1372 if let Some(default) = property.get("default") {
1373 literals.push((
1374 default.as_str().expect("scalar default"),
1375 &property["x-lemma-value"],
1376 ));
1377 }
1378 if let Some(examples) = property.get("examples") {
1379 literals.push((
1380 examples[0].as_str().expect("scalar example"),
1381 &property["x-lemma-suggestion"],
1382 ));
1383 }
1384 for (literal, value) in literals {
1385 assert!(
1386 pattern_accepts(pattern, literal),
1387 "{name}: '{literal}' vs {pattern}"
1388 );
1389 assert_literal_denotes(name, literal, value, specification);
1390 checked += 1;
1391 }
1392 }
1393 assert_eq!(checked, 8);
1394 }
1395
1396 #[test]
1397 fn pattern_and_parser_agree_on_samples() {
1398 let (_, show, document) = document();
1399 let cases: [(&str, &[&str], &[&str]); 4] = [
1400 (
1401 "quantity",
1402 &["0", "12.5", "-3", "1_000", "1_000.25"],
1403 &["1,000", "12 eur", "abc", ""],
1404 ),
1405 (
1406 "price",
1407 &["10 eur", "2.5 cent", "-1 eur", "1_000 eur"],
1408 &["84eur", "10 usd", "10", "eur 10", "10 eur extra"],
1409 ),
1410 (
1411 "rate",
1412 &["21%", "5%%", "0.5 percent", "2 permille"],
1413 &["21", "21 %", "21 basis_points", "%"],
1414 ),
1415 (
1416 "bps",
1417 &["500 basis_points", "5%", "5%%", "5 percent"],
1418 &["500 bps", "5", "5 basis_point"],
1419 ),
1420 ];
1421 for (name, accepted, rejected) in cases {
1422 let property = &document["properties"][name];
1423 let pattern = property["pattern"]
1424 .as_str()
1425 .or_else(|| property["oneOf"][0]["pattern"].as_str())
1426 .expect("pattern");
1427 let specification = &show.data[name].lemma_type.specifications;
1428 for literal in accepted {
1429 assert!(
1430 pattern_accepts(pattern, literal),
1431 "{name}: pattern must accept '{literal}'"
1432 );
1433 assert!(
1434 parses(literal, specification),
1435 "{name}: parser must accept '{literal}'"
1436 );
1437 }
1438 for literal in rejected {
1439 assert!(
1440 !pattern_accepts(pattern, literal),
1441 "{name}: pattern must reject '{literal}'"
1442 );
1443 assert!(
1444 !parses(literal, specification),
1445 "{name}: parser must reject '{literal}'"
1446 );
1447 }
1448 }
1449 }
1450
1451 fn identity(repository: Option<&str>, spec: &str) -> SpecIdentity {
1452 SpecIdentity {
1453 repository: repository.map(str::to_string),
1454 spec: spec.to_string(),
1455 effective: crate::parsing::ast::EffectiveDate::Origin,
1456 }
1457 }
1458
1459 #[test]
1460 fn row_def_name_is_injective_and_component_safe() {
1461 assert_eq!(row_def_name(&identity(None, "line")), "LemmaRow_line");
1462 assert_eq!(
1463 row_def_name(&identity(None, "tax_bracket")),
1464 "LemmaRow_tax__bracket"
1465 );
1466 let names = [
1467 row_def_name(&identity(Some("@org/repo"), "line")),
1468 row_def_name(&identity(None, "_org_repo_line")),
1469 row_def_name(&identity(None, "line")),
1470 ];
1471 for name in &names {
1472 assert!(
1473 name.chars()
1474 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-'),
1475 "{name}"
1476 );
1477 }
1478 assert_ne!(names[0], names[1]);
1479 assert_ne!(names[0], names[2]);
1480 }
1481}