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