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