eof.rs
raw
//! 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)
);
}
}