-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Move multiline attribute value parsing out of attribute parser
- Loading branch information
Showing
2 changed files
with
57 additions
and
52 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,8 +2,8 @@ use std::iter::once; | |
|
||
use winnow::{ | ||
ascii::{newline, space0}, | ||
combinator::{delimited, peek, preceded, repeat, separated_pair, terminated}, | ||
error::{ContextError, ParserError}, | ||
combinator::{delimited, opt, preceded, repeat, separated_pair, terminated}, | ||
error::ParserError, | ||
token::{one_of, take_while}, | ||
PResult, Parser, | ||
}; | ||
|
@@ -22,28 +22,14 @@ pub fn server_message<'s>(input: &mut &'s str) -> PResult<&'s str> { | |
.parse_next(input) | ||
} | ||
|
||
// A RPSL attribute consisting of a name and one or more values. | ||
// The name is followed by a colon and optional spaces. | ||
// Single value attributes are limited to one line, while multi value attributes span over multiple lines. | ||
pub fn attribute<'s>(input: &mut &'s str) -> PResult<Attribute<'s>> { | ||
let (name, first_value) = separated_pair( | ||
attribute_name(), | ||
(':', space0), | ||
terminated(attribute_value(), newline), | ||
) | ||
.parse_next(input)?; | ||
|
||
if peek(continuation_char::<ContextError>()) | ||
.parse_next(input) | ||
.is_ok() | ||
{ | ||
let continuation_values: Vec<&str> = | ||
repeat(1.., continuation_line(attribute_value())).parse_next(input)?; | ||
let value = Value::unchecked_multi(once(first_value).chain(continuation_values)); | ||
return Ok(Attribute::new(name, value)); | ||
} | ||
|
||
Ok(Attribute::new(name, Value::unchecked_single(first_value))) | ||
// Generate an attribute parser. | ||
// The attributes name and value are separated by a colon and optional spaces. | ||
pub fn attribute<'s, E>() -> impl Parser<&'s str, Attribute<'s>, E> | ||
where | ||
E: ParserError<&'s str>, | ||
{ | ||
separated_pair(attribute_name(), (':', space0), attribute_value_opt_multi()) | ||
.map(|(name, value)| Attribute::new(name, value)) | ||
} | ||
|
||
/// Generate an attribute value parser that parses an ASCII sequence of letters, | ||
|
@@ -66,17 +52,40 @@ fn attribute_value<'s, E>() -> impl Parser<&'s str, &'s str, E> | |
where | ||
E: ParserError<&'s str>, | ||
{ | ||
take_while(0.., |c| Value::validate_char(c).is_ok()) | ||
terminated( | ||
take_while(0.., |c| Value::validate_char(c).is_ok()), | ||
newline, | ||
) | ||
} | ||
|
||
/// Generate a parser that extends an attributes value over multiple lines, | ||
/// where each value is prefixed with a continuation character. | ||
fn continuation_line<'s, P, E>(value_parser: P) -> impl Parser<&'s str, &'s str, E> | ||
/// Generate a parser for continuation values. | ||
fn attribute_value_continuation<'s, P, E>(value_parser: P) -> impl Parser<&'s str, Vec<&'s str>, E> | ||
where | ||
P: Parser<&'s str, &'s str, E>, | ||
E: ParserError<&'s str>, | ||
{ | ||
delimited(continuation_char(), preceded(space0, value_parser), newline) | ||
repeat( | ||
1.., | ||
preceded(continuation_char(), preceded(space0, value_parser)), | ||
) | ||
} | ||
|
||
/// Generate an attribute value parser that optionally parses continuation lines. | ||
fn attribute_value_opt_multi<'s, E>() -> impl Parser<&'s str, Value<'s>, E> | ||
where | ||
E: ParserError<&'s str>, | ||
{ | ||
( | ||
attribute_value(), | ||
opt(attribute_value_continuation(attribute_value())), | ||
) | ||
.map(|(first_value, continuation)| { | ||
if let Some(continuation_values) = continuation { | ||
Value::unchecked_multi(once(first_value).chain(continuation_values)) | ||
} else { | ||
Value::unchecked_single(first_value) | ||
} | ||
}) | ||
} | ||
|
||
/// Generate a parser for a single continuation character. | ||
|
@@ -91,6 +100,7 @@ where | |
mod tests { | ||
use proptest::prelude::*; | ||
use rstest::*; | ||
use winnow::error::ContextError; | ||
|
||
use super::*; | ||
|
||
|
@@ -131,7 +141,7 @@ mod tests { | |
#[case] expected: Attribute, | ||
#[case] remaining: &str, | ||
) { | ||
let parsed = attribute(given).unwrap(); | ||
let parsed = attribute::<ContextError>().parse_next(given).unwrap(); | ||
assert_eq!(parsed, expected); | ||
assert_eq!(*given, remaining); | ||
} | ||
|
@@ -159,7 +169,7 @@ mod tests { | |
#[case] expected: Attribute, | ||
#[case] remaining: &str, | ||
) { | ||
let parsed = attribute(given).unwrap(); | ||
let parsed = attribute::<ContextError>().parse_next(given).unwrap(); | ||
assert_eq!(parsed, expected); | ||
assert_eq!(*given, remaining); | ||
} | ||
|
@@ -207,27 +217,22 @@ mod tests { | |
#[case( | ||
&mut "This is an example remark\n", | ||
"This is an example remark", | ||
"\n" | ||
"" | ||
)] | ||
#[case( | ||
&mut "Concerning abuse and spam ... mailto: [email protected]\n", | ||
"Concerning abuse and spam ... mailto: [email protected]", | ||
"\n" | ||
"" | ||
)] | ||
#[case( | ||
&mut "+49 176 07071964\n", | ||
"+49 176 07071964", | ||
"\n" | ||
"" | ||
)] | ||
#[case( | ||
&mut "* Equinix FR5, Kleyerstr, Frankfurt am Main\n", | ||
"* Equinix FR5, Kleyerstr, Frankfurt am Main", | ||
"\n" | ||
)] | ||
#[case( | ||
&mut "\n", | ||
"", | ||
"\n" | ||
"" | ||
)] | ||
fn attribute_value_valid( | ||
#[case] given: &mut &str, | ||
|
@@ -241,38 +246,38 @@ mod tests { | |
} | ||
|
||
proptest! { | ||
/// Any non extended ASCII is not returned by the value parser. | ||
/// Parsing any non extended ASCII returns an error. | ||
#[test] | ||
fn attribute_value_non_extended_ascii_not_parsed(s in r"[^\x00-\xFF]+") { | ||
fn attribute_value_non_extended_ascii_is_err(s in r"[^\x00-\xFF]+") { | ||
let mut parser = attribute_value::<ContextError>(); | ||
let parsed = parser.parse_next(&mut s.as_str()).unwrap(); | ||
prop_assert_eq!(parsed, ""); | ||
assert!(parser.parse_next(&mut s.as_str()).is_err()); | ||
} | ||
} | ||
|
||
#[rstest] | ||
#[case( | ||
&mut " continuation value prefixed by a space\n", | ||
"continuation value prefixed by a space", | ||
vec!["continuation value prefixed by a space"], | ||
"" | ||
)] | ||
#[case( | ||
&mut "\t continuation value prefixed by a tab\n", | ||
"continuation value prefixed by a tab", | ||
vec!["continuation value prefixed by a tab"], | ||
"" | ||
)] | ||
#[case( | ||
&mut "+ continuation value prefixed by a plus\n", | ||
"continuation value prefixed by a plus", | ||
vec!["continuation value prefixed by a plus"], | ||
"" | ||
)] | ||
fn continuation_line_valid( | ||
fn attribute_value_continuation_valid( | ||
#[case] given: &mut &str, | ||
#[case] expected: &str, | ||
#[case] expected: Vec<&str>, | ||
#[case] remaining: &str, | ||
) { | ||
let mut parser = continuation_line::<_, ContextError>(attribute_value()); | ||
let parsed = parser.parse_next(given).unwrap(); | ||
let value_parser = attribute_value::<ContextError>(); | ||
let mut continuation_parser = attribute_value_continuation::<_, ContextError>(value_parser); | ||
let parsed = continuation_parser.parse_next(given).unwrap(); | ||
assert_eq!(parsed, expected); | ||
assert_eq!(*given, remaining); | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters