cucumber_expressions/parse.rs
1// Copyright (c) 2021-2025 Brendan Molloy <brendan@bbqsrc.net>,
2// Ilya Solovyiov <ilya.solovyiov@gmail.com>,
3// Kai Ren <tyranron@gmail.com>
4//
5// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8// option. This file may not be copied, modified, or distributed
9// except according to those terms.
10
11//! [Cucumber Expressions][1] [AST] parser.
12//!
13//! See details in the [grammar spec][0].
14//!
15//! [0]: crate#grammar
16//! [1]: https://github.com/cucumber/cucumber-expressions#readme
17//! [AST]: https://en.wikipedia.org/wiki/Abstract_syntax_tree
18
19use derive_more::with_trait::{Display, Error as StdError};
20use nom::{
21 AsChar, Compare, Err, FindToken, IResult, Input, Needed, Offset, Parser,
22 branch::alt,
23 bytes::complete::{tag, take_while, take_while1},
24 character::complete::one_of,
25 combinator::{map, peek, verify},
26 error::{ErrorKind, ParseError},
27 multi::{many0, many1, separated_list1},
28};
29
30use crate::{
31 ast::{
32 Alternation, Alternative, Expression, Optional, Parameter,
33 SingleExpression,
34 },
35 combinator,
36};
37
38/// Reserved characters requiring a special handling.
39pub const RESERVED_CHARS: &str = r"{}()\/ ";
40
41/// Matches `normal` and [`RESERVED_CHARS`] escaped with `\`.
42///
43/// Uses [`combinator::escaped0`] under the hood.
44///
45/// # Errors
46///
47/// ## Recoverable [`Error`]
48///
49/// - If `normal` parser errors.
50///
51/// ## Irrecoverable [`Failure`]
52///
53/// - If `normal` parser fails.
54/// - [`EscapedEndOfLine`].
55/// - [`EscapedNonReservedCharacter`].
56///
57/// [`Error`]: Err::Error
58/// [`EscapedEndOfLine`]: Error::EscapedEndOfLine
59/// [`EscapedNonReservedCharacter`]: Error::EscapedNonReservedCharacter
60/// [`Failure`]: Err::Failure
61fn escaped_reserved_chars0<I, F>(
62 normal: F,
63) -> impl FnMut(I) -> IResult<I, I, Error<I>>
64where
65 I: Clone + Display + Offset + Input,
66 <I as Input>::Item: AsChar + Copy,
67 F: Parser<I, Error = Error<I>>,
68 Error<I>: ParseError<I>,
69 for<'s> &'s str: FindToken<<I as Input>::Item>,
70{
71 combinator::map_err(
72 combinator::escaped0(normal, '\\', one_of(RESERVED_CHARS)),
73 |e| {
74 if let Err::Error(Error::Other(span, ErrorKind::Escaped)) = e {
75 match span.input_len() {
76 1 => Error::EscapedEndOfLine(span),
77 n if n > 1 => Error::EscapedNonReservedCharacter(
78 span.take(span.slice_index(2).unwrap_or_default()),
79 ),
80 _ => Error::EscapedNonReservedCharacter(span),
81 }
82 .failure()
83 } else {
84 e
85 }
86 },
87 )
88}
89
90/// Parses a `parameter` as defined in the [grammar spec][0].
91///
92/// # Grammar
93///
94/// ```ebnf
95/// parameter = '{', name*, '}'
96/// name = (- name-to-escape) | ('\', name-to-escape)
97/// name-to-escape = '{' | '}' | '(' | '/' | '\'
98/// ```
99///
100/// # Example
101///
102/// ```text
103/// {}
104/// {name}
105/// {with spaces}
106/// {escaped \/\{\(}
107/// {no need to escape )}
108/// {🦀}
109/// ```
110///
111/// # Errors
112///
113/// ## Recoverable [`Error`]
114///
115/// - If `input` doesn't start with `{`.
116///
117/// ## Irrecoverable [`Failure`].
118///
119/// - [`EscapedNonReservedCharacter`].
120/// - [`NestedParameter`].
121/// - [`OptionalInParameter`].
122/// - [`UnescapedReservedCharacter`].
123/// - [`UnfinishedParameter`].
124///
125/// # Indexing
126///
127/// The given `indexer` is incremented only if the parsed [`Parameter`] is
128/// returned.
129///
130/// [`Error`]: Err::Error
131/// [`Failure`]: Err::Failure
132/// [`EscapedNonReservedCharacter`]: Error::EscapedNonReservedCharacter
133/// [`NestedParameter`]: Error::NestedParameter
134/// [`OptionalInParameter`]: Error::OptionalInParameter
135/// [`UnescapedReservedCharacter`]: Error::UnescapedReservedCharacter
136/// [`UnfinishedParameter`]: Error::UnfinishedParameter
137/// [0]: crate#grammar
138pub fn parameter<I>(
139 input: I,
140 indexer: &mut usize,
141) -> IResult<I, Parameter<I>, Error<I>>
142where
143 I: Clone + Display + Offset + Input + for<'s> Compare<&'s str>,
144 <I as Input>::Item: AsChar + Copy,
145 Error<I>: ParseError<I>,
146 for<'s> &'s str: FindToken<<I as Input>::Item>,
147{
148 fn is_name(c: impl AsChar) -> bool {
149 !"{}(\\/".contains(c.as_char())
150 }
151
152 let fail = |inp: I, opening_brace| {
153 match inp.iter_elements().next().map(AsChar::as_char) {
154 Some('{') => {
155 if let Ok((_, (par, ..))) = peek((
156 // We don't use `indexer` here, because we do this parsing
157 // for error reporting only.
158 |i| parameter(i, &mut 0),
159 escaped_reserved_chars0(take_while(is_name)),
160 tag("}"),
161 ))
162 .parse(inp.clone())
163 {
164 return Error::NestedParameter(
165 inp.take(par.input.input_len() + 2),
166 )
167 .failure();
168 }
169 return Error::UnescapedReservedCharacter(inp.take(1))
170 .failure();
171 }
172 Some('(') => {
173 if let Ok((_, opt)) = peek(optional).parse(inp.clone()) {
174 return Error::OptionalInParameter(
175 inp.take(opt.0.input_len() + 2),
176 )
177 .failure();
178 }
179 return Error::UnescapedReservedCharacter(inp.take(1))
180 .failure();
181 }
182 Some(c) if RESERVED_CHARS.contains(c) => {
183 return Error::UnescapedReservedCharacter(inp.take(1))
184 .failure();
185 }
186 _ => {}
187 }
188 Error::UnfinishedParameter(opening_brace).failure()
189 };
190
191 let (input, opening_brace) = tag("{")(input)?;
192 let (input, par_name) =
193 escaped_reserved_chars0(take_while(is_name))(input)?;
194 let (input, _) = combinator::map_err(tag("}"), |_| {
195 fail(input.clone(), opening_brace.clone())
196 })(input.clone())?;
197
198 *indexer += 1;
199 Ok((input, Parameter { input: par_name, id: *indexer - 1 }))
200}
201
202/// Parses an `optional` as defined in the [grammar spec][0].
203///
204/// # Grammar
205///
206/// ```ebnf
207/// optional = '(' text-in-optional+ ')'
208/// text-in-optional = (- optional-to-escape) | ('\', optional-to-escape)
209/// optional-to-escape = '(' | ')' | '{' | '/' | '\'
210/// ```
211///
212/// # Example
213///
214/// ```text
215/// (name)
216/// (with spaces)
217/// (escaped \/\{\()
218/// (no need to escape })
219/// (🦀)
220/// ```
221///
222/// # Errors
223///
224/// ## Recoverable [`Error`]
225///
226/// - If `input` doesn't start with `(`.
227///
228/// ## Irrecoverable [`Failure`]
229///
230/// - [`AlternationInOptional`].
231/// - [`EmptyOptional`].
232/// - [`EscapedEndOfLine`].
233/// - [`EscapedNonReservedCharacter`].
234/// - [`NestedOptional`].
235/// - [`ParameterInOptional`].
236/// - [`UnescapedReservedCharacter`].
237/// - [`UnfinishedOptional`].
238///
239/// [`Error`]: Err::Error
240/// [`Failure`]: Err::Failure
241/// [`AlternationInOptional`]: Error::AlternationInOptional
242/// [`EmptyOptional`]: Error::EmptyOptional
243/// [`EscapedEndOfLine`]: Error::EscapedEndOfLine
244/// [`EscapedNonReservedCharacter`]: Error::EscapedNonReservedCharacter
245/// [`NestedOptional`]: Error::NestedOptional
246/// [`ParameterInOptional`]: Error::ParameterInOptional
247/// [`UnescapedReservedCharacter`]: Error::UnescapedReservedCharacter
248/// [`UnfinishedOptional`]: Error::UnfinishedOptional
249/// [0]: crate#grammar
250pub fn optional<I>(input: I) -> IResult<I, Optional<I>, Error<I>>
251where
252 I: Clone + Display + Offset + Input + for<'s> Compare<&'s str>,
253 <I as Input>::Item: AsChar + Copy,
254 Error<I>: ParseError<I>,
255 for<'s> &'s str: FindToken<<I as Input>::Item>,
256{
257 fn is_in_optional(c: impl AsChar) -> bool {
258 !"(){\\/".contains(c.as_char())
259 }
260
261 let fail = |inp: I, opening_brace| {
262 match inp.iter_elements().next().map(AsChar::as_char) {
263 Some('(') => {
264 if let Ok((_, (opt, ..))) = peek((
265 optional,
266 escaped_reserved_chars0(take_while(is_in_optional)),
267 tag(")"),
268 ))
269 .parse(inp.clone())
270 {
271 return Error::NestedOptional(
272 inp.take(opt.0.input_len() + 2),
273 )
274 .failure();
275 }
276 return Error::UnescapedReservedCharacter(inp.take(1))
277 .failure();
278 }
279 Some('{') => {
280 // We use just `0` as `indexer` here, because we do this parsing
281 // for error reporting only.
282 if let Ok((_, par)) =
283 peek(|i| parameter(i, &mut 0)).parse(inp.clone())
284 {
285 return Error::ParameterInOptional(
286 inp.take(par.input.input_len() + 2),
287 )
288 .failure();
289 }
290 return Error::UnescapedReservedCharacter(inp.take(1))
291 .failure();
292 }
293 Some('/') => {
294 return Error::AlternationInOptional(inp.take(1)).failure();
295 }
296 Some(c) if RESERVED_CHARS.contains(c) => {
297 return Error::UnescapedReservedCharacter(inp.take(1))
298 .failure();
299 }
300 _ => {}
301 }
302 Error::UnfinishedOptional(opening_brace).failure()
303 };
304
305 let original_input = input.clone();
306 let (input, opening_paren) = tag("(")(input)?;
307 let (input, opt) =
308 escaped_reserved_chars0(take_while(is_in_optional))(input)?;
309 let (input, _) = combinator::map_err(tag(")"), |_| {
310 fail(input.clone(), opening_paren.clone())
311 })(input.clone())?;
312
313 if opt.input_len() == 0 {
314 return Err(Err::Failure(Error::EmptyOptional(original_input.take(2))));
315 }
316
317 Ok((input, Optional(opt)))
318}
319
320/// Parses an `alternative` as defined in the [grammar spec][0].
321///
322/// # Grammar
323///
324/// ```ebnf
325/// alternative = optional | (text-in-alternative+)
326/// text-in-alternative = (- alternative-to-escape)
327/// | ('\', alternative-to-escape)
328/// alternative-to-escape = ' ' | '(' | '{' | '/' | '\'
329/// ```
330///
331/// # Example
332///
333/// ```text
334/// text
335/// escaped\ whitespace
336/// no-need-to-escape)}
337/// 🦀
338/// (optional)
339/// ```
340///
341/// # Errors
342///
343/// ## Irrecoverable [`Failure`]
344///
345/// Any [`Failure`] of [`optional()`].
346///
347/// [`Failure`]: Err::Failure
348/// [0]: crate#grammar
349pub fn alternative<I>(input: I) -> IResult<I, Alternative<I>, Error<I>>
350where
351 I: Clone + Display + Offset + Input + for<'s> Compare<&'s str>,
352 <I as Input>::Item: AsChar + Copy,
353 Error<I>: ParseError<I>,
354 for<'s> &'s str: FindToken<<I as Input>::Item>,
355{
356 fn is_without_whitespace(c: impl AsChar) -> bool {
357 !" ({\\/".contains(c.as_char())
358 }
359
360 alt((
361 map(optional, Alternative::Optional),
362 map(
363 verify(
364 escaped_reserved_chars0(take_while(is_without_whitespace)),
365 |p| p.input_len() > 0,
366 ),
367 Alternative::Text,
368 ),
369 ))
370 .parse(input)
371}
372
373/// Parses an `alternation` as defined in the [grammar spec][0].
374///
375/// # Grammar
376///
377/// ```ebnf
378/// alternation = single-alternation, (`/`, single-alternation)+
379/// single-alternation = ((text-in-alternative+, optional*)
380/// | (optional+, text-in-alternative+))+
381/// ```
382///
383/// # Example
384///
385/// ```text
386/// left/right
387/// left(opt)/(opt)right
388/// escaped\ /text
389/// no-need-to-escape)}/text
390/// 🦀/⚙️
391/// ```
392///
393/// # Errors
394///
395/// ## Recoverable [`Error`]
396///
397/// - If `input` doesn't have `/`.
398///
399/// ## Irrecoverable [`Failure`]
400///
401/// - Any [`Failure`] of [`optional()`].
402/// - [`EmptyAlternation`].
403/// - [`OnlyOptionalInAlternation`].
404///
405/// [`Error`]: Err::Error
406/// [`Failure`]: Err::Failure
407/// [`EmptyAlternation`]: Error::EmptyAlternation
408/// [`OnlyOptionalInAlternation`]: Error::OnlyOptionalInAlternation
409/// [0]: crate#grammar
410pub fn alternation<I>(input: I) -> IResult<I, Alternation<I>, Error<I>>
411where
412 I: Clone + Display + Offset + Input + for<'s> Compare<&'s str>,
413 <I as Input>::Item: AsChar + Copy,
414 Error<I>: ParseError<I>,
415 for<'s> &'s str: FindToken<<I as Input>::Item>,
416{
417 let original_input = input.clone();
418 let (rest, alt) = match separated_list1(tag("/"), many1(alternative))
419 .parse(input)
420 {
421 Ok((rest, alt)) => {
422 if let Ok((_, slash)) =
423 peek(tag::<_, _, Error<I>>("/")).parse(rest.clone())
424 {
425 Err(Error::EmptyAlternation(slash).failure())
426 } else if alt.len() == 1 {
427 Err(Err::Error(Error::Other(rest, ErrorKind::Tag)))
428 } else {
429 Ok((rest, Alternation(alt)))
430 }
431 }
432 Err(Err::Error(Error::Other(sp, ErrorKind::Many1)))
433 if peek(tag::<_, _, Error<I>>("/")).parse(sp.clone()).is_ok() =>
434 {
435 Err(Error::EmptyAlternation(sp.take(1)).failure())
436 }
437 Err(e) => Err(e),
438 }?;
439
440 if alt.contains_only_optional() {
441 Err(Error::OnlyOptionalInAlternation(
442 original_input.take(alt.span_len()),
443 )
444 .failure())
445 } else {
446 Ok((rest, alt))
447 }
448}
449
450/// Parses a `single-expression` as defined in the [grammar spec][0].
451///
452/// # Grammar
453///
454/// ```ebnf
455/// single-expression = alternation
456/// | optional
457/// | parameter
458/// | text-without-whitespace+
459/// | whitespace+
460/// text-without-whitespace = (- (text-to-escape | whitespace))
461/// | ('\', text-to-escape)
462/// text-to-escape = '(' | '{' | '/' | '\'
463/// ```
464///
465/// # Example
466///
467/// ```text
468/// text(opt)/text
469/// (opt)
470/// {string}
471/// text
472/// ```
473///
474/// # Errors
475///
476/// ## Irrecoverable [`Failure`]
477///
478/// Any [`Failure`] of [`alternation()`], [`optional()`] or [`parameter()`].
479///
480/// # Indexing
481///
482/// The given `indexer` is incremented only if the parsed [`SingleExpression`]
483/// is returned and it represents a [`Parameter`].
484///
485/// [`Failure`]: Err::Failure
486/// [0]: crate#grammar
487pub fn single_expression<I>(
488 input: I,
489 indexer: &mut usize,
490) -> IResult<I, SingleExpression<I>, Error<I>>
491where
492 I: Clone + Display + Offset + Input + for<'s> Compare<&'s str>,
493 <I as Input>::Item: AsChar + Copy,
494 Error<I>: ParseError<I>,
495 for<'s> &'s str: FindToken<<I as Input>::Item>,
496{
497 fn is_without_whitespace(c: impl AsChar) -> bool {
498 !" ({\\/".contains(c.as_char())
499 }
500 fn is_whitespace(c: impl AsChar) -> bool {
501 c.as_char() == ' '
502 }
503
504 alt((
505 map(alternation, SingleExpression::Alternation),
506 map(optional, SingleExpression::Optional),
507 map(|i| parameter(i, indexer), SingleExpression::Parameter),
508 map(
509 verify(
510 escaped_reserved_chars0(take_while(is_without_whitespace)),
511 |s| s.input_len() > 0,
512 ),
513 SingleExpression::Text,
514 ),
515 map(take_while1(is_whitespace), SingleExpression::Whitespaces),
516 ))
517 .parse(input)
518}
519
520/// Parses an `expression` as defined in the [grammar spec][0].
521///
522/// # Grammar
523///
524/// ```ebnf
525/// expression = single-expression*
526/// ```
527///
528/// # Example
529///
530/// ```text
531/// text(opt)/text
532/// (opt)
533/// {string}
534/// text
535/// ```
536///
537/// > **NOTE:** Empty string is matched too.
538///
539/// # Errors
540///
541/// ## Irrecoverable [`Failure`]
542///
543/// Any [`Failure`] of [`alternation()`], [`optional()`] or [`parameter()`].
544///
545/// [`Failure`]: Err::Failure
546/// [0]: crate#grammar
547pub fn expression<I>(input: I) -> IResult<I, Expression<I>, Error<I>>
548where
549 I: Clone + Display + Offset + Input + for<'s> Compare<&'s str>,
550 <I as Input>::Item: AsChar + Copy,
551 Error<I>: ParseError<I>,
552 for<'s> &'s str: FindToken<<I as Input>::Item>,
553{
554 let mut indexer = 0;
555 map(many0(move |i| single_expression(i, &mut indexer)), Expression)
556 .parse(input)
557}
558
559/// Possible parsing errors.
560#[derive(Clone, Copy, Debug, Display, Eq, PartialEq, StdError)]
561pub enum Error<Input> {
562 /// Nested [`Parameter`]s.
563 #[display(
564 "{_0}\n\
565 A parameter may not contain an other parameter.\n\
566 If you did not mean to use an optional type you can use '\\{{' to \
567 escape the '{{'. For more complicated expressions consider using a \
568 regular expression instead."
569 )]
570 NestedParameter(#[error(not(source))] Input),
571
572 /// [`Optional`] inside a [`Parameter`].
573 #[display(
574 "{_0}\n\
575 A parameter may not contain an optional.\n\
576 If you did not mean to use an parameter type you can use '\\(' to \
577 escape the '('."
578 )]
579 OptionalInParameter(#[error(not(source))] Input),
580
581 /// Unfinished [`Parameter`].
582 #[display(
583 "{_0}\n\
584 The '{{' does not have a matching '}}'.\n\
585 If you did not intend to use a parameter you can use '\\{{' to escape \
586 the '{{'."
587 )]
588 UnfinishedParameter(#[error(not(source))] Input),
589
590 /// Nested [`Optional`].
591 #[display(
592 "{_0}\n\
593 An optional may not contain an other optional.\n\
594 If you did not mean to use an optional type you can use '\\(' to \
595 escape the '('. For more complicated expressions consider using a \
596 regular expression instead."
597 )]
598 NestedOptional(#[error(not(source))] Input),
599
600 /// [`Parameter`] inside an [`Optional`].
601 #[display(
602 "{_0}\n\
603 An optional may not contain a parameter.\n\
604 If you did not mean to use an parameter type you can use '\\{{' to \
605 escape the '{{'."
606 )]
607 ParameterInOptional(#[error(not(source))] Input),
608
609 /// Empty [`Optional`].
610 #[display(
611 "{_0}\n\
612 An optional must contain some text.\n\
613 If you did not mean to use an optional you can use '\\(' to escape \
614 the '('."
615 )]
616 EmptyOptional(#[error(not(source))] Input),
617
618 /// [`Alternation`] inside an [`Optional`].
619 #[display(
620 "{_0}\n\
621 An alternation can not be used inside an optional.\n\
622 You can use '\\/' to escape the '/'."
623 )]
624 AlternationInOptional(#[error(not(source))] Input),
625
626 /// Unfinished [`Optional`].
627 #[display(
628 "{_0}\n\
629 The '(' does not have a matching ')'.\n\
630 If you did not intend to use an optional you can use '\\(' to escape \
631 the '('."
632 )]
633 UnfinishedOptional(#[error(not(source))] Input),
634
635 /// Empty [`Alternation`].
636 #[display(
637 "{_0}\n\
638 An alternation can not be empty.\n\
639 If you did not mean to use an alternative you can use '\\/' to \
640 escape the '/'."
641 )]
642 EmptyAlternation(#[error(not(source))] Input),
643
644 /// Only [`Optional`] inside [`Alternation`].
645 #[display(
646 "{_0}\n\
647 An alternation may not exclusively contain optionals.\n\
648 If you did not mean to use an optional you can use '\\(' to escape \
649 the '('."
650 )]
651 OnlyOptionalInAlternation(#[error(not(source))] Input),
652
653 /// Unescaped [`RESERVED_CHARS`].
654 #[display(
655 "{_0}\n\
656 Unescaped reserved character.\n\
657 You can use an '\\' to escape it."
658 )]
659 UnescapedReservedCharacter(#[error(not(source))] Input),
660
661 /// Escaped non-[`RESERVED_CHARS`].
662 #[display(
663 "{_0}\n\
664 Only the characters '{{', '}}', '(', ')', '\\', '/' and whitespace \
665 can be escaped.\n\
666 If you did mean to use an '\\' you can use '\\\\' to escape it."
667 )]
668 EscapedNonReservedCharacter(#[error(not(source))] Input),
669
670 /// Escaped EOL.
671 #[display(
672 "{_0}\n\
673 The end of line can not be escaped.\n\
674 You can use '\\' to escape the the '\'."
675 )]
676 EscapedEndOfLine(#[error(not(source))] Input),
677
678 /// Unknown error.
679 #[display(
680 "{_0}\n\
681 Unknown parsing error."
682 )]
683 Other(#[error(not(source))] Input, ErrorKind),
684
685 /// Parsing requires more data.
686 #[display(
687 "{}", match _0 {
688 Needed::Size(n) => {
689 write!(__derive_more_f, "Parsing requires {n} bytes/chars")
690 }
691 Needed::Unknown => {
692 write!(__derive_more_f, "Parsing requires more data")
693 }
694 }.map(|()| "")?
695 )]
696 Needed(#[error(not(source))] Needed),
697}
698
699impl<Input> Error<Input> {
700 /// Converts this [`Error`] into a [`Failure`].
701 ///
702 /// [`Error`]: enum@Error
703 /// [`Failure`]: Err::Failure
704 const fn failure(self) -> Err<Self> {
705 Err::Failure(self)
706 }
707}
708
709impl<Input> ParseError<Input> for Error<Input> {
710 fn from_error_kind(input: Input, kind: ErrorKind) -> Self {
711 Self::Other(input, kind)
712 }
713
714 fn append(input: Input, kind: ErrorKind, other: Self) -> Self {
715 if let Self::Other(..) = other {
716 Self::from_error_kind(input, kind)
717 } else {
718 other
719 }
720 }
721}
722
723#[cfg(test)]
724mod spec {
725 use std::fmt;
726
727 use nom::{Err, IResult, error::ErrorKind};
728
729 use crate::{
730 Alternative, Spanned,
731 parse::{
732 Error, alternation, alternative, expression, optional, parameter,
733 },
734 };
735
736 /// Asserts two given text representations of [AST] to be equal.
737 ///
738 /// [AST]: https://en.wikipedia.org/wiki/Abstract_syntax_tree
739 fn assert_ast_eq(actual: impl fmt::Debug, expected: impl AsRef<str>) {
740 assert_eq!(
741 format!("{actual:#?}")
742 .lines()
743 .map(|line| line.trim_start().trim_end_matches('\n'))
744 .collect::<String>(),
745 expected
746 .as_ref()
747 .lines()
748 .map(|line| line.trim_end_matches('\n').trim())
749 .collect::<String>(),
750 );
751 }
752
753 /// Unwraps the given `parser` result asserting it has finished and succeed.
754 fn unwrap_parser<'s, T>(
755 parser: IResult<Spanned<'s>, T, Error<Spanned<'s>>>,
756 ) -> T {
757 let (rest, par) = parser.unwrap_or_else(|e| match &e {
758 Err::Error(e) | Err::Failure(e) => {
759 panic!("expected `Ok`, found `Err`: {e}")
760 }
761 Err::Incomplete(_) => {
762 panic!("expected `Ok`, but `Err::Incomplete`: {e}")
763 }
764 });
765 assert_eq!(*rest, "");
766 par
767 }
768
769 mod parameter {
770 use super::{Err, Error, ErrorKind, Spanned, parameter, unwrap_parser};
771
772 #[test]
773 fn empty() {
774 assert_eq!(
775 **unwrap_parser(parameter(Spanned::new("{}"), &mut 0)),
776 "",
777 );
778 }
779
780 #[test]
781 fn named() {
782 assert_eq!(
783 **unwrap_parser(parameter(Spanned::new("{string}"), &mut 0)),
784 "string",
785 );
786 }
787
788 #[test]
789 fn named_with_spaces() {
790 assert_eq!(
791 **unwrap_parser(parameter(
792 Spanned::new("{with space}"),
793 &mut 0,
794 )),
795 "with space",
796 );
797 }
798
799 #[test]
800 fn named_with_escaped() {
801 assert_eq!(
802 **unwrap_parser(parameter(Spanned::new("{with \\{}"), &mut 0)),
803 "with \\{",
804 );
805 }
806
807 #[test]
808 fn named_with_closing_paren() {
809 assert_eq!(
810 **unwrap_parser(parameter(Spanned::new("{with )}"), &mut 0)),
811 "with )",
812 );
813 }
814
815 #[test]
816 fn named_with_emoji() {
817 assert_eq!(
818 **unwrap_parser(parameter(Spanned::new("{🦀}"), &mut 0)),
819 "🦀",
820 );
821 }
822
823 #[test]
824 fn errors_on_empty() {
825 let span = Spanned::new("");
826
827 assert_eq!(
828 parameter(span, &mut 0),
829 Err(Err::Error(Error::Other(span, ErrorKind::Tag))),
830 );
831 }
832
833 #[test]
834 fn fails_on_escaped_non_reserved() {
835 let err = parameter(Spanned::new("{\\r}"), &mut 0).unwrap_err();
836
837 match err {
838 Err::Failure(Error::EscapedNonReservedCharacter(e)) => {
839 assert_eq!(*e, "\\r");
840 }
841 Err::Incomplete(_) | Err::Error(_) | Err::Failure(_) => {
842 panic!("wrong error: {err:?}");
843 }
844 }
845 }
846
847 #[test]
848 fn fails_on_nested() {
849 for input in [
850 "{{nest}}",
851 "{before{nest}}",
852 "{{nest}after}",
853 "{bef{nest}aft}",
854 ] {
855 match parameter(Spanned::new(input), &mut 0).expect_err("error")
856 {
857 Err::Failure(Error::NestedParameter(e)) => {
858 assert_eq!(*e, "{nest}", "on input: {input}");
859 }
860 e => panic!("wrong error: {e:?}"),
861 }
862 }
863 }
864
865 #[test]
866 fn fails_on_optional() {
867 for input in [
868 "{(nest)}",
869 "{before(nest)}",
870 "{(nest)after}",
871 "{bef(nest)aft}",
872 ] {
873 match parameter(Spanned::new(input), &mut 0).expect_err("error")
874 {
875 Err::Failure(Error::OptionalInParameter(e)) => {
876 assert_eq!(*e, "(nest)", "on input: {input}");
877 }
878 e => panic!("wrong error: {e:?}"),
879 }
880 }
881 }
882
883 #[test]
884 fn fails_on_unescaped_reserved_char() {
885 for (input, expected) in [
886 ("{(opt}", "("),
887 ("{(n(e)st)}", "("),
888 ("{{nest}", "{"),
889 ("{l/r}", "/"),
890 ] {
891 match parameter(Spanned::new(input), &mut 0).expect_err("error")
892 {
893 Err::Failure(Error::UnescapedReservedCharacter(e)) => {
894 assert_eq!(*e, expected, "on input: {input}");
895 }
896 e => panic!("wrong error: {e:?}"),
897 }
898 }
899 }
900
901 #[test]
902 fn fails_on_unfinished() {
903 for input in ["{", "{name "] {
904 match parameter(Spanned::new(input), &mut 0).expect_err("error")
905 {
906 Err::Failure(Error::UnfinishedParameter(e)) => {
907 assert_eq!(*e, "{", "on input: {input}");
908 }
909 e => panic!("wrong error: {e:?}"),
910 }
911 }
912 }
913 }
914
915 mod optional {
916 use super::{Err, Error, ErrorKind, Spanned, optional, unwrap_parser};
917
918 #[test]
919 fn basic() {
920 assert_eq!(
921 **unwrap_parser(optional(Spanned::new("(string)"))),
922 "string",
923 );
924 }
925
926 #[test]
927 fn with_spaces() {
928 assert_eq!(
929 **unwrap_parser(optional(Spanned::new("(with space)"))),
930 "with space",
931 );
932 }
933
934 #[test]
935 fn with_escaped() {
936 assert_eq!(
937 **unwrap_parser(optional(Spanned::new("(with \\{)"))),
938 "with \\{",
939 );
940 }
941
942 #[test]
943 fn with_closing_brace() {
944 assert_eq!(
945 **unwrap_parser(optional(Spanned::new("(with })"))),
946 "with }",
947 );
948 }
949
950 #[test]
951 fn with_emoji() {
952 assert_eq!(**unwrap_parser(optional(Spanned::new("(🦀)"))), "🦀");
953 }
954
955 #[test]
956 fn errors_on_empty() {
957 let span = Spanned::new("");
958
959 assert_eq!(
960 optional(span),
961 Err(Err::Error(Error::Other(span, ErrorKind::Tag))),
962 );
963 }
964
965 #[test]
966 fn fails_on_empty() {
967 let err = optional(Spanned::new("()")).unwrap_err();
968
969 match err {
970 Err::Failure(Error::EmptyOptional(e)) => {
971 assert_eq!(*e, "()");
972 }
973 Err::Incomplete(_) | Err::Error(_) | Err::Failure(_) => {
974 panic!("wrong error: {err:?}")
975 }
976 }
977 }
978
979 #[test]
980 fn fails_on_escaped_non_reserved() {
981 let err = optional(Spanned::new("(\\r)")).unwrap_err();
982
983 match err {
984 Err::Failure(Error::EscapedNonReservedCharacter(e)) => {
985 assert_eq!(*e, "\\r");
986 }
987 Err::Incomplete(_) | Err::Error(_) | Err::Failure(_) => {
988 panic!("wrong error: {err:?}")
989 }
990 }
991 }
992
993 #[test]
994 fn fails_on_nested() {
995 for input in [
996 "((nest))",
997 "(before(nest))",
998 "((nest)after)",
999 "(bef(nest)aft)",
1000 ] {
1001 match optional(Spanned::new(input)).expect_err("error") {
1002 Err::Failure(Error::NestedOptional(e)) => {
1003 assert_eq!(*e, "(nest)", "on input: {input}");
1004 }
1005 e => panic!("wrong error: {e:?}"),
1006 }
1007 }
1008 }
1009
1010 #[test]
1011 fn fails_on_parameter() {
1012 for input in [
1013 "({nest})",
1014 "(before{nest})",
1015 "({nest}after)",
1016 "(bef{nest}aft)",
1017 ] {
1018 match optional(Spanned::new(input)).expect_err("error") {
1019 Err::Failure(Error::ParameterInOptional(e)) => {
1020 assert_eq!(*e, "{nest}", "on input: {input}");
1021 }
1022 e => panic!("wrong error: {e:?}"),
1023 }
1024 }
1025 }
1026
1027 #[test]
1028 fn fails_on_alternation() {
1029 for input in ["(/)", "(bef/)", "(/aft)", "(bef/aft)"] {
1030 match optional(Spanned::new(input)).expect_err("error") {
1031 Err::Failure(Error::AlternationInOptional(e)) => {
1032 assert_eq!(*e, "/", "on input: {input}");
1033 }
1034 e => panic!("wrong error: {e:?}"),
1035 }
1036 }
1037 }
1038
1039 #[test]
1040 fn fails_on_unescaped_reserved_char() {
1041 for (input, expected) in
1042 [("({opt)", "{"), ("({n{e}st})", "{"), ("((nest)", "(")]
1043 {
1044 match optional(Spanned::new(input)).expect_err("error") {
1045 Err::Failure(Error::UnescapedReservedCharacter(e)) => {
1046 assert_eq!(*e, expected, "on input: {input}");
1047 }
1048 e => panic!("wrong error: {e:?}"),
1049 }
1050 }
1051 }
1052
1053 #[test]
1054 fn fails_on_unfinished() {
1055 for input in ["(", "(name "] {
1056 match optional(Spanned::new(input)).expect_err("error") {
1057 Err::Failure(Error::UnfinishedOptional(e)) => {
1058 assert_eq!(*e, "(", "on input: {input}");
1059 }
1060 e => panic!("wrong error: {e:?}"),
1061 }
1062 }
1063 }
1064 }
1065
1066 mod alternative {
1067 use super::{
1068 Alternative, Err, Error, ErrorKind, Spanned, alternative,
1069 unwrap_parser,
1070 };
1071
1072 #[test]
1073 fn text() {
1074 for input in ["string", "🦀"] {
1075 match unwrap_parser(alternative(Spanned::new(input))) {
1076 Alternative::Text(t) => {
1077 assert_eq!(*t, input, "on input: {input}");
1078 }
1079 _ => panic!("expected `Alternative::Text`"),
1080 }
1081 }
1082 }
1083
1084 #[test]
1085 fn escaped_spaces() {
1086 for input in ["bef\\ ", "\\ aft", "bef\\ aft"] {
1087 match unwrap_parser(alternative(Spanned::new(input))) {
1088 Alternative::Text(t) => {
1089 assert_eq!(*t, input, "on input: {input}");
1090 }
1091 _ => panic!("expected `Alternative::Text`"),
1092 }
1093 }
1094 }
1095
1096 #[test]
1097 fn optional() {
1098 match unwrap_parser(alternative(Spanned::new("(opt)"))) {
1099 Alternative::Optional(t) => {
1100 assert_eq!(**t, "opt");
1101 }
1102 Alternative::Text(_) => {
1103 panic!("expected `Alternative::Optional`");
1104 }
1105 }
1106 }
1107
1108 #[test]
1109 fn not_captures_unescaped_whitespace() {
1110 match alternative(Spanned::new("text ")) {
1111 Ok((rest, matched)) => {
1112 assert_eq!(*rest, " ");
1113
1114 match matched {
1115 Alternative::Text(t) => assert_eq!(*t, "text"),
1116 Alternative::Optional(_) => {
1117 panic!("expected `Alternative::Text`");
1118 }
1119 }
1120 }
1121 Err(..) => panic!("expected ok"),
1122 }
1123 }
1124
1125 #[test]
1126 fn errors_on_empty() {
1127 match alternative(Spanned::new("")).unwrap_err() {
1128 Err::Error(Error::Other(_, ErrorKind::Alt)) => {}
1129 e @ (Err::Incomplete(_) | Err::Error(_) | Err::Failure(_)) => {
1130 panic!("wrong error: {e:?}");
1131 }
1132 }
1133 }
1134
1135 #[test]
1136 fn fails_on_unfinished_optional() {
1137 for input in ["(", "(opt"] {
1138 match alternative(Spanned::new(input)).unwrap_err() {
1139 Err::Failure(Error::UnfinishedOptional(e)) => {
1140 assert_eq!(*e, "(", "on input: {input}");
1141 }
1142 e => panic!("wrong error: {e:?}"),
1143 }
1144 }
1145 }
1146
1147 #[test]
1148 fn fails_on_escaped_non_reserved() {
1149 for input in ["(\\r)", "\\r"] {
1150 match alternative(Spanned::new(input)).unwrap_err() {
1151 Err::Failure(Error::EscapedNonReservedCharacter(e)) => {
1152 assert_eq!(*e, "\\r", "on input: {input}");
1153 }
1154 e => panic!("wrong error: {e:?}"),
1155 }
1156 }
1157 }
1158 }
1159
1160 mod alternation {
1161 use super::{
1162 Err, Error, ErrorKind, Spanned, alternation, assert_ast_eq,
1163 unwrap_parser,
1164 };
1165
1166 #[test]
1167 fn basic() {
1168 assert_ast_eq(
1169 unwrap_parser(alternation(Spanned::new("l/🦀"))),
1170 r#"Alternation(
1171 [
1172 [
1173 Text(
1174 LocatedSpan {
1175 offset: 0,
1176 line: 1,
1177 fragment: "l",
1178 extra: (),
1179 },
1180 ),
1181 ],
1182 [
1183 Text(
1184 LocatedSpan {
1185 offset: 2,
1186 line: 1,
1187 fragment: "🦀",
1188 extra: (),
1189 },
1190 ),
1191 ],
1192 ],
1193 )"#,
1194 );
1195 }
1196
1197 #[test]
1198 fn with_optionals() {
1199 assert_ast_eq(
1200 unwrap_parser(alternation(Spanned::new(
1201 "l(opt)/(opt)r/l(opt)r",
1202 ))),
1203 r#"Alternation(
1204 [
1205 [
1206 Text(
1207 LocatedSpan {
1208 offset: 0,
1209 line: 1,
1210 fragment: "l",
1211 extra: (),
1212 },
1213 ),
1214 Optional(
1215 Optional(
1216 LocatedSpan {
1217 offset: 2,
1218 line: 1,
1219 fragment: "opt",
1220 extra: (),
1221 },
1222 ),
1223 ),
1224 ],
1225 [
1226 Optional(
1227 Optional(
1228 LocatedSpan {
1229 offset: 8,
1230 line: 1,
1231 fragment: "opt",
1232 extra: (),
1233 },
1234 ),
1235 ),
1236 Text(
1237 LocatedSpan {
1238 offset: 12,
1239 line: 1,
1240 fragment: "r",
1241 extra: (),
1242 },
1243 ),
1244 ],
1245 [
1246 Text(
1247 LocatedSpan {
1248 offset: 14,
1249 line: 1,
1250 fragment: "l",
1251 extra: (),
1252 },
1253 ),
1254 Optional(
1255 Optional(
1256 LocatedSpan {
1257 offset: 16,
1258 line: 1,
1259 fragment: "opt",
1260 extra: (),
1261 },
1262 ),
1263 ),
1264 Text(
1265 LocatedSpan {
1266 offset: 20,
1267 line: 1,
1268 fragment: "r",
1269 extra: (),
1270 },
1271 ),
1272 ],
1273 ],
1274 )"#,
1275 );
1276 }
1277
1278 #[test]
1279 fn with_more_optionals() {
1280 assert_ast_eq(
1281 unwrap_parser(alternation(Spanned::new(
1282 "l(opt)(opt)/(opt)(opt)r/(opt)m(opt)",
1283 ))),
1284 r#"Alternation(
1285 [
1286 [
1287 Text(
1288 LocatedSpan {
1289 offset: 0,
1290 line: 1,
1291 fragment: "l",
1292 extra: (),
1293 },
1294 ),
1295 Optional(
1296 Optional(
1297 LocatedSpan {
1298 offset: 2,
1299 line: 1,
1300 fragment: "opt",
1301 extra: (),
1302 },
1303 ),
1304 ),
1305 Optional(
1306 Optional(
1307 LocatedSpan {
1308 offset: 7,
1309 line: 1,
1310 fragment: "opt",
1311 extra: (),
1312 },
1313 ),
1314 ),
1315 ],
1316 [
1317 Optional(
1318 Optional(
1319 LocatedSpan {
1320 offset: 13,
1321 line: 1,
1322 fragment: "opt",
1323 extra: (),
1324 },
1325 ),
1326 ),
1327 Optional(
1328 Optional(
1329 LocatedSpan {
1330 offset: 18,
1331 line: 1,
1332 fragment: "opt",
1333 extra: (),
1334 },
1335 ),
1336 ),
1337 Text(
1338 LocatedSpan {
1339 offset: 22,
1340 line: 1,
1341 fragment: "r",
1342 extra: (),
1343 },
1344 ),
1345 ],
1346 [
1347 Optional(
1348 Optional(
1349 LocatedSpan {
1350 offset: 25,
1351 line: 1,
1352 fragment: "opt",
1353 extra: (),
1354 },
1355 ),
1356 ),
1357 Text(
1358 LocatedSpan {
1359 offset: 29,
1360 line: 1,
1361 fragment: "m",
1362 extra: (),
1363 },
1364 ),
1365 Optional(
1366 Optional(
1367 LocatedSpan {
1368 offset: 31,
1369 line: 1,
1370 fragment: "opt",
1371 extra: (),
1372 },
1373 ),
1374 ),
1375 ],
1376 ],
1377 )"#,
1378 );
1379 }
1380
1381 #[test]
1382 fn errors_without_slash() {
1383 for (input, expected) in [
1384 ("", ErrorKind::Many1),
1385 ("{par}", ErrorKind::Many1),
1386 ("text", ErrorKind::Tag),
1387 ("(opt)", ErrorKind::Tag),
1388 ] {
1389 match alternation(Spanned::new(input)).unwrap_err() {
1390 Err::Error(Error::Other(_, kind)) => {
1391 assert_eq!(kind, expected, "on input: {input}");
1392 }
1393 e => panic!("wrong error: {e:?}"),
1394 }
1395 }
1396 }
1397
1398 #[test]
1399 fn fails_on_empty_alternation() {
1400 for input in ["/", "l/", "/r", "l/m/", "l//r", "/m/r"] {
1401 match alternation(Spanned::new(input)).unwrap_err() {
1402 Err::Failure(Error::EmptyAlternation(e)) => {
1403 assert_eq!(*e, "/", "on input: {input}");
1404 }
1405 e => panic!("wrong error: {e:?}"),
1406 }
1407 }
1408 }
1409
1410 #[test]
1411 fn fails_on_only_optional() {
1412 for input in
1413 ["text/(opt)", "text/(opt)(opt)", "(opt)/text", "(opt)/(opt)"]
1414 {
1415 match alternation(Spanned::new(input)).unwrap_err() {
1416 Err::Failure(Error::OnlyOptionalInAlternation(e)) => {
1417 assert_eq!(*e, input, "on input: {input}");
1418 }
1419 e => panic!("wrong error: {e:?}"),
1420 }
1421 }
1422 }
1423 }
1424
1425 // All test examples from: <https://git.io/J159C>
1426 // Naming of test cases is preserved.
1427 mod expression {
1428 use super::{
1429 Err, Error, Spanned, assert_ast_eq, expression, unwrap_parser,
1430 };
1431
1432 #[test]
1433 fn parameters_ids() {
1434 assert_ast_eq(
1435 unwrap_parser(expression(Spanned::new("{string} {string}"))),
1436 r#"Expression(
1437 [
1438 Parameter(
1439 Parameter {
1440 input: LocatedSpan {
1441 offset: 1,
1442 line: 1,
1443 fragment: "string",
1444 extra: (),
1445 },
1446 id: 0,
1447 },
1448 ),
1449 Whitespaces(
1450 LocatedSpan {
1451 offset: 8,
1452 line: 1,
1453 fragment: " ",
1454 extra: (),
1455 },
1456 ),
1457 Parameter(
1458 Parameter {
1459 input: LocatedSpan {
1460 offset: 10,
1461 line: 1,
1462 fragment: "string",
1463 extra: (),
1464 },
1465 id: 1,
1466 },
1467 ),
1468 ],
1469 )"#,
1470 )
1471 }
1472
1473 #[test]
1474 fn allows_escaped_optional_parameter_types() {
1475 assert_ast_eq(
1476 unwrap_parser(expression(Spanned::new("\\({int})"))),
1477 r#"Expression(
1478 [
1479 Text(
1480 LocatedSpan {
1481 offset: 0,
1482 line: 1,
1483 fragment: "\\(",
1484 extra: (),
1485 },
1486 ),
1487 Parameter(
1488 Parameter {
1489 input: LocatedSpan {
1490 offset: 3,
1491 line: 1,
1492 fragment: "int",
1493 extra: (),
1494 },
1495 id: 0,
1496 },
1497 ),
1498 Text(
1499 LocatedSpan {
1500 offset: 7,
1501 line: 1,
1502 fragment: ")",
1503 extra: (),
1504 },
1505 ),
1506 ],
1507 )"#,
1508 );
1509 }
1510
1511 #[test]
1512 fn allows_parameter_type_in_alternation() {
1513 assert_ast_eq(
1514 unwrap_parser(expression(Spanned::new("a/i{int}n/y"))),
1515 r#"Expression(
1516 [
1517 Alternation(
1518 Alternation(
1519 [
1520 [
1521 Text(
1522 LocatedSpan {
1523 offset: 0,
1524 line: 1,
1525 fragment: "a",
1526 extra: (),
1527 },
1528 ),
1529 ],
1530 [
1531 Text(
1532 LocatedSpan {
1533 offset: 2,
1534 line: 1,
1535 fragment: "i",
1536 extra: (),
1537 },
1538 ),
1539 ],
1540 ],
1541 ),
1542 ),
1543 Parameter(
1544 Parameter {
1545 input: LocatedSpan {
1546 offset: 4,
1547 line: 1,
1548 fragment: "int",
1549 extra: (),
1550 },
1551 id: 0,
1552 },
1553 ),
1554 Alternation(
1555 Alternation(
1556 [
1557 [
1558 Text(
1559 LocatedSpan {
1560 offset: 8,
1561 line: 1,
1562 fragment: "n",
1563 extra: (),
1564 },
1565 ),
1566 ],
1567 [
1568 Text(
1569 LocatedSpan {
1570 offset: 10,
1571 line: 1,
1572 fragment: "y",
1573 extra: (),
1574 },
1575 ),
1576 ],
1577 ],
1578 ),
1579 ),
1580 ],
1581 )"#,
1582 );
1583 }
1584
1585 #[test]
1586 fn does_allow_parameter_adjacent_to_alternation() {
1587 assert_ast_eq(
1588 unwrap_parser(expression(Spanned::new("{int}st/nd/rd/th"))),
1589 r#"Expression(
1590 [
1591 Parameter(
1592 Parameter {
1593 input: LocatedSpan {
1594 offset: 1,
1595 line: 1,
1596 fragment: "int",
1597 extra: (),
1598 },
1599 id: 0,
1600 },
1601 ),
1602 Alternation(
1603 Alternation(
1604 [
1605 [
1606 Text(
1607 LocatedSpan {
1608 offset: 5,
1609 line: 1,
1610 fragment: "st",
1611 extra: (),
1612 },
1613 ),
1614 ],
1615 [
1616 Text(
1617 LocatedSpan {
1618 offset: 8,
1619 line: 1,
1620 fragment: "nd",
1621 extra: (),
1622 },
1623 ),
1624 ],
1625 [
1626 Text(
1627 LocatedSpan {
1628 offset: 11,
1629 line: 1,
1630 fragment: "rd",
1631 extra: (),
1632 },
1633 ),
1634 ],
1635 [
1636 Text(
1637 LocatedSpan {
1638 offset: 14,
1639 line: 1,
1640 fragment: "th",
1641 extra: (),
1642 },
1643 ),
1644 ],
1645 ],
1646 ),
1647 ),
1648 ],
1649 )"#,
1650 );
1651 }
1652
1653 #[test]
1654 fn does_not_allow_alternation_in_optional() {
1655 match expression(Spanned::new("three( brown/black) mice"))
1656 .unwrap_err()
1657 {
1658 Err::Failure(Error::AlternationInOptional(s)) => {
1659 assert_eq!(*s, "/");
1660 }
1661 e @ (Err::Incomplete(_) | Err::Error(_) | Err::Failure(_)) => {
1662 panic!("wrong error: {e:?}");
1663 }
1664 }
1665 }
1666
1667 #[rustfmt::skip]
1668 #[test]
1669 fn does_not_allow_alternation_with_empty_alternative_by_adjacent_left_parameter(
1670 ) {
1671 match expression(Spanned::new("{int}/x")).unwrap_err() {
1672 Err::Failure(Error::EmptyAlternation(s)) => {
1673 assert_eq!(*s, "/");
1674 }
1675 e @ (Err::Incomplete(_) | Err::Error(_) | Err::Failure(_)) => {
1676 panic!("wrong error: {e:?}");
1677 }
1678 }
1679 }
1680
1681 #[rustfmt::skip]
1682 #[test]
1683 fn does_not_allow_alternation_with_empty_alternative_by_adjacent_optional(
1684 ) {
1685 match expression(Spanned::new("three (brown)/black mice"))
1686 .unwrap_err()
1687 {
1688 Err::Failure(Error::OnlyOptionalInAlternation(s)) => {
1689 assert_eq!(*s, "(brown)/black");
1690 }
1691 e @ (Err::Incomplete(_) | Err::Error(_) | Err::Failure(_)) => {
1692 panic!("wrong error: {e:?}");
1693 }
1694 }
1695 }
1696
1697 #[rustfmt::skip]
1698 #[test]
1699 fn does_not_allow_alternation_with_empty_alternative_by_adjacent_right_parameter(
1700 ) {
1701 match expression(Spanned::new("x/{int}")).unwrap_err() {
1702 Err::Failure(Error::EmptyAlternation(s)) => {
1703 assert_eq!(*s, "/");
1704 }
1705 e @ (Err::Incomplete(_) | Err::Error(_) | Err::Failure(_)) => {
1706 panic!("wrong error: {e:?}");
1707 }
1708 }
1709 }
1710
1711 #[test]
1712 fn does_not_allow_alternation_with_empty_alternative() {
1713 match expression(Spanned::new("three brown//black mice"))
1714 .unwrap_err()
1715 {
1716 Err::Failure(Error::EmptyAlternation(s)) => {
1717 assert_eq!(*s, "/");
1718 }
1719 e @ (Err::Incomplete(_) | Err::Error(_) | Err::Failure(_)) => {
1720 panic!("wrong error: {e:?}");
1721 }
1722 }
1723 }
1724
1725 #[test]
1726 fn does_not_allow_empty_optional() {
1727 match expression(Spanned::new("three () mice")).unwrap_err() {
1728 Err::Failure(Error::EmptyOptional(s)) => {
1729 assert_eq!(*s, "()");
1730 }
1731 e @ (Err::Incomplete(_) | Err::Error(_) | Err::Failure(_)) => {
1732 panic!("wrong error: {e:?}");
1733 }
1734 }
1735 }
1736
1737 #[test]
1738 fn does_not_allow_nested_optional() {
1739 match expression(Spanned::new("(a(b))")).unwrap_err() {
1740 Err::Failure(Error::NestedOptional(s)) => {
1741 assert_eq!(*s, "(b)");
1742 }
1743 e @ (Err::Incomplete(_) | Err::Error(_) | Err::Failure(_)) => {
1744 panic!("wrong error: {e:?}");
1745 }
1746 }
1747 }
1748
1749 #[test]
1750 fn does_not_allow_optional_parameter_types() {
1751 match expression(Spanned::new("({int})")).unwrap_err() {
1752 Err::Failure(Error::ParameterInOptional(s)) => {
1753 assert_eq!(*s, "{int}");
1754 }
1755 e @ (Err::Incomplete(_) | Err::Error(_) | Err::Failure(_)) => {
1756 panic!("wrong error: {e:?}");
1757 }
1758 }
1759 }
1760
1761 #[test]
1762 fn does_not_allow_parameter_name_with_reserved_characters() {
1763 match expression(Spanned::new("{(string)}")).unwrap_err() {
1764 Err::Failure(Error::OptionalInParameter(s)) => {
1765 assert_eq!(*s, "(string)");
1766 }
1767 e @ (Err::Incomplete(_) | Err::Error(_) | Err::Failure(_)) => {
1768 panic!("wrong error: {e:?}");
1769 }
1770 }
1771 }
1772
1773 #[test]
1774 fn does_not_allow_unfinished_parenthesis_1() {
1775 match expression(Spanned::new(
1776 "three (exceptionally\\) {string\\} mice",
1777 ))
1778 .unwrap_err()
1779 {
1780 Err::Failure(Error::UnescapedReservedCharacter(s)) => {
1781 assert_eq!(*s, "{");
1782 }
1783 e @ (Err::Incomplete(_) | Err::Error(_) | Err::Failure(_)) => {
1784 panic!("wrong error: {e:?}");
1785 }
1786 }
1787 }
1788
1789 #[test]
1790 fn does_not_allow_unfinished_parenthesis_2() {
1791 match expression(Spanned::new(
1792 "three (exceptionally\\) {string} mice",
1793 ))
1794 .unwrap_err()
1795 {
1796 Err::Failure(Error::ParameterInOptional(s)) => {
1797 assert_eq!(*s, "{string}");
1798 }
1799 e @ (Err::Incomplete(_) | Err::Error(_) | Err::Failure(_)) => {
1800 panic!("wrong error: {e:?}");
1801 }
1802 }
1803 }
1804
1805 #[test]
1806 fn does_not_allow_unfinished_parenthesis_3() {
1807 match expression(Spanned::new(
1808 "three ((exceptionally\\) strong) mice",
1809 ))
1810 .unwrap_err()
1811 {
1812 Err::Failure(Error::UnescapedReservedCharacter(s)) => {
1813 assert_eq!(*s, "(");
1814 }
1815 e @ (Err::Incomplete(_) | Err::Error(_) | Err::Failure(_)) => {
1816 panic!("wrong error: {e:?}");
1817 }
1818 }
1819 }
1820
1821 #[test]
1822 fn matches_alternation() {
1823 assert_ast_eq(
1824 unwrap_parser(expression(Spanned::new(
1825 "mice/rats and rats\\/mice",
1826 ))),
1827 r#"Expression(
1828 [
1829 Alternation(
1830 Alternation(
1831 [
1832 [
1833 Text(
1834 LocatedSpan {
1835 offset: 0,
1836 line: 1,
1837 fragment: "mice",
1838 extra: (),
1839 },
1840 ),
1841 ],
1842 [
1843 Text(
1844 LocatedSpan {
1845 offset: 5,
1846 line: 1,
1847 fragment: "rats",
1848 extra: (),
1849 },
1850 ),
1851 ],
1852 ],
1853 ),
1854 ),
1855 Whitespaces(
1856 LocatedSpan {
1857 offset: 9,
1858 line: 1,
1859 fragment: " ",
1860 extra: (),
1861 },
1862 ),
1863 Text(
1864 LocatedSpan {
1865 offset: 10,
1866 line: 1,
1867 fragment: "and",
1868 extra: (),
1869 },
1870 ),
1871 Whitespaces(
1872 LocatedSpan {
1873 offset: 13,
1874 line: 1,
1875 fragment: " ",
1876 extra: (),
1877 },
1878 ),
1879 Text(
1880 LocatedSpan {
1881 offset: 14,
1882 line: 1,
1883 fragment: "rats\\/mice",
1884 extra: (),
1885 },
1886 ),
1887 ],
1888 )"#,
1889 );
1890 }
1891
1892 #[test]
1893 fn matches_anonymous_parameter_type() {
1894 assert_ast_eq(
1895 unwrap_parser(expression(Spanned::new("{}"))),
1896 r#"Expression(
1897 [
1898 Parameter(
1899 Parameter {
1900 input: LocatedSpan {
1901 offset: 1,
1902 line: 1,
1903 fragment: "",
1904 extra: (),
1905 },
1906 id: 0,
1907 },
1908 ),
1909 ],
1910 )"#,
1911 );
1912 }
1913
1914 #[test]
1915 fn matches_doubly_escaped_parenthesis() {
1916 assert_ast_eq(
1917 unwrap_parser(expression(Spanned::new(
1918 "three \\(exceptionally) \\{string} mice",
1919 ))),
1920 r#"Expression(
1921 [
1922 Text(
1923 LocatedSpan {
1924 offset: 0,
1925 line: 1,
1926 fragment: "three",
1927 extra: (),
1928 },
1929 ),
1930 Whitespaces(
1931 LocatedSpan {
1932 offset: 5,
1933 line: 1,
1934 fragment: " ",
1935 extra: (),
1936 },
1937 ),
1938 Text(
1939 LocatedSpan {
1940 offset: 6,
1941 line: 1,
1942 fragment: "\\(exceptionally)",
1943 extra: (),
1944 },
1945 ),
1946 Whitespaces(
1947 LocatedSpan {
1948 offset: 22,
1949 line: 1,
1950 fragment: " ",
1951 extra: (),
1952 },
1953 ),
1954 Text(
1955 LocatedSpan {
1956 offset: 23,
1957 line: 1,
1958 fragment: "\\{string}",
1959 extra: (),
1960 },
1961 ),
1962 Whitespaces(
1963 LocatedSpan {
1964 offset: 32,
1965 line: 1,
1966 fragment: " ",
1967 extra: (),
1968 },
1969 ),
1970 Text(
1971 LocatedSpan {
1972 offset: 33,
1973 line: 1,
1974 fragment: "mice",
1975 extra: (),
1976 },
1977 ),
1978 ],
1979 )"#,
1980 );
1981 }
1982
1983 #[test]
1984 fn matches_doubly_escaped_slash() {
1985 assert_ast_eq(
1986 unwrap_parser(expression(Spanned::new("12\\\\/2020"))),
1987 r#"Expression(
1988 [
1989 Alternation(
1990 Alternation(
1991 [
1992 [
1993 Text(
1994 LocatedSpan {
1995 offset: 0,
1996 line: 1,
1997 fragment: "12\\\\",
1998 extra: (),
1999 },
2000 ),
2001 ],
2002 [
2003 Text(
2004 LocatedSpan {
2005 offset: 5,
2006 line: 1,
2007 fragment: "2020",
2008 extra: (),
2009 },
2010 ),
2011 ],
2012 ],
2013 ),
2014 ),
2015 ],
2016 )"#,
2017 );
2018 }
2019
2020 #[test]
2021 fn matches_optional_before_alternation() {
2022 assert_ast_eq(
2023 unwrap_parser(expression(Spanned::new(
2024 "three (brown )mice/rats",
2025 ))),
2026 r#"Expression(
2027 [
2028 Text(
2029 LocatedSpan {
2030 offset: 0,
2031 line: 1,
2032 fragment: "three",
2033 extra: (),
2034 },
2035 ),
2036 Whitespaces(
2037 LocatedSpan {
2038 offset: 5,
2039 line: 1,
2040 fragment: " ",
2041 extra: (),
2042 },
2043 ),
2044 Alternation(
2045 Alternation(
2046 [
2047 [
2048 Optional(
2049 Optional(
2050 LocatedSpan {
2051 offset: 7,
2052 line: 1,
2053 fragment: "brown ",
2054 extra: (),
2055 },
2056 ),
2057 ),
2058 Text(
2059 LocatedSpan {
2060 offset: 14,
2061 line: 1,
2062 fragment: "mice",
2063 extra: (),
2064 },
2065 ),
2066 ],
2067 [
2068 Text(
2069 LocatedSpan {
2070 offset: 19,
2071 line: 1,
2072 fragment: "rats",
2073 extra: (),
2074 },
2075 ),
2076 ],
2077 ],
2078 ),
2079 ),
2080 ],
2081 )"#,
2082 );
2083 }
2084
2085 #[test]
2086 fn matches_optional_in_alternation() {
2087 assert_ast_eq(
2088 unwrap_parser(expression(Spanned::new(
2089 "{int} rat(s)/mouse/mice",
2090 ))),
2091 r#"Expression(
2092 [
2093 Parameter(
2094 Parameter {
2095 input: LocatedSpan {
2096 offset: 1,
2097 line: 1,
2098 fragment: "int",
2099 extra: (),
2100 },
2101 id: 0,
2102 },
2103 ),
2104 Whitespaces(
2105 LocatedSpan {
2106 offset: 5,
2107 line: 1,
2108 fragment: " ",
2109 extra: (),
2110 },
2111 ),
2112 Alternation(
2113 Alternation(
2114 [
2115 [
2116 Text(
2117 LocatedSpan {
2118 offset: 6,
2119 line: 1,
2120 fragment: "rat",
2121 extra: (),
2122 },
2123 ),
2124 Optional(
2125 Optional(
2126 LocatedSpan {
2127 offset: 10,
2128 line: 1,
2129 fragment: "s",
2130 extra: (),
2131 },
2132 ),
2133 ),
2134 ],
2135 [
2136 Text(
2137 LocatedSpan {
2138 offset: 13,
2139 line: 1,
2140 fragment: "mouse",
2141 extra: (),
2142 },
2143 ),
2144 ],
2145 [
2146 Text(
2147 LocatedSpan {
2148 offset: 19,
2149 line: 1,
2150 fragment: "mice",
2151 extra: (),
2152 },
2153 ),
2154 ],
2155 ],
2156 ),
2157 ),
2158 ],
2159 )"#,
2160 );
2161 }
2162
2163 #[test]
2164 fn err_on_escaped_end_of_line() {
2165 match expression(Spanned::new("\\")).unwrap_err() {
2166 Err::Failure(Error::EscapedEndOfLine(_)) => {}
2167 e @ (Err::Incomplete(_) | Err::Error(_) | Err::Failure(_)) => {
2168 panic!("wrong err: {e}");
2169 }
2170 }
2171 }
2172
2173 #[test]
2174 fn empty() {
2175 assert_ast_eq(
2176 unwrap_parser(expression(Spanned::new(""))),
2177 r#"Expression([],)"#,
2178 );
2179 }
2180 }
2181}