Skip to content

Commit

Permalink
Use functions returning parser instead of functions implicitly implem…
Browse files Browse the repository at this point in the history
…enting parser (#116)

Allows for easier modification of parser behavior at runtime which might
be implemented in the future.
  • Loading branch information
SRv6d authored Sep 30, 2024
1 parent c28e176 commit 51fd3d8
Show file tree
Hide file tree
Showing 5 changed files with 401 additions and 374 deletions.
8 changes: 5 additions & 3 deletions src/attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ impl<'a> Attribute<'a> {
Self { name, value }
}

#[cfg(test)]
pub(crate) fn unchecked_single<V>(name: &'a str, value: V) -> Self
where
V: Into<Option<&'a str>>,
Expand All @@ -44,6 +45,7 @@ impl<'a> Attribute<'a> {
Self { name, value }
}

#[cfg(test)]
pub(crate) fn unchecked_multi<I, V>(name: &'a str, values: I) -> Self
where
I: IntoIterator<Item = V>,
Expand Down Expand Up @@ -95,7 +97,7 @@ impl fmt::Display for Attribute<'_> {
pub struct Name<'a>(Cow<'a, str>);

impl<'a> Name<'a> {
fn unchecked(name: &'a str) -> Self {
pub(crate) fn unchecked(name: &'a str) -> Self {
Self(Cow::Borrowed(name))
}

Expand Down Expand Up @@ -195,7 +197,7 @@ pub enum Value<'a> {
impl<'a> Value<'a> {
/// Create a single line value without checking that characters conform to any specification
/// while still coercing empty values to `None`.
fn unchecked_single<V>(value: V) -> Self
pub(crate) fn unchecked_single<V>(value: V) -> Self
where
V: Into<Option<&'a str>>,
{
Expand All @@ -204,7 +206,7 @@ impl<'a> Value<'a> {

/// Create a multi line value without checking that characters conform to any specification
/// while still coercing empty values to `None`.
fn unchecked_multi<I, V>(values: I) -> Self
pub(crate) fn unchecked_multi<I, V>(values: I) -> Self
where
I: IntoIterator<Item = V>,
V: Into<Option<&'a str>>,
Expand Down
116 changes: 8 additions & 108 deletions src/parser/main.rs → src/parser/api.rs
Original file line number Diff line number Diff line change
@@ -1,38 +1,12 @@
use winnow::{
ascii::{multispace0, newline},
combinator::{alt, delimited, repeat, terminated},
PResult, Parser,
ascii::multispace0,
combinator::{delimited, repeat},
Parser,
};

use super::component;
use super::core::{object_block, object_block_padded};
use crate::{Object, ParseError};

/// Parse an object with at least one attribute terminated by a newline.
///
/// As per [RFC 2622](https://datatracker.ietf.org/doc/html/rfc2622#section-2), an RPSL object
/// is textually represented as a list of attribute-value pairs that ends when a blank line is encountered.
fn object_block<'s>(input: &mut &'s str) -> PResult<Object<'s>> {
let (attributes, source) = terminated(repeat(1.., component::attribute), newline)
.with_taken()
.parse_next(input)?;
Ok(Object::from_parsed(source, attributes))
}

/// Extends the object block parser to consume optional padding server messages or newlines.
fn object_block_padded<'s>(input: &mut &'s str) -> PResult<Object<'s>> {
delimited(
consume_opt_message_or_newlines,
object_block,
consume_opt_message_or_newlines,
)
.parse_next(input)
}

/// Consume optional server messages or newlines.
fn consume_opt_message_or_newlines(input: &mut &str) -> PResult<()> {
repeat(0.., alt((newline.void(), component::server_message.void()))).parse_next(input)
}

/// Parse RPSL into an [`Object`], borrowing from the source.
///
/// ```text
Expand Down Expand Up @@ -152,7 +126,8 @@ fn consume_opt_message_or_newlines(input: &mut &str) -> PResult<()> {
/// # }
/// ```
pub fn parse_object(rpsl: &str) -> Result<Object, ParseError> {
let object = delimited(multispace0, object_block, multispace0).parse(rpsl)?;
let block_parser = object_block();
let object = delimited(multispace0, block_parser, multispace0).parse(rpsl)?;
Ok(object)
}

Expand Down Expand Up @@ -217,82 +192,7 @@ pub fn parse_object(rpsl: &str) -> Result<Object, ParseError> {
/// # Ok(())
/// # }
pub fn parse_whois_response(response: &str) -> Result<Vec<Object>, ParseError> {
let objects = repeat(1.., object_block_padded).parse(response)?;
let block_parser = object_block_padded(object_block());
let objects = repeat(1.., block_parser).parse(response)?;
Ok(objects)
}

#[cfg(test)]
mod tests {
use rstest::*;

use super::*;
use crate::{Attribute, Object};

#[test]
fn object_block_valid() {
let object = &mut concat!(
"email: [email protected]\n",
"nic-hdl: RPSL1-RIPE\n",
"\n"
);
assert_eq!(
object_block(object),
Ok(Object::from_parsed(
object,
vec![
Attribute::unchecked_single("email", "[email protected]"),
Attribute::unchecked_single("nic-hdl", "RPSL1-RIPE")
]
))
);
}

#[test]
/// When parsing RPSL, the resulting object contains the original source it was created from.
fn parsed_object_contains_source() {
let rpsl = &mut concat!(
"email: [email protected]\n",
"nic-hdl: RPSL1-RIPE\n",
"\n"
);
let source = *rpsl;
let object = object_block(rpsl).unwrap();
assert_eq!(object.source().unwrap(), source);
}

#[test]
fn object_block_without_newline_termination_is_err() {
let object = &mut concat!(
"email: [email protected]\n",
"nic-hdl: RPSL1-RIPE\n",
);
assert!(object_block(object).is_err());
}

#[rstest]
#[case(
&mut "% Note: This is a server message\n"
)]
#[case(
&mut concat!(
"\n",
"% Note: This is a server message followed by an empty line\n"
)
)]
#[case(
&mut concat!(
"% Note: This is a server message preceding some newlines.\n",
"\n",
"\n",
)
)]
fn optional_comment_or_newlines_consumed(#[case] given: &mut &str) {
consume_opt_message_or_newlines(given).unwrap();
assert_eq!(*given, "");
}

#[test]
fn optional_comment_or_newlines_optional() {
assert_eq!(consume_opt_message_or_newlines(&mut ""), Ok(()));
}
}
Loading

0 comments on commit 51fd3d8

Please sign in to comment.