diff --git a/src/lib.rs b/src/lib.rs index 8a4d51779..a921f6074 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ pub use record::{ByteRecord, StringRecord}; mod error; pub mod matcher; +mod parser; pub mod prelude; pub mod primitives; mod record; diff --git a/src/matcher.rs b/src/matcher/mod.rs similarity index 52% rename from src/matcher.rs rename to src/matcher/mod.rs index 902a7a072..b959ccce6 100644 --- a/src/matcher.rs +++ b/src/matcher/mod.rs @@ -1,23 +1,16 @@ -//! Various matcher against records (and record primitives). +//! Various matcher against record primitives. pub use error::ParseMatcherError; pub use occurrence::OccurrenceMatcher; pub use operator::{BooleanOp, RelationalOp}; pub use options::MatcherOptions; pub use quantifier::Quantifier; -pub use subfield::{ - CardinalityMatcher, ExistsMatcher, InMatcher, RegexMatcher, - RegexSetMatcher, RelationMatcher, SingletonMatcher, - SubfieldMatcher, -}; pub use tag::TagMatcher; mod error; mod occurrence; mod operator; mod options; -mod parse; mod quantifier; -mod string; -mod subfield; +pub mod subfield; mod tag; diff --git a/src/matcher/occurrence.rs b/src/matcher/occurrence.rs index b28ef258f..d036605e6 100644 --- a/src/matcher/occurrence.rs +++ b/src/matcher/occurrence.rs @@ -1,12 +1,15 @@ +//! Matcher that can be applied on a list of [OccurrenceRef]. + use std::fmt::{self, Display}; -use winnow::Parser; +use winnow::combinator::{alt, empty, preceded, separated_pair}; +use winnow::prelude::*; -use super::parse::parse_occurrence_matcher; use super::ParseMatcherError; +use crate::primitives::parse::parse_occurrence_ref; use crate::primitives::{Occurrence, OccurrenceRef}; -/// A matcher that checks for occurrences (or no occurrence). +/// A matcher that matches against a [OccurrenceRef]. #[derive(Debug, Clone, PartialEq)] pub enum OccurrenceMatcher { Exact(Occurrence), @@ -97,3 +100,107 @@ impl Display for OccurrenceMatcher { Ok(()) } } + +#[inline] +fn parse_occurrence_matcher_inner( + i: &mut &[u8], +) -> PResult { + parse_occurrence_ref.map(Occurrence::from).parse_next(i) +} + +#[inline] +fn parse_occurrence_matcher_exact( + i: &mut &[u8], +) -> PResult { + parse_occurrence_matcher_inner + .verify(|occurrence| occurrence.as_bytes() != b"00") + .map(OccurrenceMatcher::Exact) + .parse_next(i) +} + +#[inline] +fn parse_occurrence_matcher_range( + i: &mut &[u8], +) -> PResult { + separated_pair( + parse_occurrence_matcher_inner, + '-', + parse_occurrence_matcher_inner, + ) + .verify(|(min, max)| min.len() == max.len() && min < max) + .map(|(min, max)| OccurrenceMatcher::Range(min, max)) + .parse_next(i) +} + +pub(crate) fn parse_occurrence_matcher( + i: &mut &[u8], +) -> PResult { + alt(( + preceded( + '/', + alt(( + parse_occurrence_matcher_range, + parse_occurrence_matcher_exact, + "00".value(OccurrenceMatcher::None), + "*".value(OccurrenceMatcher::Any), + )), + ), + empty.value(OccurrenceMatcher::None), + )) + .parse_next(i) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_occurrence_matcher() -> anyhow::Result<()> { + macro_rules! parse_success { + ($i:expr, $o:expr) => { + let o = parse_occurrence_matcher + .parse($i.as_bytes()) + .unwrap(); + + assert_eq!(o.to_string(), $i); + assert_eq!(o, $o); + }; + } + + parse_success!("", OccurrenceMatcher::None); + parse_success!("/*", OccurrenceMatcher::Any); + + parse_success!( + "/01", + OccurrenceMatcher::Exact(Occurrence::new("01")?) + ); + + parse_success!( + "/999", + OccurrenceMatcher::Exact(Occurrence::new("999")?) + ); + + parse_success!( + "/01-99", + OccurrenceMatcher::Range( + Occurrence::new("01")?, + Occurrence::new("99")? + ) + ); + + parse_success!( + "/01-99", + OccurrenceMatcher::Range( + Occurrence::new("01")?, + Occurrence::new("99")? + ) + ); + + assert_eq!( + parse_occurrence_matcher.parse(b"/00").unwrap(), + OccurrenceMatcher::None + ); + + Ok(()) + } +} diff --git a/src/matcher/operator.rs b/src/matcher/operator.rs index 67d0a02c1..72a8f1389 100644 --- a/src/matcher/operator.rs +++ b/src/matcher/operator.rs @@ -1,5 +1,8 @@ use std::fmt::{self, Display}; +use winnow::combinator::alt; +use winnow::{PResult, Parser}; + /// Relational Operator #[derive(Debug, Clone, PartialEq, Eq)] pub enum RelationalOp { @@ -130,6 +133,30 @@ impl quickcheck::Arbitrary for RelationalOp { } } +/// Parse RelationalOp which can be used for string comparisons. +#[inline] +pub(crate) fn parse_relational_operator( + i: &mut &[u8], +) -> PResult { + use RelationalOp::*; + + alt(( + "==".value(Equal), + "!=".value(NotEqual), + "=^".value(StartsWith), + "!^".value(StartsNotWith), + "=$".value(EndsWith), + "!$".value(EndsNotWith), + "=*".value(Similar), + "=?".value(Contains), + ">=".value(GreaterThanOrEqual), + ">".value(GreaterThan), + "<=".value(LessThanOrEqual), + "<".value(LessThan), + )) + .parse_next(i) +} + /// Boolean Operators. #[derive(Debug, Clone, PartialEq, Eq)] pub enum BooleanOp { @@ -154,3 +181,37 @@ impl quickcheck::Arbitrary for BooleanOp { g.choose(&[Self::And, Self::Or, Self::Xor]).unwrap().clone() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_relational_operator() { + use RelationalOp::*; + + macro_rules! parse_success { + ($input:expr, $expected:expr) => { + assert_eq!( + parse_relational_operator + .parse($input.as_bytes()) + .unwrap(), + $expected + ); + }; + } + + parse_success!("==", Equal); + parse_success!("!=", NotEqual); + parse_success!(">=", GreaterThanOrEqual); + parse_success!(">", GreaterThan); + parse_success!("<=", LessThanOrEqual); + parse_success!("<", LessThan); + parse_success!("=^", StartsWith); + parse_success!("!^", StartsNotWith); + parse_success!("=$", EndsWith); + parse_success!("!$", EndsNotWith); + parse_success!("=*", Similar); + parse_success!("=?", Contains); + } +} diff --git a/src/matcher/quantifier.rs b/src/matcher/quantifier.rs index 1e116a9cb..a058756f2 100644 --- a/src/matcher/quantifier.rs +++ b/src/matcher/quantifier.rs @@ -1,5 +1,8 @@ use std::fmt::{self, Display}; +use winnow::combinator::alt; +use winnow::{PResult, Parser}; + #[derive(Debug, Clone, PartialEq, Eq, Default)] pub enum Quantifier { All, @@ -25,3 +28,22 @@ impl quickcheck::Arbitrary for Quantifier { } } } + +#[inline] +pub(crate) fn parse_quantifier(i: &mut &[u8]) -> PResult { + alt(("ALL".value(Quantifier::All), "ANY".value(Quantifier::Any))) + .parse_next(i) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_quantifier() { + use Quantifier::*; + + assert_eq!(parse_quantifier.parse(b"ALL").unwrap(), All); + assert_eq!(parse_quantifier.parse(b"ANY").unwrap(), Any); + } +} diff --git a/src/matcher/string.rs b/src/matcher/string.rs deleted file mode 100644 index d705e54d4..000000000 --- a/src/matcher/string.rs +++ /dev/null @@ -1,169 +0,0 @@ -use winnow::ascii::multispace1; -use winnow::combinator::{alt, delimited, preceded, repeat}; -use winnow::error::{ContextError, ParserError}; -use winnow::prelude::*; -use winnow::stream::{AsChar, Compare, Stream, StreamIsPartial}; -use winnow::token::take_till; - -#[derive(Debug, Copy, Clone)] -enum Quotes { - Single, - Double, -} - -fn parse_literal( - quotes: Quotes, -) -> impl Parser::Slice, E> -where - I: Stream + StreamIsPartial, - ::Token: AsChar, - E: ParserError, -{ - match quotes { - Quotes::Single => take_till(1.., ['\'', '\\']), - Quotes::Double => take_till(1.., ['"', '\\']), - } -} - -fn parse_escaped_char(quotes: Quotes) -> impl Parser -where - I: Stream + StreamIsPartial + Compare, - ::Token: AsChar + Clone, - E: ParserError, -{ - let v = match quotes { - Quotes::Single => '\'', - Quotes::Double => '"', - }; - - preceded( - '\\', - alt(( - 'n'.value('\n'), - 'r'.value('\r'), - 't'.value('\t'), - 'b'.value('\u{08}'), - 'f'.value('\u{0C}'), - '\\'.value('\\'), - '/'.value('/'), - v.value(v), - )), - ) -} - -#[derive(Debug, Clone)] -enum StringFragment<'a> { - Literal(&'a [u8]), - EscapedChar(char), - EscapedWs, -} - -fn parse_quoted_fragment<'a, E: ParserError<&'a [u8]>>( - quotes: Quotes, -) -> impl Parser<&'a [u8], StringFragment<'a>, E> { - use StringFragment::*; - - alt(( - parse_literal::<&'a [u8], E>(quotes).map(Literal), - parse_escaped_char::<&'a [u8], E>(quotes).map(EscapedChar), - preceded('\\', multispace1).value(EscapedWs), - )) -} - -fn parse_quoted_string<'a, E>( - quotes: Quotes, -) -> impl Parser<&'a [u8], Vec, E> -where - E: ParserError<&'a [u8]>, -{ - use StringFragment::*; - - let string_builder = repeat( - 0.., - parse_quoted_fragment::(quotes), - ) - .fold(Vec::new, |mut acc, fragment| { - match fragment { - Literal(s) => acc.extend_from_slice(s), - EscapedChar(c) => acc.push(c as u8), - EscapedWs => {} - } - acc - }); - - match quotes { - Quotes::Single => delimited('\'', string_builder, '\''), - Quotes::Double => delimited('"', string_builder, '"'), - } -} - -#[inline] -fn parse_string_single_quoted(i: &mut &[u8]) -> PResult> { - parse_quoted_string::(Quotes::Single).parse_next(i) -} - -#[inline] -fn parse_string_double_quoted(i: &mut &[u8]) -> PResult> { - parse_quoted_string::(Quotes::Double).parse_next(i) -} - -pub(crate) fn parse_string(i: &mut &[u8]) -> PResult> { - alt((parse_string_single_quoted, parse_string_double_quoted)) - .parse_next(i) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_string_single_quoted() { - macro_rules! parse_success { - ($input:expr, $output:expr) => { - assert_eq!( - parse_string_single_quoted - .parse($input.as_bytes()) - .unwrap(), - $output, - ); - }; - } - - parse_success!("'abc'", b"abc"); - parse_success!("'a\\nbc'", b"a\nbc"); - parse_success!("'a\\rbc'", b"a\rbc"); - parse_success!("'a\\tbc'", b"a\tbc"); - parse_success!("'a\\bbc'", b"a\x08bc"); - parse_success!("'a\\fbc'", b"a\x0Cbc"); - parse_success!("'a\\\\c'", b"a\\c"); - parse_success!("'a\\'c'", b"a\'c"); - parse_success!("'a\"c'", b"a\"c"); - parse_success!("'a\\/bc'", b"a/bc"); - parse_success!("'a\\ bc'", b"abc"); - } - - #[test] - fn test_parse_string_double_quoted() { - macro_rules! parse_success { - ($input:expr, $output:expr) => { - assert_eq!( - parse_string_double_quoted - .parse($input.as_bytes()) - .unwrap(), - $output, - ); - }; - } - - parse_success!("\"abc\"", b"abc"); - parse_success!("\"a\\nbc\"", b"a\nbc"); - parse_success!("\"a\\rbc\"", b"a\rbc"); - parse_success!("\"a\\tbc\"", b"a\tbc"); - parse_success!("\"a\\bbc\"", b"a\x08bc"); - parse_success!("\"a\\fbc\"", b"a\x0Cbc"); - parse_success!("\"a\\\\c\"", b"a\\c"); - parse_success!("\"a'c\"", b"a'c"); - parse_success!("\"a\\/bc\"", b"a/bc"); - parse_success!("\"a\\ bc\"", b"abc"); - } -} diff --git a/src/matcher/subfield.rs b/src/matcher/subfield/mod.rs similarity index 95% rename from src/matcher/subfield.rs rename to src/matcher/subfield/mod.rs index e4425d64c..f9e70f074 100644 --- a/src/matcher/subfield.rs +++ b/src/matcher/subfield/mod.rs @@ -1,24 +1,26 @@ +//! Matcher that can be applied on a list of [SubfieldRef]. + use std::fmt::{self, Display}; use std::ops::{ BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not, }; use bstr::ByteSlice; +use parser::{ + parse_cardinality_matcher, parse_exists_matcher, parse_in_matcher, + parse_regex_matcher, parse_regex_set_matcher, + parse_relation_matcher, parse_singleton_matcher, + parse_subfield_matcher, +}; use regex::bytes::{RegexBuilder, RegexSetBuilder}; use smallvec::SmallVec; use strsim::normalized_levenshtein; use winnow::Parser; -use super::parse::{ - parse_cardinality_matcher, parse_in_matcher, parse_regex_matcher, - parse_regex_set_matcher, parse_relation_matcher, - parse_singleton_matcher, parse_subfield_matcher, -}; use super::{ BooleanOp, MatcherOptions, ParseMatcherError, Quantifier, RelationalOp, }; -use crate::matcher::parse::parse_exists_matcher; use crate::primitives::{SubfieldCode, SubfieldRef}; /// A matcher that checks for the existance of subfields. @@ -28,6 +30,8 @@ pub struct ExistsMatcher { pub(crate) raw_data: String, } +pub(crate) mod parser; + impl ExistsMatcher { /// Creates a new [ExistsMatcher]. /// @@ -39,7 +43,7 @@ impl ExistsMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::ExistsMatcher; + /// use pica_record::matcher::subfield::ExistsMatcher; /// /// let _matcher = ExistsMatcher::new("a?")?; /// let _matcher = ExistsMatcher::new("[a-c]?")?; @@ -61,7 +65,8 @@ impl ExistsMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::{ExistsMatcher, MatcherOptions}; + /// use pica_record::matcher::subfield::ExistsMatcher; + /// use pica_record::matcher::MatcherOptions; /// use pica_record::primitives::SubfieldRef; /// /// let options = MatcherOptions::default(); @@ -100,7 +105,8 @@ impl Display for ExistsMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::{ExistsMatcher, MatcherOptions}; + /// use pica_record::matcher::subfield::ExistsMatcher; + /// use pica_record::matcher::MatcherOptions; /// use pica_record::primitives::SubfieldRef; /// /// let matcher = ExistsMatcher::new("[a0-3]?")?; @@ -135,7 +141,7 @@ impl RelationMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::RelationMatcher; + /// use pica_record::matcher::subfield::RelationMatcher; /// /// let _matcher = RelationMatcher::new("0 == 'Tp1'")?; /// @@ -159,7 +165,8 @@ impl RelationMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::{MatcherOptions, RelationMatcher}; + /// use pica_record::matcher::subfield::RelationMatcher; + /// use pica_record::matcher::MatcherOptions; /// use pica_record::primitives::SubfieldRef; /// /// let options = MatcherOptions::default(); @@ -209,7 +216,8 @@ impl RelationMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::{MatcherOptions, RelationMatcher}; + /// use pica_record::matcher::subfield::RelationMatcher; + /// use pica_record::matcher::MatcherOptions; /// use pica_record::primitives::SubfieldRef; /// /// let options = MatcherOptions::new().case_ignore(true); @@ -242,7 +250,8 @@ impl RelationMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::{MatcherOptions, RelationMatcher}; + /// use pica_record::matcher::subfield::RelationMatcher; + /// use pica_record::matcher::MatcherOptions; /// use pica_record::primitives::SubfieldRef; /// /// let options = MatcherOptions::new(); @@ -282,7 +291,8 @@ impl RelationMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::{MatcherOptions, RelationMatcher}; + /// use pica_record::matcher::subfield::RelationMatcher; + /// use pica_record::matcher::MatcherOptions; /// use pica_record::primitives::SubfieldRef; /// /// let options = MatcherOptions::new(); @@ -324,7 +334,8 @@ impl RelationMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::{MatcherOptions, RelationMatcher}; + /// use pica_record::matcher::subfield::RelationMatcher; + /// use pica_record::matcher::MatcherOptions; /// use pica_record::primitives::SubfieldRef; /// /// let subfield = SubfieldRef::new('a', "baz")?; @@ -361,7 +372,8 @@ impl RelationMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::{MatcherOptions, RelationMatcher}; + /// use pica_record::matcher::subfield::RelationMatcher; + /// use pica_record::matcher::MatcherOptions; /// use pica_record::primitives::SubfieldRef; /// /// let options = MatcherOptions::default(); @@ -393,7 +405,8 @@ impl Display for RelationMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::{MatcherOptions, RelationMatcher}; + /// use pica_record::matcher::subfield::RelationMatcher; + /// use pica_record::matcher::MatcherOptions; /// use pica_record::primitives::SubfieldRef; /// /// let matcher = RelationMatcher::new("[a0-3] == 'foo'")?; @@ -428,7 +441,7 @@ impl RegexMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::RegexMatcher; + /// use pica_record::matcher::subfield::RegexMatcher; /// /// let _matcher = RegexMatcher::new("0 =~ '^Tp'")?; /// @@ -448,7 +461,8 @@ impl RegexMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::{MatcherOptions, RegexMatcher}; + /// use pica_record::matcher::subfield::RegexMatcher; + /// use pica_record::matcher::MatcherOptions; /// use pica_record::primitives::SubfieldRef; /// /// let options = MatcherOptions::default(); @@ -496,7 +510,7 @@ impl Display for RegexMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::{MatcherOptions, RegexMatcher}; + /// use pica_record::matcher::subfield::RegexMatcher; /// /// let matcher = RegexMatcher::new("ALL [ab] =~ '^f.*o$'")?; /// assert_eq!(matcher.to_string(), "ALL [ab] =~ '^f.*o$'"); @@ -530,7 +544,7 @@ impl RegexSetMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::RegexMatcher; + /// use pica_record::matcher::subfield::RegexMatcher; /// /// let _matcher = RegexMatcher::new("0 =~ '^Tp'")?; /// @@ -552,7 +566,8 @@ impl RegexSetMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::{MatcherOptions, RegexSetMatcher}; + /// use pica_record::matcher::subfield::RegexSetMatcher; + /// use pica_record::matcher::MatcherOptions; /// use pica_record::primitives::SubfieldRef; /// /// let options = MatcherOptions::default(); @@ -600,7 +615,8 @@ impl Display for RegexSetMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::{MatcherOptions, RegexSetMatcher}; + /// use pica_record::matcher::subfield::RegexSetMatcher; + /// use pica_record::matcher::MatcherOptions; /// /// let matcher = /// RegexSetMatcher::new("ANY [ab] !~ ['^f.*o$', 'bar']")?; @@ -635,7 +651,7 @@ impl InMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::InMatcher; + /// use pica_record::matcher::subfield::InMatcher; /// /// let _matcher = InMatcher::new("0 in ['Tp1', 'Tpz']")?; /// @@ -653,7 +669,8 @@ impl InMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::{InMatcher, MatcherOptions}; + /// use pica_record::matcher::subfield::InMatcher; + /// use pica_record::matcher::MatcherOptions; /// use pica_record::primitives::SubfieldRef; /// /// let options = MatcherOptions::default(); @@ -706,7 +723,8 @@ impl Display for InMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::{InMatcher, MatcherOptions}; + /// use pica_record::matcher::subfield::InMatcher; + /// use pica_record::matcher::MatcherOptions; /// /// let matcher = InMatcher::new("ANY [ab] in ['foo', 'bar']")?; /// assert_eq!(matcher.to_string(), "ANY [ab] in ['foo', 'bar']"); @@ -739,7 +757,7 @@ impl CardinalityMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::CardinalityMatcher; + /// use pica_record::matcher::subfield::CardinalityMatcher; /// /// let _matcher = CardinalityMatcher::new("#a > 5")?; /// @@ -762,7 +780,8 @@ impl CardinalityMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::{CardinalityMatcher, MatcherOptions}; + /// use pica_record::matcher::subfield::CardinalityMatcher; + /// use pica_record::matcher::MatcherOptions; /// use pica_record::primitives::SubfieldRef; /// /// let options = MatcherOptions::default(); @@ -811,7 +830,8 @@ impl Display for CardinalityMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::{CardinalityMatcher, MatcherOptions}; + /// use pica_record::matcher::subfield::CardinalityMatcher; + /// use pica_record::matcher::MatcherOptions; /// /// let matcher = CardinalityMatcher::new("#a >= 3")?; /// assert_eq!(matcher.to_string(), "#a >= 3"); @@ -849,7 +869,7 @@ impl SingletonMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::CardinalityMatcher; + /// use pica_record::matcher::subfield::CardinalityMatcher; /// /// let _matcher = CardinalityMatcher::new("#a > 5")?; /// @@ -870,7 +890,8 @@ impl SingletonMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::{MatcherOptions, SingletonMatcher}; + /// use pica_record::matcher::subfield::SingletonMatcher; + /// use pica_record::matcher::MatcherOptions; /// use pica_record::primitives::SubfieldRef; /// /// let options = MatcherOptions::default(); @@ -907,7 +928,7 @@ impl Display for SingletonMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::{MatcherOptions, SingletonMatcher}; + /// use pica_record::matcher::subfield::SingletonMatcher; /// /// let matcher = SingletonMatcher::new("#a >= 3")?; /// assert_eq!(matcher.to_string(), "#a >= 3"); @@ -952,7 +973,7 @@ impl SubfieldMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::SubfieldMatcher; + /// use pica_record::matcher::subfield::SubfieldMatcher; /// /// let _matcher = SubfieldMatcher::new("a == 'foo'")?; /// @@ -973,7 +994,8 @@ impl SubfieldMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::{MatcherOptions, SubfieldMatcher}; + /// use pica_record::matcher::subfield::SubfieldMatcher; + /// use pica_record::matcher::MatcherOptions; /// use pica_record::primitives::SubfieldRef; /// /// let options = MatcherOptions::default(); @@ -1014,7 +1036,7 @@ impl Display for SubfieldMatcher { /// # Example /// /// ```rust - /// use pica_record::matcher::{MatcherOptions, SubfieldMatcher}; + /// use pica_record::matcher::subfield::SubfieldMatcher; /// /// let matcher = SubfieldMatcher::new("#a >= 3")?; /// assert_eq!(matcher.to_string(), "#a >= 3"); diff --git a/src/matcher/parse.rs b/src/matcher/subfield/parser.rs similarity index 73% rename from src/matcher/parse.rs rename to src/matcher/subfield/parser.rs index 14455809c..b0b4d3741 100644 --- a/src/matcher/parse.rs +++ b/src/matcher/subfield/parser.rs @@ -2,154 +2,39 @@ use std::cell::RefCell; use bstr::ByteSlice; use regex::bytes::Regex; -use smallvec::SmallVec; -use winnow::ascii::{digit1, multispace0, multispace1}; +use winnow::ascii::{digit1, multispace1}; use winnow::combinator::{ - alt, delimited, empty, opt, preceded, repeat, separated, - separated_pair, terminated, + alt, delimited, opt, preceded, repeat, separated, terminated, }; use winnow::error::ParserError; use winnow::prelude::*; -use winnow::stream::{AsChar, Stream, StreamIsPartial}; -use super::string::parse_string; -use super::subfield::{ - ExistsMatcher, RegexSetMatcher, SubfieldMatcher, -}; use super::{ - CardinalityMatcher, InMatcher, OccurrenceMatcher, Quantifier, - RegexMatcher, RelationMatcher, RelationalOp, SingletonMatcher, + CardinalityMatcher, ExistsMatcher, InMatcher, RegexMatcher, + RegexSetMatcher, RelationMatcher, SingletonMatcher, + SubfieldMatcher, }; -use crate::primitives::parse::{ - parse_occurrence_ref, parse_subfield_code, +use crate::matcher::operator::{ + parse_relational_operator, RelationalOp, }; -use crate::primitives::{Occurrence, SubfieldCode}; - -/// Strip whitespaces from the beginning and end. -pub(crate) fn ws, F>( - mut inner: F, -) -> impl Parser -where - I: Stream + StreamIsPartial, - ::Token: AsChar + Clone, - F: Parser, -{ - move |i: &mut I| { - let _ = multispace0.parse_next(i)?; - let o = inner.parse_next(i); - let _ = multispace0.parse_next(i)?; - o - } -} - -#[inline] -pub(crate) fn parse_subfield_code_range( - i: &mut &[u8], -) -> PResult> { - separated_pair(parse_subfield_code, b'-', parse_subfield_code) - .verify(|(min, max)| min < max) - .map(|(min, max)| { - (min.as_byte()..=max.as_byte()) - .map(SubfieldCode::from_unchecked) - .collect() - }) - .parse_next(i) -} - -#[inline] -fn parse_subfield_code_list( - i: &mut &[u8], -) -> PResult> { - delimited( - '[', - repeat( - 1.., - alt(( - parse_subfield_code_range, - parse_subfield_code.map(|code| vec![code]), - )), - ) - .fold(Vec::new, |mut acc: Vec<_>, item| { - acc.extend_from_slice(&item); - acc - }), - ']', - ) - .parse_next(i) -} +use crate::matcher::quantifier::parse_quantifier; +use crate::parser::{parse_string, parse_subfield_codes, ws}; +use crate::primitives::parse::parse_subfield_code; -#[inline] -fn parse_subfield_code_all( - i: &mut &[u8], -) -> PResult> { - const SUBFIELD_CODES: &[u8; 62] = b"0123456789\ - abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - - '*'.value( - SUBFIELD_CODES - .iter() - .map(|code| SubfieldCode::from_unchecked(*code)) - .collect(), - ) - .parse_next(i) -} - -/// Parse a list of subfield codes -#[allow(dead_code)] -pub(crate) fn parse_subfield_codes( - i: &mut &[u8], -) -> PResult> { - alt(( - parse_subfield_code_list, - parse_subfield_code.map(|code| vec![code]), - parse_subfield_code_all, - )) - .map(SmallVec::from_vec) - .parse_next(i) -} - -/// Parse the matcher expression from a byte slice. +/// Parses a [ExistsMatcher] expression. pub(crate) fn parse_exists_matcher( i: &mut &[u8], ) -> PResult { terminated(parse_subfield_codes, '?') .with_taken() - .map(|(codes, raw_data)| ExistsMatcher { - raw_data: raw_data.to_str().unwrap().to_string(), - codes, + .map(|(codes, raw_data)| { + let raw_data = raw_data.to_str().unwrap().to_string(); + ExistsMatcher { raw_data, codes } }) .parse_next(i) } -#[inline] -pub(crate) fn parse_quantifier(i: &mut &[u8]) -> PResult { - alt(("ALL".value(Quantifier::All), "ANY".value(Quantifier::Any))) - .parse_next(i) -} - -/// Parse RelationalOp which can be used for string comparisons. -#[inline] -pub(crate) fn parse_relational_operator( - i: &mut &[u8], -) -> PResult { - alt(( - "==".value(RelationalOp::Equal), - "!=".value(RelationalOp::NotEqual), - "=^".value(RelationalOp::StartsWith), - "!^".value(RelationalOp::StartsNotWith), - "=$".value(RelationalOp::EndsWith), - "!$".value(RelationalOp::EndsNotWith), - "=*".value(RelationalOp::Similar), - "=?".value(RelationalOp::Contains), - ">=".value(RelationalOp::GreaterThanOrEqual), - ">".value(RelationalOp::GreaterThan), - "<=".value(RelationalOp::LessThanOrEqual), - "<".value(RelationalOp::LessThan), - )) - .parse_next(i) -} - -/// Parse a relational expression +/// Parse a [RelationMatcher] expression. #[inline] pub(crate) fn parse_relation_matcher( i: &mut &[u8], @@ -178,6 +63,7 @@ pub(crate) fn parse_relation_matcher( .parse_next(i) } +/// Parse a [RegexMatcher] expression. pub(crate) fn parse_regex_matcher( i: &mut &[u8], ) -> PResult { @@ -203,6 +89,7 @@ pub(crate) fn parse_regex_matcher( .parse_next(i) } +/// Parse a [RegexSetMatcher] expression. pub(crate) fn parse_regex_set_matcher( i: &mut &[u8], ) -> PResult { @@ -225,7 +112,6 @@ pub(crate) fn parse_regex_set_matcher( .with_taken() .map(|((quantifier, codes, invert, re), raw_data)| { let raw_data = raw_data.to_str().unwrap().to_string(); - RegexSetMatcher { quantifier, codes, @@ -237,7 +123,7 @@ pub(crate) fn parse_regex_set_matcher( .parse_next(i) } -/// Parse a in matcher expression. +/// Parse a [InMatcher] expression. pub(crate) fn parse_in_matcher(i: &mut &[u8]) -> PResult { ( opt(ws(parse_quantifier)).map(Option::unwrap_or_default), @@ -281,7 +167,7 @@ pub(crate) fn parse_in_matcher(i: &mut &[u8]) -> PResult { .parse_next(i) } -/// Parse a cardinality matcher expression. +/// Parse a [CardinalityMatcher] expression. pub(crate) fn parse_cardinality_matcher( i: &mut &[u8], ) -> PResult { @@ -495,132 +381,17 @@ pub(crate) fn parse_subfield_matcher( .parse_next(i) } -#[inline] -fn parse_occurrence_matcher_inner( - i: &mut &[u8], -) -> PResult { - parse_occurrence_ref.map(Occurrence::from).parse_next(i) -} - -#[inline] -fn parse_occurrence_matcher_exact( - i: &mut &[u8], -) -> PResult { - parse_occurrence_matcher_inner - .verify(|occurrence| occurrence.as_bytes() != b"00") - .map(OccurrenceMatcher::Exact) - .parse_next(i) -} - -#[inline] -fn parse_occurrence_matcher_range( - i: &mut &[u8], -) -> PResult { - separated_pair( - parse_occurrence_matcher_inner, - '-', - parse_occurrence_matcher_inner, - ) - .verify(|(min, max)| min.len() == max.len() && min < max) - .map(|(min, max)| OccurrenceMatcher::Range(min, max)) - .parse_next(i) -} - -pub(crate) fn parse_occurrence_matcher( - i: &mut &[u8], -) -> PResult { - alt(( - preceded( - '/', - alt(( - parse_occurrence_matcher_range, - parse_occurrence_matcher_exact, - "00".value(OccurrenceMatcher::None), - "*".value(OccurrenceMatcher::Any), - )), - ), - empty.value(OccurrenceMatcher::None), - )) - .parse_next(i) -} - #[cfg(test)] mod tests { + use smallvec::SmallVec; + use super::*; - use crate::matcher::BooleanOp; + use crate::matcher::{BooleanOp, Quantifier}; + use crate::primitives::SubfieldCode; const SUBFIELD_CODES: &str = "0123456789\ abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - type TestResult = anyhow::Result<()>; - - #[test] - fn test_parse_subfield_code_range() { - macro_rules! parse_success { - ($input:expr, $expected:expr) => { - assert_eq!( - parse_subfield_code_range - .parse($input.as_bytes()) - .unwrap(), - $expected - .into_iter() - .map(SubfieldCode::from_unchecked) - .collect::>() - ); - }; - } - - parse_success!("a-b", ['a', 'b']); - parse_success!("a-c", ['a', 'b', 'c']); - parse_success!("a-z", ('a'..='z')); - parse_success!("0-9", ('0'..='9')); - parse_success!("A-Z", ('A'..='Z')); - - assert!(parse_subfield_code_range.parse(b"a-a").is_err()); - assert!(parse_subfield_code_range.parse(b"a-!").is_err()); - assert!(parse_subfield_code_range.parse(b"c-a").is_err()); - } - - #[test] - fn test_parse_subfield_code_list() { - macro_rules! parse_success { - ($input:expr, $expected:expr) => { - assert_eq!( - parse_subfield_code_list - .parse($input.as_bytes()) - .unwrap(), - $expected - .into_iter() - .map(SubfieldCode::from_unchecked) - .collect::>() - ); - }; - } - - parse_success!("[ab]", ['a', 'b']); - parse_success!("[abc]", ['a', 'b', 'c']); - parse_success!("[a-z]", ('a'..='z')); - parse_success!("[0-9]", ('0'..='9')); - parse_success!("[A-Z]", ('A'..='Z')); - parse_success!("[0a-cz]", ['0', 'a', 'b', 'c', 'z']); - - assert!(parse_subfield_code_range.parse(b"[ab!]").is_err()); - assert!(parse_subfield_code_range.parse(b"[a-a]").is_err()); - assert!(parse_subfield_code_range.parse(b"[a-!]").is_err()); - assert!(parse_subfield_code_range.parse(b"[c-a]").is_err()); - } - - #[test] - fn test_parse_subfield_code_all() { - assert_eq!( - parse_subfield_code_all.parse(b"*").unwrap(), - SUBFIELD_CODES - .chars() - .map(SubfieldCode::from_unchecked) - .collect::>() - ); - } - #[test] fn test_parse_exists_matcher() { macro_rules! parse_success { @@ -660,46 +431,6 @@ mod tests { assert!(parse_exists_matcher.parse(b"ANY a?").is_err()); } - #[test] - fn test_parse_quantifier() { - assert_eq!( - parse_quantifier.parse(b"ALL").unwrap(), - Quantifier::All - ); - - assert_eq!( - parse_quantifier.parse(b"ANY").unwrap(), - Quantifier::Any - ); - } - - #[test] - fn test_parse_relational_operator() { - macro_rules! parse_success { - ($input:expr, $expected:expr) => { - assert_eq!( - parse_relational_operator - .parse($input.as_bytes()) - .unwrap(), - $expected - ); - }; - } - - parse_success!("==", RelationalOp::Equal); - parse_success!("!=", RelationalOp::NotEqual); - parse_success!(">=", RelationalOp::GreaterThanOrEqual); - parse_success!(">", RelationalOp::GreaterThan); - parse_success!("<=", RelationalOp::LessThanOrEqual); - parse_success!("<", RelationalOp::LessThan); - parse_success!("=^", RelationalOp::StartsWith); - parse_success!("!^", RelationalOp::StartsNotWith); - parse_success!("=$", RelationalOp::EndsWith); - parse_success!("!$", RelationalOp::EndsNotWith); - parse_success!("=*", RelationalOp::Similar); - parse_success!("=?", RelationalOp::Contains); - } - #[test] fn test_parse_relation_matcher() { use Quantifier::*; @@ -1232,54 +963,4 @@ mod tests { a !~ '(?i)(Abdr|Sonderdr|Diss(\\\\.|ertation)|Teile)'" ); } - - #[test] - fn test_parse_occurrence_matcher() -> TestResult { - macro_rules! parse_success { - ($i:expr, $o:expr) => { - let o = parse_occurrence_matcher - .parse($i.as_bytes()) - .unwrap(); - - assert_eq!(o.to_string(), $i); - assert_eq!(o, $o); - }; - } - - parse_success!("", OccurrenceMatcher::None); - parse_success!("/*", OccurrenceMatcher::Any); - - parse_success!( - "/01", - OccurrenceMatcher::Exact(Occurrence::new("01")?) - ); - - parse_success!( - "/999", - OccurrenceMatcher::Exact(Occurrence::new("999")?) - ); - - parse_success!( - "/01-99", - OccurrenceMatcher::Range( - Occurrence::new("01")?, - Occurrence::new("99")? - ) - ); - - parse_success!( - "/01-99", - OccurrenceMatcher::Range( - Occurrence::new("01")?, - Occurrence::new("99")? - ) - ); - - assert_eq!( - parse_occurrence_matcher.parse(b"/00").unwrap(), - OccurrenceMatcher::None - ); - - Ok(()) - } } diff --git a/src/matcher/tag.rs b/src/matcher/tag.rs index 43ae7cce5..5a5b2641e 100644 --- a/src/matcher/tag.rs +++ b/src/matcher/tag.rs @@ -1,3 +1,5 @@ +//! Matcher that can be applied on a list of [TagRef]. + use std::fmt::{self, Display}; use bstr::ByteSlice; @@ -10,7 +12,7 @@ use super::ParseMatcherError; use crate::primitives::parse::parse_tag_ref; use crate::primitives::{Tag, TagRef}; -/// A matcher that matches against a TagRef. +/// A matcher that matches against a [TagRef]. #[derive(Debug, Clone, PartialEq)] pub enum TagMatcher { Tag(Tag), diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 000000000..35366ee4e --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,331 @@ +//! This module contains shared parsers. + +use smallvec::SmallVec; +use winnow::ascii::{multispace0, multispace1}; +use winnow::combinator::{ + alt, delimited, preceded, repeat, separated_pair, +}; +use winnow::error::{ContextError, ParserError}; +use winnow::prelude::*; +use winnow::stream::{AsChar, Compare, Stream, StreamIsPartial}; +use winnow::token::take_till; +use winnow::Parser; + +use crate::primitives::parse::parse_subfield_code; +use crate::primitives::SubfieldCode; + +#[inline] +pub(crate) fn parse_subfield_code_range( + i: &mut &[u8], +) -> PResult> { + separated_pair(parse_subfield_code, b'-', parse_subfield_code) + .verify(|(min, max)| min < max) + .map(|(min, max)| { + (min.as_byte()..=max.as_byte()) + .map(SubfieldCode::from_unchecked) + .collect() + }) + .parse_next(i) +} + +#[inline] +fn parse_subfield_code_list( + i: &mut &[u8], +) -> PResult> { + delimited( + '[', + repeat( + 1.., + alt(( + parse_subfield_code_range, + parse_subfield_code.map(|code| vec![code]), + )), + ) + .fold(Vec::new, |mut acc: Vec<_>, item| { + acc.extend_from_slice(&item); + acc + }), + ']', + ) + .parse_next(i) +} + +#[inline] +fn parse_subfield_code_all( + i: &mut &[u8], +) -> PResult> { + const SUBFIELD_CODES: &[u8; 62] = b"0123456789\ + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + '*'.value( + SUBFIELD_CODES + .iter() + .map(|code| SubfieldCode::from_unchecked(*code)) + .collect(), + ) + .parse_next(i) +} + +/// Parse a list of subfield codes +#[allow(dead_code)] +pub(crate) fn parse_subfield_codes( + i: &mut &[u8], +) -> PResult> { + alt(( + parse_subfield_code_list, + parse_subfield_code.map(|code| vec![code]), + parse_subfield_code_all, + )) + .map(SmallVec::from_vec) + .parse_next(i) +} + +/// Strip whitespaces from the beginning and end. +pub(crate) fn ws, F>( + mut inner: F, +) -> impl Parser +where + I: Stream + StreamIsPartial, + ::Token: AsChar + Clone, + F: Parser, +{ + move |i: &mut I| { + let _ = multispace0.parse_next(i)?; + let o = inner.parse_next(i); + let _ = multispace0.parse_next(i)?; + o + } +} + +#[derive(Debug, Copy, Clone)] +enum Quotes { + Single, + Double, +} + +fn parse_literal( + quotes: Quotes, +) -> impl Parser::Slice, E> +where + I: Stream + StreamIsPartial, + ::Token: AsChar, + E: ParserError, +{ + match quotes { + Quotes::Single => take_till(1.., ['\'', '\\']), + Quotes::Double => take_till(1.., ['"', '\\']), + } +} + +fn parse_escaped_char(quotes: Quotes) -> impl Parser +where + I: Stream + StreamIsPartial + Compare, + ::Token: AsChar + Clone, + E: ParserError, +{ + let v = match quotes { + Quotes::Single => '\'', + Quotes::Double => '"', + }; + + preceded( + '\\', + alt(( + 'n'.value('\n'), + 'r'.value('\r'), + 't'.value('\t'), + 'b'.value('\u{08}'), + 'f'.value('\u{0C}'), + '\\'.value('\\'), + '/'.value('/'), + v.value(v), + )), + ) +} + +#[derive(Debug, Clone)] +enum StringFragment<'a> { + Literal(&'a [u8]), + EscapedChar(char), + EscapedWs, +} + +fn parse_quoted_fragment<'a, E: ParserError<&'a [u8]>>( + quotes: Quotes, +) -> impl Parser<&'a [u8], StringFragment<'a>, E> { + use StringFragment::*; + + alt(( + parse_literal::<&'a [u8], E>(quotes).map(Literal), + parse_escaped_char::<&'a [u8], E>(quotes).map(EscapedChar), + preceded('\\', multispace1).value(EscapedWs), + )) +} + +fn parse_quoted_string<'a, E>( + quotes: Quotes, +) -> impl Parser<&'a [u8], Vec, E> +where + E: ParserError<&'a [u8]>, +{ + use StringFragment::*; + + let string_builder = repeat( + 0.., + parse_quoted_fragment::(quotes), + ) + .fold(Vec::new, |mut acc, fragment| { + match fragment { + Literal(s) => acc.extend_from_slice(s), + EscapedChar(c) => acc.push(c as u8), + EscapedWs => {} + } + acc + }); + + match quotes { + Quotes::Single => delimited('\'', string_builder, '\''), + Quotes::Double => delimited('"', string_builder, '"'), + } +} + +#[inline] +fn parse_string_single_quoted(i: &mut &[u8]) -> PResult> { + parse_quoted_string::(Quotes::Single).parse_next(i) +} + +#[inline] +fn parse_string_double_quoted(i: &mut &[u8]) -> PResult> { + parse_quoted_string::(Quotes::Double).parse_next(i) +} + +pub(crate) fn parse_string(i: &mut &[u8]) -> PResult> { + alt((parse_string_single_quoted, parse_string_double_quoted)) + .parse_next(i) +} + +#[cfg(test)] +mod tests { + use super::*; + + const SUBFIELD_CODES: &str = "0123456789\ + abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + #[test] + fn test_parse_subfield_code_range() { + macro_rules! parse_success { + ($input:expr, $expected:expr) => { + assert_eq!( + parse_subfield_code_range + .parse($input.as_bytes()) + .unwrap(), + $expected + .into_iter() + .map(SubfieldCode::from_unchecked) + .collect::>() + ); + }; + } + + parse_success!("a-b", ['a', 'b']); + parse_success!("a-c", ['a', 'b', 'c']); + parse_success!("a-z", ('a'..='z')); + parse_success!("0-9", ('0'..='9')); + parse_success!("A-Z", ('A'..='Z')); + + assert!(parse_subfield_code_range.parse(b"a-a").is_err()); + assert!(parse_subfield_code_range.parse(b"a-!").is_err()); + assert!(parse_subfield_code_range.parse(b"c-a").is_err()); + } + + #[test] + fn test_parse_subfield_code_list() { + macro_rules! parse_success { + ($input:expr, $expected:expr) => { + assert_eq!( + parse_subfield_code_list + .parse($input.as_bytes()) + .unwrap(), + $expected + .into_iter() + .map(SubfieldCode::from_unchecked) + .collect::>() + ); + }; + } + + parse_success!("[ab]", ['a', 'b']); + parse_success!("[abc]", ['a', 'b', 'c']); + parse_success!("[a-z]", ('a'..='z')); + parse_success!("[0-9]", ('0'..='9')); + parse_success!("[A-Z]", ('A'..='Z')); + parse_success!("[0a-cz]", ['0', 'a', 'b', 'c', 'z']); + + assert!(parse_subfield_code_range.parse(b"[ab!]").is_err()); + assert!(parse_subfield_code_range.parse(b"[a-a]").is_err()); + assert!(parse_subfield_code_range.parse(b"[a-!]").is_err()); + assert!(parse_subfield_code_range.parse(b"[c-a]").is_err()); + } + + #[test] + fn test_parse_subfield_code_all() { + assert_eq!( + parse_subfield_code_all.parse(b"*").unwrap(), + SUBFIELD_CODES + .chars() + .map(SubfieldCode::from_unchecked) + .collect::>() + ); + } + + #[test] + fn test_parse_string_single_quoted() { + macro_rules! parse_success { + ($input:expr, $output:expr) => { + assert_eq!( + parse_string_single_quoted + .parse($input.as_bytes()) + .unwrap(), + $output, + ); + }; + } + + parse_success!("'abc'", b"abc"); + parse_success!("'a\\nbc'", b"a\nbc"); + parse_success!("'a\\rbc'", b"a\rbc"); + parse_success!("'a\\tbc'", b"a\tbc"); + parse_success!("'a\\bbc'", b"a\x08bc"); + parse_success!("'a\\fbc'", b"a\x0Cbc"); + parse_success!("'a\\\\c'", b"a\\c"); + parse_success!("'a\\'c'", b"a\'c"); + parse_success!("'a\"c'", b"a\"c"); + parse_success!("'a\\/bc'", b"a/bc"); + parse_success!("'a\\ bc'", b"abc"); + } + + #[test] + fn test_parse_string_double_quoted() { + macro_rules! parse_success { + ($input:expr, $output:expr) => { + assert_eq!( + parse_string_double_quoted + .parse($input.as_bytes()) + .unwrap(), + $output, + ); + }; + } + + parse_success!("\"abc\"", b"abc"); + parse_success!("\"a\\nbc\"", b"a\nbc"); + parse_success!("\"a\\rbc\"", b"a\rbc"); + parse_success!("\"a\\tbc\"", b"a\tbc"); + parse_success!("\"a\\bbc\"", b"a\x08bc"); + parse_success!("\"a\\fbc\"", b"a\x0Cbc"); + parse_success!("\"a\\\\c\"", b"a\\c"); + parse_success!("\"a'c\"", b"a'c"); + parse_success!("\"a\\/bc\"", b"a/bc"); + parse_success!("\"a\\ bc\"", b"abc"); + } +} diff --git a/src/prelude.rs b/src/prelude.rs index fd798e1f5..906eed548 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -14,5 +14,8 @@ //! # Ok::<(), Box>(()) //! ``` -pub use crate::matcher::{MatcherOptions, SubfieldMatcher}; +pub use crate::matcher::subfield::SubfieldMatcher; +pub use crate::matcher::{ + MatcherOptions, OccurrenceMatcher, TagMatcher, +}; pub use crate::{ByteRecord, Error, StringRecord};