diff --git a/src/combinators.rs b/src/combinators.rs index 9f9798d..3c88269 100644 --- a/src/combinators.rs +++ b/src/combinators.rs @@ -675,4 +675,78 @@ macro_rules! until { ($i:expr, $term:ident) => { consume_until!($i, run!($term)) }; +} + +/// Discards the output of a combinator rule when it completes and just returns `()`. +/// Leaves Failures, Aborts, and Incompletes untouched. +#[macro_export] +macro_rules! discard { + ($i:expr, $term:ident) => { + discard!($i, run!($term)) + }; + + ($i:expr, $term:ident!( $( $args:tt )* ) ) => {{ + use $crate::Result; + match $term!($i, $($args)*) { + Result::Complete(i, _) => Result::Complete(i, ()), + Result::Incomplete(offset) => Result::Incomplete(offset), + Result::Fail(e) => Result::Fail(e), + Result::Abort(e) => Result::Abort(e), + } + }}; +} + +/// Matches and returns any ascii charactar whitespace byte. +pub fn ascii_ws<'a, I: InputIter>(mut i: I) -> Result { + match i.next() { + Some(b) => { + match b { + b'\r' => Result::Complete(i, *b), + b'\n' => Result::Complete(i, *b), + b'\t' => Result::Complete(i, *b), + b' ' => Result::Complete(i, *b), + _ => Result::Fail(Error::new("Not whitespace", &i)), + } + }, + None => { + Result::Fail(Error::new("Unexpected End Of Input", &i)) + } + } +} + +/// Matches the end of input for any InputIter. +/// Returns `()` for any match. +pub fn eoi(i: I) -> Result { + let mut _i = i.clone(); + match _i.next() { + Some(_) => Result::Fail(Error::new("Expected End Of Input", &i)), + None => Result::Complete(i, ()), + } +} + +/// constructs a function named $name that takes an input of type $i and produces an output +/// of type $o. +/// +#[macro_export] +macro_rules! make_fn { + ($name:ident<$i:ty, $o:ty>, $rule:ident!($( $body:tt )* )) => { + fn $name(i: $i) -> Result<$i,$o> { + $rule!(i, $($body)*) + } + }; + + (pub $name:ident<$i:ty, $o:ty>, $rule:ident!($( $body:tt )* )) => { + pub fn $name(i: $i) -> Result<$i,$o> { + $rule!(i, $($body)*) + } + }; + + ($name:ident<$i:ty, $o:ty>, $rule:ident) => { + make_fn!($name<$i, $o>, run!($rule)) + }; + + (pub $name:ident<$i:ty, $o:ty>, $rule:ident) => { + make_fn!(pub $name<$i, $o>, run!($rule)) + }; + } \ No newline at end of file diff --git a/src/integration_tests.rs b/src/integration_tests.rs new file mode 100644 index 0000000..339769d --- /dev/null +++ b/src/integration_tests.rs @@ -0,0 +1,42 @@ + use iter::StrIter; + use super::{Result, eoi, ascii_ws}; + + make_fn!(proto, + do_each!( + proto => until!(text_token!("://")), + _ => must!(text_token!("://")), + (proto) + ) + ); + + make_fn!(domain, + until!(either!( + discard!(text_token!("/")), + discard!(ascii_ws), + eoi)) + ); + + make_fn!(path, + until!(either!(discard!(ascii_ws), eoi)) + ); + + make_fn!(pub url, Option<&str>, &str)>, + do_each!( + protocol => optional!(proto), + domain => optional!(domain), + path => path, + (protocol, domain, path) + ) + ); + +#[test] +fn test_url_parser() { + let iter = StrIter::new("http://example.com/some/path "); + let result = url(iter); + assert!(result.is_complete()); + if let Result::Complete(_, (proto, domain, path)) = result { + assert!(proto.is_some()); + assert!(domain.is_some()); + assert_eq!(path, "/some/path"); + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index b672953..87c62bf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,52 @@ //! An opinionated parser combinator library with a focus on fully abortable parsing and error handling. +//! +//! # Example +//! +//! ``` +//! #[macro_use] +//! extern crate abortable_parser; +//! use abortable_parser::iter::StrIter; +//! use abortable_parser::{Result, eoi, ascii_ws}; +//! +//! make_fn!(proto, +//! do_each!( +//! proto => until!(text_token!("://")), +//! _ => must!(text_token!("://")), +//! (proto) +//! ) +//! ); +//! +//! make_fn!(domain, +//! until!(either!( +//! discard!(text_token!("/")), +//! discard!(ascii_ws), +//! eoi)) +//! ); +//! +//! make_fn!(path, +//! until!(either!(discard!(ascii_ws), eoi)) +//! ); +//! +//! make_fn!(url, Option<&str>, &str)>, +//! do_each!( +//! protocol => optional!(proto), +//! domain => optional!(domain), +//! path => path, +//! (protocol, domain, path) +//! ) +//! ); +//! +//! # fn main() { +//! let iter = StrIter::new("http://example.com/some/path "); +//! let result = url(iter); +//! assert!(result.is_complete()); +//! if let Result::Complete(_, (proto, domain, path)) = result { +//! assert!(proto.is_some()); +//! assert!(domain.is_some()); +//! assert_eq!(path, "/some/path"); +//! } +//! # } +//! ``` use std::fmt::Display; use std::iter::Iterator; @@ -142,6 +190,7 @@ impl Result { } pub use iter::SliceIter; +pub use combinators::*; #[macro_use] pub mod combinators; @@ -149,3 +198,5 @@ pub mod iter; #[cfg(test)] mod test; +#[cfg(test)] +mod integration_tests; \ No newline at end of file diff --git a/src/test.rs b/src/test.rs index 324afa5..a7f1b6e 100644 --- a/src/test.rs +++ b/src/test.rs @@ -2,6 +2,7 @@ use std::fmt::{Debug, Display}; use super::{InputIter, Offsetable, Result}; use iter::{StrIter, SliceIter}; +use combinators::*; #[test] fn test_slice_iter() { @@ -377,4 +378,87 @@ fn test_until_incomplete() { let iter = SliceIter::new(input_str.as_bytes()); let result = until!(iter, text_token!("; ")); assert!(result.is_incomplete()); +} + +#[test] +fn test_discard_success() { + let input_str = "foo"; + let iter = SliceIter::new(input_str.as_bytes()); + let result = discard!(iter, text_token!("foo")); + assert!(result.is_complete()); + if let Result::Complete(_, o) = result { + assert_eq!(o, ()); + } +} + +#[test] +fn test_discard_fail() { + let input_str = "foo"; + let iter = SliceIter::new(input_str.as_bytes()); + let result = discard!(iter, will_fail); + assert!(result.is_fail()); +} + +#[test] +fn test_discard_abort() { + let input_str = "foo"; + let iter = SliceIter::new(input_str.as_bytes()); + let result = discard!(iter, must!(will_fail)); + assert!(result.is_abort()); +} + +#[test] +fn test_discard_incomplete() { + let input_str = "foo"; + let iter = SliceIter::new(input_str.as_bytes()); + let result = discard!(iter, will_not_complete); + assert!(result.is_incomplete()); +} + +#[test] +fn test_eoi_success() { + let input_str = ""; + let iter = StrIter::new(input_str); + let result = eoi(iter); + assert!(result.is_complete()); +} + +#[test] +fn test_eoi_fail() { + let input_str = " "; + let iter = StrIter::new(input_str); + let result = eoi(iter); + assert!(result.is_fail()); +} + +#[test] +fn test_ascii_ws_space() { + let input_str = " "; + let iter = StrIter::new(input_str); + let result = ascii_ws(iter); + assert!(result.is_complete()); +} + +#[test] +fn test_ascii_ws_tab() { + let input_str = "\t"; + let iter = StrIter::new(input_str); + let result = ascii_ws(iter); + assert!(result.is_complete()); +} + +#[test] +fn test_ascii_ws_newline() { + let input_str = "\n"; + let iter = StrIter::new(input_str); + let result = ascii_ws(iter); + assert!(result.is_complete()); +} + +#[test] +fn test_ascii_ws_carriage_return() { + let input_str = "\r"; + let iter = StrIter::new(input_str); + let result = ascii_ws(iter); + assert!(result.is_complete()); } \ No newline at end of file