neotoma 0.1.1

A flexible, cached parser combinator framework for Rust.
Documentation
//! End-of-file detection for complete input consumption.
//!
//! This module provides the [`EndOfFile`] parser that succeeds only when there
//! are no more bytes to read from the input source. This is essential for
//! ensuring that parsers have consumed the entire input, not just a prefix.
//!
//! The EndOfFile parser is commonly used as the final parser in a sequence
//! to validate that parsing was complete and no unexpected trailing content
//! remains in the input stream.

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

/// A parser that succeeds only at the end of input.
///
/// EndOfFile succeeds when there are no more bytes to read from the input source,
/// making it useful for ensuring complete consumption of input. It returns unit
/// on success and fails with `NoMatch` if there is still input remaining.
///
/// # Examples
///
/// ```rust
/// use neotoma::{eof::EndOfFile, literal::Literal, seq, parser::{parse, Source}};
/// use std::io::Cursor;
///
/// // Parser that matches "hello" followed by end of file
/// let hello_eof = seq![Literal::from_str("hello"), EndOfFile::new()];
///
/// // Succeeds: "hello" (complete match)
/// let mut input1 = Cursor::new(b"hello");
/// let mut source1 = Source::new(input1);
/// let result1 = parse(hello_eof.clone(), &mut source1);
/// assert!(result1.is_ok());
///
/// // Fails: "hello world" (has trailing content)
/// let mut input2 = Cursor::new(b"hello world");
/// let mut source2 = Source::new(input2);
/// let result2 = parse(hello_eof, &mut source2);
/// assert!(result2.is_err());
/// ```
///
/// # Use Cases
///
/// - Ensuring complete parsing of input files
/// - Validating that expressions don't have trailing garbage
/// - Creating strict parsers for configuration files
/// - Testing parser completeness in unit tests
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EndOfFile;

impl EndOfFile {
    /// Create a new EndOfFile parser.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use neotoma::{eof::EndOfFile, parser::{parse, Source}};
    /// use std::io::Cursor;
    ///
    /// let eof = EndOfFile::new();
    ///
    /// // Succeeds on empty input
    /// let mut input1 = Cursor::new(b"");
    /// let mut source1 = Source::new(input1);
    /// let result1 = parse(eof.clone(), &mut source1);
    /// assert!(result1.is_ok());
    ///
    /// // Fails on non-empty input
    /// let mut input2 = Cursor::new(b"a");
    /// let mut source2 = Source::new(input2);
    /// let result2 = parse(eof, &mut source2);
    /// assert!(result2.is_err());
    /// ```
    pub fn new() -> Self {
        Self
    }
}

impl Default for EndOfFile {
    fn default() -> Self {
        Self::new()
    }
}

impl<Ctx> Parser<Ctx> for EndOfFile {
    type Output = ();

    fn read<S>(
        &self,
        source: &mut Source<S>,
        _cache: &mut impl ParsingCache,
        _context: &mut Ctx,
    ) -> ParseResult<Self::Output>
    where
        S: Parsable,
    {
        // Try to peek at the next byte - if we get EOF, that's success
        match source.peek1() {
            Err(Error::NoMatch) => Ok(()),        // End of file reached - success!
            Ok(_) => Err(Error::NoMatch),         // Still have input - fail
            Err(other_error) => Err(other_error), // Propagate other I/O errors
        }
    }
}

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

    #[test]
    fn test_eof_on_empty_input() {
        let eof_parser = EndOfFile::new();
        let mut input = Cursor::new(b"");
        let mut source = Source::new(&mut input);

        let result = parse(eof_parser, &mut source);
        assert!(result.is_ok());
    }

    #[test]
    fn test_eof_fails_on_non_empty_input() {
        let eof_parser = EndOfFile::new();
        let mut input = Cursor::new(b"a");
        let mut source = Source::new(&mut input);

        let result = parse(eof_parser, &mut source);
        assert!(result.is_err());
        assert!(matches!(result, Err(Error::NoMatch)));
    }

    #[test]
    fn test_eof_fails_on_whitespace() {
        let eof_parser = EndOfFile::new();
        let mut input = Cursor::new(b" ");
        let mut source = Source::new(&mut input);

        let result = parse(eof_parser, &mut source);
        assert!(result.is_err());
        assert!(matches!(result, Err(Error::NoMatch)));
    }

    #[test]
    fn test_eof_with_sequence_success() {
        let parser = seq![Literal::from_str("hello"), EndOfFile::new()];
        let mut input = Cursor::new(b"hello");
        let mut source = Source::new(&mut input);

        let result = parse(parser, &mut source);
        assert!(result.is_ok());
    }

    #[test]
    fn test_eof_with_sequence_failure_trailing_content() {
        let parser = seq![Literal::from_str("hello"), EndOfFile::new()];
        let mut input = Cursor::new(b"hello world");
        let mut source = Source::new(&mut input);

        let result = parse(parser, &mut source);
        assert!(result.is_err());
        assert!(matches!(result, Err(Error::NoMatch)));
    }

    #[test]
    fn test_eof_with_sequence_failure_incomplete() {
        let parser = seq![Literal::from_str("hello"), EndOfFile::new()];
        let mut input = Cursor::new(b"hel");
        let mut source = Source::new(&mut input);

        let result = parse(parser, &mut source);
        assert!(result.is_err());
    }

    #[test]
    fn test_eof_id_consistency() {
        let eof1 = EndOfFile::new();
        let eof2 = EndOfFile::new();

        // Same parser type should have same ID
        assert_eq!(
            <EndOfFile as Parser<()>>::id(&eof1),
            <EndOfFile as Parser<()>>::id(&eof2)
        );
    }

    #[test]
    fn test_eof_default() {
        let eof_default = EndOfFile;
        let eof_new = EndOfFile::new();

        assert_eq!(
            <EndOfFile as Parser<()>>::id(&eof_default),
            <EndOfFile as Parser<()>>::id(&eof_new)
        );
    }
}