1use crate::error::Error;
2use crate::limits::ResourceLimits;
3
4pub mod ast;
5pub mod lexer;
6pub mod parser;
7pub mod source;
8
9pub use ast::*;
10pub use parser::ParseResult;
11
12pub fn parse(
13 content: &str,
14 source_type: source::SourceType,
15 limits: &ResourceLimits,
16) -> Result<ParseResult, Error> {
17 parser::parse(content, source_type, limits)
18}
19
20#[cfg(test)]
25mod tests {
26 use super::{parse, ArithmeticComputation, Expression, ExpressionKind};
27 use crate::formatting::format_parse_result;
28 use crate::Error;
29 use crate::ResourceLimits;
30
31 #[test]
32 fn parse_empty_input_returns_no_specs() {
33 let result = parse(
34 "",
35 crate::parsing::source::SourceType::Volatile,
36 &ResourceLimits::default(),
37 )
38 .unwrap()
39 .into_flattened_specs();
40 assert_eq!(result.len(), 0);
41 }
42
43 #[test]
44 fn parse_workspace_file_yields_expected_spec_datas_and_rules() {
45 let input = r#"spec person
46data name: "John Doe"
47rule adult: true"#;
48 let result = parse(
49 input,
50 crate::parsing::source::SourceType::Volatile,
51 &ResourceLimits::default(),
52 )
53 .unwrap()
54 .into_flattened_specs();
55 assert_eq!(result.len(), 1);
56 assert_eq!(result[0].name, "person");
57 assert_eq!(result[0].data.len(), 1);
58 assert_eq!(result[0].rules.len(), 1);
59 assert_eq!(result[0].rules[0].name, "adult");
60 }
61
62 #[test]
63 fn mixing_data_and_rules_is_collected_into_spec() {
64 let input = r#"spec test
65data name: "John"
66rule is_adult: age >= 18
67data age: 25
68rule can_drink: age >= 21
69data status: "active"
70rule is_eligible: is_adult and status is "active""#;
71
72 let result = parse(
73 input,
74 crate::parsing::source::SourceType::Volatile,
75 &ResourceLimits::default(),
76 )
77 .unwrap()
78 .into_flattened_specs();
79 assert_eq!(result.len(), 1);
80 assert_eq!(result[0].data.len(), 3);
81 assert_eq!(result[0].rules.len(), 3);
82 }
83
84 #[test]
85 fn parse_simple_spec_collects_data() {
86 let input = r#"spec person
87data name: "John"
88data age: 25"#;
89 let result = parse(
90 input,
91 crate::parsing::source::SourceType::Volatile,
92 &ResourceLimits::default(),
93 )
94 .unwrap()
95 .into_flattened_specs();
96 assert_eq!(result.len(), 1);
97 assert_eq!(result[0].name, "person");
98 assert_eq!(result[0].data.len(), 2);
99 }
100
101 #[test]
102 fn parse_dotted_spec_name() {
103 let input = r#"spec contracts.employment.jack
104data name: "Jack""#;
105 let result = parse(
106 input,
107 crate::parsing::source::SourceType::Volatile,
108 &ResourceLimits::default(),
109 )
110 .unwrap()
111 .into_flattened_specs();
112 assert_eq!(result.len(), 1);
113 assert_eq!(result[0].name, "contracts.employment.jack");
114 }
115
116 #[test]
117 fn parse_slashed_spec_name() {
118 let input = "spec contracts/employment/jack\ndata x: 1";
119 let result = parse(
120 input,
121 crate::parsing::source::SourceType::Volatile,
122 &ResourceLimits::default(),
123 )
124 .unwrap()
125 .into_flattened_specs();
126 assert_eq!(result.len(), 1);
127 assert_eq!(result[0].name, "contracts/employment/jack");
128 }
129
130 #[test]
131 fn parse_spec_name_no_version_tag() {
132 let input = "spec myspec\nrule x: 1";
133 let result = parse(
134 input,
135 crate::parsing::source::SourceType::Volatile,
136 &ResourceLimits::default(),
137 )
138 .unwrap()
139 .into_flattened_specs();
140 assert_eq!(result.len(), 1);
141 assert_eq!(result[0].name, "myspec");
142 assert_eq!(result[0].effective_from(), None);
143 }
144
145 #[test]
146 fn parse_commentary_block_is_attached_to_spec() {
147 let input = r#"spec person
148"""
149This is a markdown comment
150uses **bold** text
151"""
152data name: "John""#;
153 let result = parse(
154 input,
155 crate::parsing::source::SourceType::Volatile,
156 &ResourceLimits::default(),
157 )
158 .unwrap()
159 .into_flattened_specs();
160 assert_eq!(result.len(), 1);
161 assert!(result[0].commentary.is_some());
162 assert!(result[0].commentary.as_ref().unwrap().contains("**bold**"));
163 }
164
165 #[test]
166 fn parse_spec_with_rule_collects_rule() {
167 let input = r#"spec person
168rule is_adult: age >= 18"#;
169 let result = parse(
170 input,
171 crate::parsing::source::SourceType::Volatile,
172 &ResourceLimits::default(),
173 )
174 .unwrap()
175 .into_flattened_specs();
176 assert_eq!(result.len(), 1);
177 assert_eq!(result[0].rules.len(), 1);
178 assert_eq!(result[0].rules[0].name, "is_adult");
179 }
180
181 #[test]
182 fn parse_multiple_specs_returns_all_specs() {
183 let input = r#"spec person
184data name: "John"
185
186spec company
187data name: "Acme Corp""#;
188 let result = parse(
189 input,
190 crate::parsing::source::SourceType::Volatile,
191 &ResourceLimits::default(),
192 )
193 .unwrap()
194 .into_flattened_specs();
195 assert_eq!(result.len(), 2);
196 assert_eq!(result[0].name, "person");
197 assert_eq!(result[1].name, "company");
198 }
199
200 #[test]
201 fn parse_allows_duplicate_data_names() {
202 let input = r#"spec person
203data name: "John"
204data name: "Jane""#;
205 let result = parse(
206 input,
207 crate::parsing::source::SourceType::Volatile,
208 &ResourceLimits::default(),
209 );
210 assert!(
211 result.is_ok(),
212 "Parser should succeed even with duplicate data"
213 );
214 }
215
216 #[test]
217 fn parse_allows_duplicate_rule_names() {
218 let input = r#"spec person
219rule is_adult: age >= 18
220rule is_adult: age >= 21"#;
221 let result = parse(
222 input,
223 crate::parsing::source::SourceType::Volatile,
224 &ResourceLimits::default(),
225 );
226 assert!(
227 result.is_ok(),
228 "Parser should succeed even with duplicate rules"
229 );
230 }
231
232 #[test]
233 fn parse_rejects_malformed_input() {
234 let input = "invalid syntax here";
235 let result = parse(
236 input,
237 crate::parsing::source::SourceType::Volatile,
238 &ResourceLimits::default(),
239 );
240 assert!(result.is_err());
241 }
242
243 #[test]
244 fn parse_handles_whitespace_variants_in_expressions() {
245 let test_cases = vec![
246 ("spec test\nrule test: 2+3", "no spaces in arithmetic"),
247 ("spec test\nrule test: age>=18", "no spaces in comparison"),
248 (
249 "spec test\nrule test: age >= 18 and salary>50000",
250 "spaces around and keyword",
251 ),
252 (
253 "spec test\nrule test: age >= 18 and salary > 50000",
254 "extra spaces",
255 ),
256 (
257 "spec test\nrule test: \n age >= 18 \n and \n salary > 50000",
258 "newlines in expression",
259 ),
260 ];
261
262 for (input, description) in test_cases {
263 let result = parse(
264 input,
265 crate::parsing::source::SourceType::Volatile,
266 &ResourceLimits::default(),
267 );
268 assert!(
269 result.is_ok(),
270 "Failed to parse {} ({}): {:?}",
271 input,
272 description,
273 result.err()
274 );
275 }
276 }
277
278 #[test]
279 fn parse_error_cases_are_rejected() {
280 let error_cases = vec![
281 (
282 "spec test\ndata name: \"unclosed string",
283 "unclosed string literal",
284 ),
285 ("spec test\nrule test: (2 + 3", "unclosed parenthesis"),
286 ("spec test\nrule test: 2 + 3)", "extra closing paren"),
287 ("spec test\ndata spec: 123", "reserved keyword as data name"),
288 (
289 "spec test\nrule rule: true",
290 "reserved keyword as rule name",
291 ),
292 ];
293
294 for (input, description) in error_cases {
295 let result = parse(
296 input,
297 crate::parsing::source::SourceType::Volatile,
298 &ResourceLimits::default(),
299 );
300 assert!(
301 result.is_err(),
302 "Expected error for {} but got success",
303 description
304 );
305 }
306 }
307
308 #[test]
309 fn parse_duration_literals_in_rules() {
310 let test_cases = vec![
311 ("2 years", "years"),
312 ("6 months", "months"),
313 ("52 weeks", "weeks"),
314 ("365 days", "days"),
315 ("24 hours", "hours"),
316 ("60 minutes", "minutes"),
317 ("3600 seconds", "seconds"),
318 ("1000 milliseconds", "milliseconds"),
319 ("500000 microseconds", "microseconds"),
320 ("50 percent", "percent"),
321 ];
322
323 for (expr, description) in test_cases {
324 let input = format!("spec test\nrule test: {}", expr);
325 let result = parse(
326 &input,
327 crate::parsing::source::SourceType::Volatile,
328 &ResourceLimits::default(),
329 );
330 assert!(
331 result.is_ok(),
332 "Failed to parse literal {} ({}): {:?}",
333 expr,
334 description,
335 result.err()
336 );
337 }
338 }
339
340 #[test]
341 fn parse_comparisons_with_duration_unit_conversions() {
342 let test_cases = vec![
343 (
344 "(duration as hours) > 2",
345 "duration conversion in comparison with parens",
346 ),
347 (
348 "(meeting_time as minutes) >= 30",
349 "duration conversion with gte",
350 ),
351 (
352 "(project_length as days) < 100",
353 "duration conversion with lt",
354 ),
355 (
356 "(delay as seconds) is 60",
357 "duration conversion with equality",
358 ),
359 (
360 "(1 hours) > (30 minutes)",
361 "duration conversions on both sides",
362 ),
363 (
364 "duration as hours > 2",
365 "duration conversion without parens",
366 ),
367 (
368 "meeting_time as seconds > 3600",
369 "variable duration conversion in comparison",
370 ),
371 (
372 "project_length as days > deadline_days",
373 "two variables with duration conversion",
374 ),
375 (
376 "duration as hours >= 1 and duration as hours <= 8",
377 "multiple duration comparisons",
378 ),
379 (
380 "(2024-06-01...2024-06-15) as days as number >= 7",
381 "chained as conversion before comparison",
382 ),
383 ("duration as hours as number > 2", "chained as on duration"),
384 ];
385
386 for (expr, description) in test_cases {
387 let input = format!("spec test\nrule test: {}", expr);
388 let result = parse(
389 &input,
390 crate::parsing::source::SourceType::Volatile,
391 &ResourceLimits::default(),
392 );
393 assert!(
394 result.is_ok(),
395 "Failed to parse {} ({}): {:?}",
396 expr,
397 description,
398 result.err()
399 );
400 }
401 }
402
403 #[test]
404 fn parse_rejects_token_after_unit_conversion() {
405 let result = parse(
406 "spec test\nuses lemma units\nrule ok: (2024-06-01...2024-06-15) as days foo",
407 crate::parsing::source::SourceType::Volatile,
408 &ResourceLimits::default(),
409 );
410 let err = result.expect_err("expected parse error");
411 let msg = err.to_string();
412 assert!(
413 msg.contains("Unexpected token") && msg.contains("foo"),
414 "expected error at 'foo', got: {}",
415 msg
416 );
417 assert!(
418 !msg.contains("Expected 'data'"),
419 "should not defer to spec-level error, got: {}",
420 msg
421 );
422 }
423
424 #[test]
425 fn parse_unit_conversion_before_next_spec() {
426 let result = parse(
427 r#"spec pricing
428rule hourly_rate: 150 eur
429 unless loyalty is "silver" then 140 eur
430 unless loyalty is "gold" then 125 usd as eur
431
432spec other
433rule x: 1"#,
434 crate::parsing::source::SourceType::Volatile,
435 &ResourceLimits::default(),
436 );
437 assert!(
438 result.is_ok(),
439 "unless branch ending with 'as' must parse before next spec: {:?}",
440 result.err()
441 );
442 }
443
444 #[test]
445 fn parse_unit_conversion_before_sibling_rule() {
446 let result = parse(
447 r#"spec s
448rule a: 100 usd as eur
449rule b: 1"#,
450 crate::parsing::source::SourceType::Volatile,
451 &ResourceLimits::default(),
452 );
453 assert!(
454 result.is_ok(),
455 "rule ending with 'as' must parse before sibling rule: {:?}",
456 result.err()
457 );
458 }
459
460 #[test]
461 fn parse_unit_conversion_before_uses() {
462 let result = parse(
463 r#"spec s
464rule rate: 10 usd as eur
465uses lemma units
466rule hours: 1 hour"#,
467 crate::parsing::source::SourceType::Volatile,
468 &ResourceLimits::default(),
469 );
470 assert!(
471 result.is_ok(),
472 "rule ending with 'as' must parse before uses: {:?}",
473 result.err()
474 );
475 }
476
477 #[test]
479 fn parse_unit_conversion_before_expression_boundaries() {
480 let cases: &[(&str, &str)] = &[
481 (
482 "sibling data",
483 r#"spec s
484rule rate: 10 usd as eur
485data price: 100 eur"#,
486 ),
487 (
488 "sibling with",
489 r#"spec s
490rule rate: 10 usd as eur
491with cfg.rate: rate"#,
492 ),
493 (
494 "sibling meta",
495 r#"spec s
496rule rate: 10 usd as eur
497meta version: 1"#,
498 ),
499 (
500 "another unless",
501 r#"spec s
502rule rate: 10 usd
503 unless active then 5 usd as eur
504 unless premium then 3 usd as eur"#,
505 ),
506 (
507 "eof",
508 r#"spec s
509rule rate: 10 usd as eur"#,
510 ),
511 (
512 "next repo",
513 r#"spec s
514rule rate: 10 usd as eur
515
516repo other
517spec t
518rule x: 1"#,
519 ),
520 (
521 "unless then before next unless",
522 r#"spec s
523rule rate: 10 usd
524 unless a then 1 usd as eur
525 unless b then 2"#,
526 ),
527 (
528 "chained as before sibling rule",
529 r#"spec s
530rule rate: (2024-01-01...2024-01-02) as days as number
531rule other: 1"#,
532 ),
533 ];
534
535 for (label, source) in cases {
536 let result = parse(
537 source,
538 crate::parsing::source::SourceType::Volatile,
539 &ResourceLimits::default(),
540 );
541 assert!(
542 result.is_ok(),
543 "unit conversion before {label} must parse: {:?}",
544 result.err()
545 );
546 }
547 }
548
549 #[test]
550 fn parse_rejects_plain_number_plus_converted_operand() {
551 let result = parse(
552 r#"spec test
553data c: measure
554 -> unit eur 1
555 -> unit usd 0.84
556rule z: 5 + c as usd"#,
557 crate::parsing::source::SourceType::Volatile,
558 &ResourceLimits::default(),
559 );
560 let err = result.expect_err("expected parse error for 5 + c as usd");
561 let msg = err.to_string();
562 assert!(
563 msg.contains("plain number") || msg.contains("each operand"),
564 "expected conversion-before-+ error, got: {msg}"
565 );
566 }
567
568 #[test]
569 fn parse_accepts_conversion_on_each_additive_operand() {
570 let cases: &[(&str, &str)] = &[
571 (
572 "money",
573 r#"spec test
574data c: measure
575 -> unit eur 1
576 -> unit usd 0.84
577rule z: 5 as usd + c as usd"#,
578 ),
579 (
580 "duration + literal",
581 r#"spec test
582uses lemma units
583rule z: duration as hours + 1"#,
584 ),
585 (
586 "duration + comparison",
587 r#"spec test
588uses lemma units
589data duration: units.duration
590 -> default 1 hour
591rule z: duration as hours + 1 > 0"#,
592 ),
593 (
594 "date range + ref",
595 r#"spec test
596uses lemma units
597data age: date range
598data c: measure
599 -> unit eur 1
600rule z: age as days + c"#,
601 ),
602 ];
603 for (label, source) in cases {
604 let result = parse(
605 source,
606 crate::parsing::source::SourceType::Volatile,
607 &ResourceLimits::default(),
608 );
609 assert!(
610 result.is_ok(),
611 "expected {label} to parse, got: {:?}",
612 result.err()
613 );
614 }
615 }
616
617 fn rule_expression(source: &str, rule_name: &str) -> Expression {
618 let parsed = parse(
619 source,
620 crate::parsing::source::SourceType::Volatile,
621 &ResourceLimits::default(),
622 )
623 .expect("expected parse");
624 let spec = parsed
625 .flatten_specs()
626 .into_iter()
627 .next()
628 .expect("expected one spec");
629 spec.rules
630 .iter()
631 .find(|rule| rule.name == rule_name)
632 .unwrap_or_else(|| panic!("rule '{rule_name}' not found"))
633 .expression
634 .clone()
635 }
636
637 fn assert_multiply_range_side(expression: &Expression, range_on_left: bool, label: &str) {
638 let ExpressionKind::Arithmetic(left, ArithmeticComputation::Multiply, right) =
639 &expression.kind
640 else {
641 panic!("{label}: expected Multiply, got {:?}", expression.kind);
642 };
643 let (range, other) = if range_on_left {
644 (left.as_ref(), right.as_ref())
645 } else {
646 (right.as_ref(), left.as_ref())
647 };
648 assert!(
649 matches!(range.kind, ExpressionKind::RangeLiteral(..)),
650 "{label}: expected RangeLiteral on {} of *, got {:?}",
651 if range_on_left { "left" } else { "right" },
652 range.kind
653 );
654 assert!(
655 !matches!(other.kind, ExpressionKind::RangeLiteral(..)),
656 "{label}: expected non-range on other side of *"
657 );
658 }
659
660 #[test]
661 fn parse_range_binds_tighter_than_multiply() {
662 let base = r#"spec test
663uses lemma units
664data rate: measure -> unit eur 1
665data period_start: 2026-01-01
666data period_end: 2026-01-02
667"#;
668 assert_multiply_range_side(
669 &rule_expression(
670 &format!("{base}rule rhs: rate * period_start...period_end"),
671 "rhs",
672 ),
673 false,
674 "rate * period_start...period_end",
675 );
676 assert_multiply_range_side(
677 &rule_expression(
678 &format!("{base}rule lhs: period_start...period_end * rate"),
679 "lhs",
680 ),
681 true,
682 "period_start...period_end * rate",
683 );
684 }
685
686 #[test]
687 fn parse_range_additive_right_endpoint_binds_inside_literal() {
688 let expression = rule_expression(
689 r#"spec test
690uses lemma units
691data start: date
692data length: units.duration
693rule valid: now in start...start + length"#,
694 "valid",
695 );
696 let ExpressionKind::RangeContainment(value, range) = &expression.kind else {
697 panic!("expected RangeContainment, got {:?}", expression.kind);
698 };
699 assert!(
700 matches!(value.kind, ExpressionKind::Now),
701 "expected now as containment value, got {:?}",
702 value.kind
703 );
704 let ExpressionKind::RangeLiteral(left, right) = &range.kind else {
705 panic!(
706 "expected RangeLiteral as containment range, got {:?}",
707 range.kind
708 );
709 };
710 assert!(
711 matches!(left.kind, ExpressionKind::Reference(..)),
712 "expected start reference as left endpoint, got {:?}",
713 left.kind
714 );
715 let ExpressionKind::Arithmetic(add_left, ArithmeticComputation::Add, add_right) =
716 &right.kind
717 else {
718 panic!(
719 "expected start + length as right endpoint, got {:?}",
720 right.kind
721 );
722 };
723 assert!(
724 matches!(add_left.kind, ExpressionKind::Reference(..)),
725 "expected start reference in right endpoint add, got {:?}",
726 add_left.kind
727 );
728 assert!(
729 matches!(add_right.kind, ExpressionKind::Reference(..)),
730 "expected length reference in right endpoint add, got {:?}",
731 add_right.kind
732 );
733 }
734
735 #[test]
736 fn parse_range_multiply_with_conversion_without_inner_parens() {
737 let expression = rule_expression(
738 r#"spec test
739uses lemma units
740data money: measure -> unit eur 1
741data rate: measure -> unit eur_per_hour eur/hour
742data hourly_rate: 50 eur_per_hour
743data period_start: 2026-01-01
744data period_end: 2026-01-02
745rule pay: (hourly_rate * period_start...period_end) as eur"#,
746 "pay",
747 );
748 let ExpressionKind::UnitConversion(inner, _) = &expression.kind else {
749 panic!("expected UnitConversion, got {:?}", expression.kind);
750 };
751 assert_multiply_range_side(inner, false, "pay");
752 }
753
754 #[test]
755 fn parse_error_includes_attribute_and_parse_error_spec_name() {
756 let result = parse(
757 r#"
758spec test
759data name: "Unclosed string
760data age: 25
761"#,
762 crate::parsing::source::SourceType::Volatile,
763 &ResourceLimits::default(),
764 );
765
766 match result {
767 Err(Error::Parsing(details)) => {
768 let src = details
769 .source
770 .as_ref()
771 .expect("BUG: parsing errors always have source");
772 assert_eq!(
773 src.source_type,
774 crate::parsing::source::SourceType::Volatile
775 );
776 }
777 Err(e) => panic!("Expected Parse error, got: {e:?}"),
778 Ok(_) => panic!("Expected parse error for unclosed string"),
779 }
780 }
781
782 #[test]
783 fn parse_single_spec_file() {
784 let input = r#"spec somespec
785data name: "Alice""#;
786 let parsed = parse(
787 input,
788 crate::parsing::source::SourceType::Volatile,
789 &ResourceLimits::default(),
790 )
791 .unwrap();
792 let specs = parsed.flatten_specs();
793 assert_eq!(specs.len(), 1);
794 assert_eq!(specs[0].name, "somespec");
795 }
796
797 #[test]
798 fn parse_uses_registry_spec_explicit_alias() {
799 let input = r#"spec example
800uses external: @user/workspace somespec"#;
801 let specs = parse(
802 input,
803 crate::parsing::source::SourceType::Volatile,
804 &ResourceLimits::default(),
805 )
806 .unwrap()
807 .into_flattened_specs();
808 assert_eq!(specs.len(), 1);
809 assert_eq!(specs[0].data.len(), 1);
810 match &specs[0].data[0].value {
811 crate::parsing::ast::DataValue::Import(spec_ref) => {
812 assert_eq!(spec_ref.name, "somespec");
813 let repository_hdr = spec_ref
814 .repository
815 .as_ref()
816 .expect("expected repository qualifier");
817 assert_eq!(repository_hdr.name, "@user/workspace");
818 }
819 other => panic!("Expected Import, got: {:?}", other),
820 }
821 }
822
823 #[test]
824 fn parse_multiple_specs_cross_reference_in_file() {
825 let input = r#"spec spec_a
826data x: 10
827
828spec spec_b
829data y: 20
830uses a: spec_a"#;
831 let parsed = parse(
832 input,
833 crate::parsing::source::SourceType::Volatile,
834 &ResourceLimits::default(),
835 )
836 .unwrap();
837 let specs = parsed.flatten_specs();
838 assert_eq!(specs.len(), 2);
839 assert_eq!(specs[0].name, "spec_a");
840 assert_eq!(specs[1].name, "spec_b");
841 }
842
843 #[test]
844 fn parse_uses_registry_spec_default_alias() {
845 let input = "spec example\nuses @owner/repo somespec";
846 let specs = parse(
847 input,
848 crate::parsing::source::SourceType::Volatile,
849 &ResourceLimits::default(),
850 )
851 .unwrap()
852 .into_flattened_specs();
853 match &specs[0].data[0].value {
854 crate::parsing::ast::DataValue::Import(spec_ref) => {
855 assert_eq!(spec_ref.name, "somespec");
856 let repository_hdr = spec_ref
857 .repository
858 .as_ref()
859 .expect("expected repository qualifier");
860 assert_eq!(repository_hdr.name, "@owner/repo");
861 }
862 other => panic!("Expected Import, got: {:?}", other),
863 }
864 }
865
866 #[test]
867 fn parse_uses_local_spec_default_alias() {
868 let input = "spec example\nuses myspec";
869 let specs = parse(
870 input,
871 crate::parsing::source::SourceType::Volatile,
872 &ResourceLimits::default(),
873 )
874 .unwrap()
875 .into_flattened_specs();
876 match &specs[0].data[0].value {
877 crate::parsing::ast::DataValue::Import(spec_ref) => {
878 assert_eq!(spec_ref.name, "myspec");
879 assert!(
880 spec_ref.repository.is_none(),
881 "same-repository reference must omit repository qualifier"
882 );
883 }
884 other => panic!("Expected Import, got: {:?}", other),
885 }
886 }
887
888 #[test]
889 fn parse_spec_name_with_trailing_dot_is_error() {
890 let input = "spec myspec.\ndata x: 1";
891 let result = parse(
892 input,
893 crate::parsing::source::SourceType::Volatile,
894 &ResourceLimits::default(),
895 );
896 assert!(
897 result.is_err(),
898 "Trailing dot after spec name should be a parse error"
899 );
900 }
901
902 #[test]
903 fn parse_multiple_specs_in_same_file() {
904 let input = "spec myspec_a\nrule x: 1\n\nspec myspec_b\nrule x: 2";
905 let result = parse(
906 input,
907 crate::parsing::source::SourceType::Volatile,
908 &ResourceLimits::default(),
909 )
910 .unwrap()
911 .into_flattened_specs();
912 assert_eq!(result.len(), 2);
913 assert_eq!(result[0].name, "myspec_a");
914 assert_eq!(result[1].name, "myspec_b");
915 }
916
917 #[test]
918 fn parse_uses_accepts_name_only() {
919 let input = "spec consumer\nuses other";
920 let result = parse(
921 input,
922 crate::parsing::source::SourceType::Volatile,
923 &ResourceLimits::default(),
924 );
925 assert!(result.is_ok(), "uses name should parse");
926 let specs = result.unwrap().into_flattened_specs();
927 let spec_ref = match &specs[0].data[0].value {
928 crate::parsing::ast::DataValue::Import(r) => r,
929 _ => panic!("expected Import"),
930 };
931 assert_eq!(spec_ref.name, "other");
932 }
933
934 #[test]
935 fn parse_uses_bare_year_effective() {
936 let input = "spec consumer\nuses other 2026";
937 let result = parse(
938 input,
939 crate::parsing::source::SourceType::Volatile,
940 &ResourceLimits::default(),
941 )
942 .unwrap();
943 let specs = result.into_flattened_specs();
944 let spec_ref = match &specs[0].data[0].value {
945 crate::parsing::ast::DataValue::Import(r) => r,
946 _ => panic!("expected Import"),
947 };
948 assert_eq!(spec_ref.name, "other");
949 let eff = spec_ref.effective.as_ref().expect("effective");
950 assert_eq!(eff.year, 2026);
951 assert_eq!(eff.month, 1);
952 assert_eq!(eff.day, 1);
953 }
954
955 #[test]
956 fn parse_uses_registry_spec_ref_records_repository_and_target_spans() {
957 let input = "spec consumer\nuses @iso/countries alpha2 2026";
958 let result = parse(
959 input,
960 crate::parsing::source::SourceType::Volatile,
961 &ResourceLimits::default(),
962 )
963 .unwrap();
964 let spec = &result.flatten_specs()[0];
965 let sr = match &spec.data[0].value {
966 crate::parsing::ast::DataValue::Import(r) => r,
967 _ => panic!("expected Import"),
968 };
969 let rs = sr
970 .repository_span
971 .as_ref()
972 .expect("repository_span should be set for @-qualified uses");
973 let ts = sr
974 .target_span
975 .as_ref()
976 .expect("target_span should cover spec name and effective");
977 assert_eq!(&input[rs.start..rs.end], "@iso/countries");
978 assert_eq!(&input[ts.start..ts.end], "alpha2 2026");
979 }
980
981 #[test]
982 fn parse_uses_alias_no_comma_continuation() {
983 let input = "spec consumer\nuses alias: pricing retail\ndata x: 1";
984 let result = parse(
985 input,
986 crate::parsing::source::SourceType::Volatile,
987 &ResourceLimits::default(),
988 )
989 .unwrap();
990 let data = &result.flatten_specs()[0].data;
991 assert_eq!(data.len(), 2);
992 assert_eq!(data[0].reference.name, "alias");
993 let sr = match &data[0].value {
994 crate::parsing::ast::DataValue::Import(r) => r,
995 _ => panic!("expected Import"),
996 };
997 assert_eq!(sr.name, "retail");
998 let repository_hdr = sr
999 .repository
1000 .as_ref()
1001 .expect("expected repository qualifier");
1002 assert_eq!(repository_hdr.name, "pricing");
1003 }
1004
1005 #[test]
1006 fn parse_data_qualified_type_with_effective_and_repository_on_uses() {
1007 let input = "spec consumer\nuses @iso/countries alpha2 2026-06-01\ndata country: alpha2.code -> option \"NL\"";
1008 let result = parse(
1009 input,
1010 crate::parsing::source::SourceType::Volatile,
1011 &ResourceLimits::default(),
1012 )
1013 .unwrap()
1014 .into_flattened_specs();
1015 let spec_ref = match &result[0].data[0].value {
1016 crate::parsing::ast::DataValue::Import(sr) => sr,
1017 other => panic!("expected Import on uses row, got: {:?}", other),
1018 };
1019 assert_eq!(spec_ref.name, "alpha2");
1020
1021 let eff = spec_ref
1022 .effective
1023 .as_ref()
1024 .expect("expected effective datetime");
1025 assert_eq!(eff.year, 2026);
1026 assert_eq!(eff.month, 6);
1027
1028 let qualifier = spec_ref
1029 .repository
1030 .as_ref()
1031 .expect("expected repository qualifier");
1032 assert_eq!(qualifier.name, "@iso/countries");
1033
1034 match &result[0].data[1].value {
1035 crate::parsing::ast::DataValue::Definition {
1036 base,
1037 constraints,
1038 value,
1039 } => {
1040 assert!(value.is_none());
1041 assert_eq!(
1042 base.as_ref().expect("expected base"),
1043 &crate::parsing::ast::ParentType::Qualified {
1044 spec_alias: "alpha2".into(),
1045 inner: Box::new(crate::parsing::ast::ParentType::Custom {
1046 name: "code".into(),
1047 }),
1048 }
1049 );
1050
1051 let cs = constraints
1052 .as_ref()
1053 .expect("expected trailing constraint chain");
1054 assert_eq!(cs.len(), 1);
1055 }
1056 other => panic!("expected Definition, got: {:?}", other),
1057 }
1058 }
1059
1060 #[test]
1061 fn parse_error_is_returned_for_garbage_input() {
1062 let result = parse(
1063 r#"
1064spec test
1065this is not valid lemma syntax @#$%
1066"#,
1067 crate::parsing::source::SourceType::Volatile,
1068 &ResourceLimits::default(),
1069 );
1070
1071 assert!(result.is_err(), "Should fail on malformed input");
1072 match result {
1073 Err(Error::Parsing { .. }) => {
1074 }
1076 Err(e) => panic!("Expected Parse error, got: {e:?}"),
1077 Ok(_) => panic!("Expected parse error"),
1078 }
1079 }
1080
1081 #[test]
1084 fn parse_local_with_literal_rejected() {
1085 let err = parse(
1086 r#"spec s
1087with x: 42"#,
1088 crate::parsing::source::SourceType::Volatile,
1089 &ResourceLimits::default(),
1090 )
1091 .unwrap_err();
1092 let msg = err.to_string();
1093 assert!(
1094 msg.contains("imported spec") || msg.contains("alias.field"),
1095 "expected local with rejection, got: {msg}"
1096 );
1097 }
1098
1099 #[test]
1100 fn parse_local_with_import_reference_rejected() {
1101 let err = parse(
1102 r#"spec s
1103uses i: inner
1104with copy: i.v"#,
1105 crate::parsing::source::SourceType::Volatile,
1106 &ResourceLimits::default(),
1107 )
1108 .unwrap_err();
1109 let msg = err.to_string();
1110 assert!(
1111 msg.contains("imported spec") || msg.contains("alias.field"),
1112 "expected local with rejection, got: {msg}"
1113 );
1114 }
1115
1116 #[test]
1117 fn parse_local_with_dotted_rhs_rejected() {
1118 let err = parse(
1119 r#"spec s
1120with x: a.something"#,
1121 crate::parsing::source::SourceType::Volatile,
1122 &ResourceLimits::default(),
1123 )
1124 .unwrap_err();
1125 let msg = err.to_string();
1126 assert!(
1127 msg.contains("imported spec") || msg.contains("alias.field"),
1128 "expected local with rejection, got: {msg}"
1129 );
1130 }
1131
1132 #[test]
1133 fn parse_local_with_multi_segment_rhs_rejected() {
1134 let err = parse(
1135 r#"spec s
1136with x: alpha.beta.gamma.delta"#,
1137 crate::parsing::source::SourceType::Volatile,
1138 &ResourceLimits::default(),
1139 )
1140 .unwrap_err();
1141 let msg = err.to_string();
1142 assert!(
1143 msg.contains("imported spec") || msg.contains("alias.field"),
1144 "expected local with rejection, got: {msg}"
1145 );
1146 }
1147
1148 #[test]
1151 fn parse_local_non_dotted_rhs_stays_definition_with_custom_base() {
1152 let input = r#"spec s
1153data x: myothertype"#;
1154 let result = parse(
1155 input,
1156 crate::parsing::source::SourceType::Volatile,
1157 &ResourceLimits::default(),
1158 )
1159 .unwrap()
1160 .into_flattened_specs();
1161 let value = &result[0].data[0].value;
1162 assert!(
1163 matches!(
1164 value,
1165 crate::parsing::ast::DataValue::Definition {
1166 base: Some(crate::parsing::ast::ParentType::Custom { .. }),
1167 ..
1168 }
1169 ),
1170 "non-dotted local RHS must stay Definition with custom base, got: {:?}",
1171 value
1172 );
1173 }
1174
1175 #[test]
1177 fn parse_binding_non_dotted_rhs_is_with_reference() {
1178 let input = r#"spec s
1179with child.slot: somename"#;
1180 let result = parse(
1181 input,
1182 crate::parsing::source::SourceType::Volatile,
1183 &ResourceLimits::default(),
1184 )
1185 .unwrap()
1186 .into_flattened_specs();
1187 let value = &result[0].data[0].value;
1188 assert!(
1189 matches!(
1190 value,
1191 crate::parsing::ast::DataValue::With(
1192 crate::parsing::ast::WithRhs::Reference { .. }
1193 )
1194 ),
1195 "non-dotted RHS in binding context must yield With(Reference); got: {:?}",
1196 value
1197 );
1198 }
1199
1200 #[test]
1202 fn parse_data_colon_spec_rhs_is_rejected() {
1203 let result = parse(
1204 r#"
1205spec s
1206data x: spec other
1207"#,
1208 crate::parsing::source::SourceType::Volatile,
1209 &ResourceLimits::default(),
1210 );
1211 match result {
1212 Ok(_) => panic!("`data x: spec other` must fail to parse"),
1213 Err(err) => {
1214 let msg = err.to_string();
1215 assert!(
1216 msg.contains("uses") && msg.contains("spec"),
1217 "error must direct to `uses` for spec import, got: {msg}"
1218 );
1219 }
1220 }
1221 }
1222
1223 #[test]
1226 fn parse_binding_with_dotted_rhs_preserves_both_sides() {
1227 let input = r#"spec s
1228with outer.inner: target.field"#;
1229 let result = parse(
1230 input,
1231 crate::parsing::source::SourceType::Volatile,
1232 &ResourceLimits::default(),
1233 )
1234 .unwrap()
1235 .into_flattened_specs();
1236 let datum = &result[0].data[0];
1237 assert_eq!(datum.reference.segments, vec!["outer"]);
1238 assert_eq!(datum.reference.name, "inner");
1239 match &datum.value {
1240 crate::parsing::ast::DataValue::With(crate::parsing::ast::WithRhs::Reference {
1241 target,
1242 }) => {
1243 assert_eq!(target.segments, vec!["target"]);
1244 assert_eq!(target.name, "field");
1245 }
1246 other => panic!("expected With(Reference), got: {:?}", other),
1247 }
1248 }
1249
1250 #[test]
1251 fn parse_data_on_binding_path_is_rejected_with_with_hint() {
1252 let result = parse(
1253 r#"spec s
1254data outer.inner: 1"#,
1255 crate::parsing::source::SourceType::Volatile,
1256 &ResourceLimits::default(),
1257 );
1258 match result {
1259 Ok(_) => panic!("data with binding path must not parse"),
1260 Err(err) => {
1261 let msg = err.to_string();
1262 assert!(
1263 msg.contains("with"),
1264 "error should steer authors toward with; got: {msg}"
1265 );
1266 }
1267 }
1268 }
1269
1270 #[test]
1271 fn parse_bare_file_yields_single_anonymous_repository_group() {
1272 let input = "spec a\ndata x: 1\nspec b\ndata y: 2";
1273 let parsed = parse(
1274 input,
1275 crate::parsing::source::SourceType::Volatile,
1276 &ResourceLimits::default(),
1277 )
1278 .unwrap();
1279 assert_eq!(parsed.repositories.len(), 1);
1280 let (repo, specs) = parsed.repositories.iter().next().unwrap();
1281 assert!(repo.name.is_none());
1282 assert_eq!(specs.len(), 2);
1283 assert_eq!(specs[0].name, "a");
1284 assert_eq!(specs[1].name, "b");
1285 }
1286
1287 #[test]
1288 fn parse_repo_sections_preserve_order_and_names() {
1289 let input = r#"repo r1
1290
1291spec a
1292data x: 1
1293
1294repo r2
1295
1296spec b
1297data y: 2"#;
1298 let parsed = parse(
1299 input,
1300 crate::parsing::source::SourceType::Volatile,
1301 &ResourceLimits::default(),
1302 )
1303 .unwrap();
1304 assert_eq!(parsed.repositories.len(), 2);
1305 let keys: Vec<_> = parsed.repositories.keys().collect();
1306 assert_eq!(keys[0].name.as_deref(), Some("r1"));
1307 assert_eq!(keys[1].name.as_deref(), Some("r2"));
1308 }
1309
1310 #[test]
1311 fn parse_duplicate_repo_name_merges_spec_lists() {
1312 let input = r#"repo dup
1313
1314spec a
1315data x: 1
1316
1317repo dup
1318
1319spec b
1320data y: 2"#;
1321 let parsed = parse(
1322 input,
1323 crate::parsing::source::SourceType::Volatile,
1324 &ResourceLimits::default(),
1325 )
1326 .unwrap();
1327 assert_eq!(parsed.repositories.len(), 1);
1328 assert_eq!(parsed.flatten_specs().len(), 2);
1329 }
1330
1331 #[test]
1332 fn parse_repo_with_no_specs_then_eof_yields_empty_spec_vec_for_that_repo() {
1333 let input = "repo empty";
1334 let parsed = parse(
1335 input,
1336 crate::parsing::source::SourceType::Volatile,
1337 &ResourceLimits::default(),
1338 )
1339 .unwrap();
1340 assert_eq!(parsed.repositories.len(), 1);
1341 let (_repo, specs) = parsed.repositories.iter().next().unwrap();
1342 assert_eq!(specs.len(), 0);
1343 }
1344
1345 #[test]
1346 fn parse_repo_followed_by_repo_without_specs_first_repo_empty_second_has_spec() {
1347 let input = "repo a\n\nrepo b\n\nspec s\ndata x: 1";
1348 let parsed = parse(
1349 input,
1350 crate::parsing::source::SourceType::Volatile,
1351 &ResourceLimits::default(),
1352 )
1353 .unwrap();
1354 assert_eq!(parsed.repositories.len(), 2);
1355 let names: Vec<_> = parsed
1356 .repositories
1357 .keys()
1358 .map(|r| r.name.as_deref())
1359 .collect();
1360 assert_eq!(names, vec![Some("a"), Some("b")]);
1361 assert!(parsed.repositories.values().next().unwrap().is_empty());
1362 assert_eq!(parsed.repositories.values().nth(1).unwrap().len(), 1);
1363 }
1364
1365 #[test]
1366 fn parse_spec_named_repo_keyword_should_be_rejected() {
1367 assert!(
1368 parse(
1369 "spec repo\ndata x: 1",
1370 crate::parsing::source::SourceType::Volatile,
1371 &ResourceLimits::default(),
1372 )
1373 .is_err(),
1374 "spec must not be allowed to use reserved keyword `repo` as its name"
1375 );
1376 }
1377
1378 #[test]
1379 fn parse_repo_declaration_cannot_use_spec_keyword_as_repository_name() {
1380 assert!(
1381 parse(
1382 "repo spec\n\nspec z\ndata q: 1\nrule r: q",
1383 crate::parsing::source::SourceType::Volatile,
1384 &ResourceLimits::default(),
1385 )
1386 .is_err(),
1387 "repository name cannot be the token `spec`"
1388 );
1389 }
1390
1391 #[test]
1392 fn parse_repo_declaration_cannot_use_data_keyword_as_repository_name() {
1393 assert!(
1394 parse(
1395 "repo data\n\nspec z\ndata q: 1\nrule r: q",
1396 crate::parsing::source::SourceType::Volatile,
1397 &ResourceLimits::default(),
1398 )
1399 .is_err(),
1400 "repository name cannot be the token `data`"
1401 );
1402 }
1403
1404 #[test]
1405 fn parse_repo_declaration_cannot_use_rule_keyword_as_repository_name() {
1406 assert!(
1407 parse(
1408 "repo rule\n\nspec z\ndata q: 1\nrule r: q",
1409 crate::parsing::source::SourceType::Volatile,
1410 &ResourceLimits::default(),
1411 )
1412 .is_err(),
1413 "repository name cannot be the token `rule`"
1414 );
1415 }
1416
1417 #[test]
1418 fn parse_data_named_repo_keyword_is_rejected() {
1419 let err = parse(
1420 "spec s\ndata repo: 1",
1421 crate::parsing::source::SourceType::Volatile,
1422 &ResourceLimits::default(),
1423 )
1424 .unwrap_err();
1425 assert!(
1426 err.to_string().contains("repo"),
1427 "data named repo should not parse: {}",
1428 err
1429 );
1430 }
1431
1432 #[test]
1433 fn parse_rule_named_repo_keyword_is_rejected() {
1434 let err = parse(
1435 "spec s\ndata x: 1\nrule repo: x",
1436 crate::parsing::source::SourceType::Volatile,
1437 &ResourceLimits::default(),
1438 )
1439 .unwrap_err();
1440 let msg = err.to_string();
1441 assert!(
1442 msg.contains("repo") || msg.contains("reserved"),
1443 "rule named repo should not parse: {msg}"
1444 );
1445 }
1446
1447 #[test]
1448 fn parse_repo_declaration_accepts_non_keyword_repository_identifier() {
1449 let parsed = parse(
1450 "repo warehouse\n\nspec z\ndata q: 1\nrule r: q",
1451 crate::parsing::source::SourceType::Volatile,
1452 &ResourceLimits::default(),
1453 )
1454 .unwrap();
1455 assert_eq!(parsed.repositories.len(), 1);
1456 assert_eq!(
1457 parsed.repositories.keys().next().unwrap().name.as_deref(),
1458 Some("warehouse")
1459 );
1460 }
1461
1462 #[test]
1463 fn parse_repo_name_case_insensitive_same_repository_merged() {
1464 let input = "repo Foo\n\nspec a\ndata x: 1\n\nrepo foo\n\nspec b\ndata y: 2";
1465 let parsed = parse(
1466 input,
1467 crate::parsing::source::SourceType::Volatile,
1468 &ResourceLimits::default(),
1469 )
1470 .unwrap();
1471 assert_eq!(
1472 parsed.repositories.len(),
1473 1,
1474 "Foo and foo are the same repository after canonicalization"
1475 );
1476 let specs: Vec<_> = parsed.repositories.values().next().unwrap().clone();
1477 assert_eq!(specs.len(), 2);
1478 assert_eq!(specs[0].name, "a");
1479 assert_eq!(specs[1].name, "b");
1480 }
1481
1482 #[test]
1483 fn parse_repo_empty_name_errors() {
1484 let err = parse(
1485 "repo \nspec a\ndata x: 1",
1486 crate::parsing::source::SourceType::Volatile,
1487 &ResourceLimits::default(),
1488 )
1489 .unwrap_err();
1490 assert!(
1491 !err.to_string().is_empty(),
1492 "empty repo name should not parse quietly: {err}"
1493 );
1494 }
1495
1496 #[test]
1497 fn parse_repo_numeric_name_behavior() {
1498 let input = "repo 123\n\nspec a\ndata x: 1";
1499 let result = parse(
1500 input,
1501 crate::parsing::source::SourceType::Volatile,
1502 &ResourceLimits::default(),
1503 );
1504 match result {
1505 Ok(parsed) => {
1506 assert_eq!(
1507 parsed.repositories.keys().next().unwrap().name.as_deref(),
1508 Some("123"),
1509 "if numeric repo names parse, identity must be stable"
1510 );
1511 }
1512 Err(e) => {
1513 assert!(
1514 !e.to_string().is_empty(),
1515 "rejecting numeric repo name is ok if explicit: {e}"
1516 );
1517 }
1518 }
1519 }
1520
1521 #[test]
1522 fn parse_duplicate_repo_three_sections_preserves_spec_order_abc() {
1523 let input = r#"repo dup
1524
1525spec a
1526data x: 1
1527
1528repo dup
1529
1530spec b
1531data y: 2
1532
1533repo dup
1534
1535spec c
1536data z: 3"#;
1537 let parsed = parse(
1538 input,
1539 crate::parsing::source::SourceType::Volatile,
1540 &ResourceLimits::default(),
1541 )
1542 .unwrap();
1543 assert_eq!(parsed.repositories.len(), 1);
1544 let specs = parsed.repositories.values().next().unwrap();
1545 assert_eq!(
1546 specs.iter().map(|s| s.name.as_str()).collect::<Vec<_>>(),
1547 vec!["a", "b", "c"]
1548 );
1549 }
1550
1551 #[test]
1552 fn parse_repo_single_section_roundtrips_through_formatter() {
1553 let input = "repo r\n\nspec a\ndata x: 1";
1554 let parsed = parse(
1555 input,
1556 crate::parsing::source::SourceType::Volatile,
1557 &ResourceLimits::default(),
1558 )
1559 .unwrap();
1560 let formatted = format_parse_result(&parsed);
1561 let again = parse(
1562 &formatted,
1563 crate::parsing::source::SourceType::Volatile,
1564 &ResourceLimits::default(),
1565 )
1566 .unwrap();
1567 assert_eq!(again.repositories.len(), parsed.repositories.len());
1568 assert_eq!(again.flatten_specs().len(), parsed.flatten_specs().len());
1569 assert_eq!(
1570 again.flatten_specs()[0].name,
1571 parsed.flatten_specs()[0].name
1572 );
1573 }
1574
1575 #[test]
1576 fn parse_repo_two_sections_roundtrips_through_formatter() {
1577 let input = "repo r1\n\nspec a\ndata x: 1\n\nrepo r2\n\nspec b\ndata y: 2";
1578 let parsed = parse(
1579 input,
1580 crate::parsing::source::SourceType::Volatile,
1581 &ResourceLimits::default(),
1582 )
1583 .unwrap();
1584 let formatted = format_parse_result(&parsed);
1585 let again = parse(
1586 &formatted,
1587 crate::parsing::source::SourceType::Volatile,
1588 &ResourceLimits::default(),
1589 )
1590 .unwrap();
1591 assert_eq!(again.repositories.len(), 2);
1592 assert_eq!(again.flatten_specs().len(), 2);
1593 }
1594
1595 #[test]
1596 fn parse_repo_duplicate_merge_formatter_emits_single_repo_block_or_equivalent_parse() {
1597 let input = r#"repo dup
1598
1599spec a
1600data x: 1
1601
1602repo dup
1603
1604spec b
1605data y: 2"#;
1606 let parsed = parse(
1607 input,
1608 crate::parsing::source::SourceType::Volatile,
1609 &ResourceLimits::default(),
1610 )
1611 .unwrap();
1612 let formatted = format_parse_result(&parsed);
1613 let again = parse(
1614 &formatted,
1615 crate::parsing::source::SourceType::Volatile,
1616 &ResourceLimits::default(),
1617 )
1618 .unwrap();
1619 assert_eq!(
1620 again.repositories.len(),
1621 1,
1622 "formatted duplicate-repo file must still merge to one logical repo"
1623 );
1624 assert_eq!(again.flatten_specs().len(), 2);
1625 }
1626
1627 #[test]
1628 fn parse_rejects_data_named_measure() {
1629 let result = parse(
1630 "spec s\ndata measure: 1",
1631 crate::parsing::source::SourceType::Volatile,
1632 &ResourceLimits::default(),
1633 );
1634 assert!(
1635 result.is_err(),
1636 "data named measure (type keyword) must be rejected"
1637 );
1638 }
1639
1640 #[test]
1641 fn parse_rejects_data_named_number() {
1642 let result = parse(
1643 "spec s\ndata number: 42",
1644 crate::parsing::source::SourceType::Volatile,
1645 &ResourceLimits::default(),
1646 );
1647 assert!(
1648 result.is_err(),
1649 "data named number (type keyword) must be rejected"
1650 );
1651 }
1652
1653 #[test]
1654 fn parse_rejects_data_named_text() {
1655 let result = parse(
1656 "spec s\ndata text: \"hello\"",
1657 crate::parsing::source::SourceType::Volatile,
1658 &ResourceLimits::default(),
1659 );
1660 assert!(
1661 result.is_err(),
1662 "data named text (type keyword) must be rejected"
1663 );
1664 }
1665
1666 #[test]
1667 fn parse_rejects_data_named_date() {
1668 let result = parse(
1669 "spec s\ndata date: 2024-01-01",
1670 crate::parsing::source::SourceType::Volatile,
1671 &ResourceLimits::default(),
1672 );
1673 assert!(
1674 result.is_err(),
1675 "data named date (type keyword) must be rejected"
1676 );
1677 }
1678
1679 #[test]
1680 fn parse_rejects_data_named_boolean() {
1681 let result = parse(
1682 "spec s\ndata boolean: true",
1683 crate::parsing::source::SourceType::Volatile,
1684 &ResourceLimits::default(),
1685 );
1686 assert!(
1687 result.is_err(),
1688 "data named boolean (type keyword) must be rejected"
1689 );
1690 }
1691
1692 #[test]
1693 fn parse_rejects_data_named_ratio() {
1694 let result = parse(
1695 "spec s\ndata ratio: 5%",
1696 crate::parsing::source::SourceType::Volatile,
1697 &ResourceLimits::default(),
1698 );
1699 assert!(
1700 result.is_err(),
1701 "data named ratio (type keyword) must be rejected"
1702 );
1703 }
1704
1705 #[test]
1706 fn parse_rejects_rule_named_measure() {
1707 let result = parse(
1708 "spec s\ndata x: 1\nrule measure: x",
1709 crate::parsing::source::SourceType::Volatile,
1710 &ResourceLimits::default(),
1711 );
1712 assert!(
1713 result.is_err(),
1714 "rule named measure (type keyword) must be rejected"
1715 );
1716 }
1717
1718 #[test]
1719 fn parse_rejects_rule_named_number() {
1720 let result = parse(
1721 "spec s\ndata x: 1\nrule number: x",
1722 crate::parsing::source::SourceType::Volatile,
1723 &ResourceLimits::default(),
1724 );
1725 assert!(
1726 result.is_err(),
1727 "rule named number (type keyword) must be rejected"
1728 );
1729 }
1730
1731 #[test]
1732 fn parse_rejects_rule_named_text() {
1733 let result = parse(
1734 "spec s\ndata x: 1\nrule text: x",
1735 crate::parsing::source::SourceType::Volatile,
1736 &ResourceLimits::default(),
1737 );
1738 assert!(
1739 result.is_err(),
1740 "rule named text (type keyword) must be rejected"
1741 );
1742 }
1743
1744 #[test]
1745 fn parse_rejects_rule_named_date() {
1746 let result = parse(
1747 "spec s\ndata x: 1\nrule date: x",
1748 crate::parsing::source::SourceType::Volatile,
1749 &ResourceLimits::default(),
1750 );
1751 assert!(
1752 result.is_err(),
1753 "rule named date (type keyword) must be rejected"
1754 );
1755 }
1756
1757 #[test]
1758 fn parse_rejects_rule_named_boolean() {
1759 let result = parse(
1760 "spec s\ndata x: 1\nrule boolean: x",
1761 crate::parsing::source::SourceType::Volatile,
1762 &ResourceLimits::default(),
1763 );
1764 assert!(
1765 result.is_err(),
1766 "rule named boolean (type keyword) must be rejected"
1767 );
1768 }
1769
1770 #[test]
1771 fn parse_rejects_rule_named_ratio() {
1772 let result = parse(
1773 "spec s\ndata x: 1\nrule ratio: x",
1774 crate::parsing::source::SourceType::Volatile,
1775 &ResourceLimits::default(),
1776 );
1777 assert!(
1778 result.is_err(),
1779 "rule named ratio (type keyword) must be rejected"
1780 );
1781 }
1782}