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}