neotoma 0.1.1

A flexible, cached parser combinator framework for Rust.
Documentation
//! Optional parser combinator for zero-or-one matching.
//!
//! This module provides the [`Optional`] parser combinator that wraps another parser
//! to make it succeed even when the wrapped parser fails. The result is wrapped in
//! an `Option<T>` where `Some(result)` indicates success and `None` indicates the
//! wrapped parser failed with `NoMatch`.
//!
//! Optional parsers are essential for building flexible grammars where certain
//! elements may or may not be present in the input.

use crate::{
    cache::ParsingCache,
    parser::{Parsable, Parser, Source},
    result::{Error, ParseResult},
};

/// A parser combinator that optionally matches another parser.
///
/// Optional applies a contained parser exactly once, returning `Some(result)` if it succeeds
/// or `None` if it fails with `NoMatch`. The Optional parser itself always succeeds,
/// making it useful for optional elements in a grammar.
///
/// # Examples
///
/// ```rust
/// use neotoma::{optional::Optional, literal::Literal, parser::{parse, Source}};
/// use std::io::Cursor;
///
/// // Optional "http://" prefix
/// let http_prefix = Optional::new(Literal::from_str("http://"));
///
/// // Matches: "http://example.com" -> Some("http://")
/// let mut input1 = Cursor::new(b"http://example.com");
/// let mut source1 = Source::new(input1);
/// let result1 = parse(http_prefix, &mut source1).unwrap();
/// assert_eq!(result1, Some(b"http://".as_slice().into()));
///
/// // Matches: "example.com" -> None
/// let http_prefix2 = Optional::new(Literal::from_str("http://"));
/// let mut input2 = Cursor::new(b"example.com");
/// let mut source2 = Source::new(input2);
/// let result2 = parse(http_prefix2, &mut source2).unwrap();
/// assert_eq!(result2, None);
///
/// // Optional sign for numbers
/// let sign = Optional::new(Literal::from_str("-"));
///
/// // Matches: "-123" -> Some("-")
/// let mut input3 = Cursor::new(b"-123");
/// let mut source3 = Source::new(input3);
/// let result3 = parse(sign, &mut source3).unwrap();
/// assert_eq!(result3, Some(b"-".as_slice().into()));
///
/// // Matches: "123" -> None
/// let sign2 = Optional::new(Literal::from_str("-"));
/// let mut input4 = Cursor::new(b"123");
/// let mut source4 = Source::new(input4);
/// let result4 = parse(sign2, &mut source4).unwrap();
/// assert_eq!(result4, None);
/// ```
#[derive(Clone)]
pub struct Optional<P> {
    parser: P,
}

impl<P> Optional<P> {
    /// Create a new Optional parser that wraps another parser.
    ///
    /// The wrapped parser will be tried once. If it succeeds, `Some(result)` is returned.
    /// If it fails with `NoMatch`, `None` is returned. Other errors are propagated.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use neotoma::{optional::Optional, literal::Literal, parser::{parse, Source}};
    /// use std::io::Cursor;
    ///
    /// let optional_comma = Optional::new(Literal::from_str(","));
    ///
    /// // Matches "," -> Some(",")
    /// let mut input1 = Cursor::new(b",hello");
    /// let mut source1 = Source::new(input1);
    /// let result1 = parse(optional_comma, &mut source1).unwrap();
    /// assert_eq!(result1, Some(b",".as_slice().into()));
    ///
    /// // Matches "abc" -> None (doesn't consume any input)
    /// let optional_comma2 = Optional::new(Literal::from_str(","));
    /// let mut input2 = Cursor::new(b"abc");
    /// let mut source2 = Source::new(input2);
    /// let result2 = parse(optional_comma2, &mut source2).unwrap();
    /// assert_eq!(result2, None);
    /// ```
    pub fn new(parser: P) -> Self {
        Self { parser }
    }
}

impl<P, Ctx> Parser<Ctx> for Optional<P>
where
    P: Parser<Ctx>,
{
    type Output = Option<P::Output>;

    fn id(&self) -> u64 {
        use std::any::TypeId;
        use std::hash::{DefaultHasher, Hash, Hasher};

        let mut hasher = DefaultHasher::new();
        TypeId::of::<Self>().hash(&mut hasher);
        self.parser.id().hash(&mut hasher);
        hasher.finish()
    }

    fn read<S>(
        &self,
        source: &mut Source<S>,
        cache: &mut impl ParsingCache,
        context: &mut Ctx,
    ) -> ParseResult<Self::Output>
    where
        S: Parsable,
    {
        match self.parser.parse(source, cache, context) {
            Ok(result) => Ok(Some(result)),
            Err(Error::NoMatch) => Ok(None),
            Err(err) => Err(err),
        }
    }
}

// Convenience From trait for easier construction
impl<P> From<P> for Optional<P>
where
    P: Parser,
{
    fn from(parser: P) -> Self {
        Self::new(parser)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{literal::Literal, parser::parse};
    use std::io::Cursor;

    #[test]
    fn test_id_implementation_different_optional_parsers() {
        // Test that Optional implements proper id() method
        // Different Optional parsers should have different IDs to avoid cache conflicts

        let optional1 = Optional::new(Literal::from_str("hello"));
        let optional2 = Optional::new(Literal::from_str("world"));

        // These should have different IDs because they have different inner parsers
        // This test will FAIL if Optional uses default id() implementation
        assert_ne!(
            <Optional<_> as crate::parser::Parser<()>>::id(&optional1),
            <Optional<_> as crate::parser::Parser<()>>::id(&optional2),
            "Different Optional instances should have different IDs to avoid cache collisions"
        );
    }

    #[test]
    fn test_id_implementation_same_optional_parsers() {
        // Test that identical Optional parsers have the same ID
        let optional1 = Optional::new(Literal::from_str("hello"));
        let optional2 = Optional::new(Literal::from_str("hello"));

        assert_eq!(
            <Optional<_> as crate::parser::Parser<()>>::id(&optional1),
            <Optional<_> as crate::parser::Parser<()>>::id(&optional2),
            "Identical Optional instances should have the same ID for cache efficiency"
        );
    }

    #[test]
    fn test_optional_success() {
        let literal = Literal::from_str("hello");
        let optional = Optional::new(literal);

        let mut input = Cursor::new(b"hello world");
        let mut source = crate::parser::Source::new(&mut input);

        let result = parse(optional, &mut source).unwrap();
        assert_eq!(result, Some(b"hello".as_slice().into()));
    }

    #[test]
    fn test_optional_none() {
        let literal = Literal::from_str("hello");
        let optional = Optional::new(literal);

        let mut input = Cursor::new(b"world");
        let mut source = crate::parser::Source::new(&mut input);

        let result = parse(optional, &mut source).unwrap();
        assert_eq!(result, None);
    }

    #[test]
    fn test_optional_empty_input() {
        let literal = Literal::from_str("hello");
        let optional = Optional::new(literal);

        let mut input = Cursor::new(b"");
        let mut source = crate::parser::Source::new(&mut input);

        let result = parse(optional, &mut source).unwrap();
        assert_eq!(result, None);
    }

    #[test]
    fn test_optional_from_trait() {
        let literal = Literal::from_str("test");
        let optional: Optional<_> = literal.into();

        let mut input = Cursor::new(b"test");
        let mut source = crate::parser::Source::new(&mut input);

        let result = parse(optional, &mut source).unwrap();
        assert_eq!(result, Some(b"test".as_slice().into()));
    }
}