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}