diff --git a/src/parse.rs b/src/parse.rs index 9f938be..b3a95c6 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -5,7 +5,9 @@ use crate::commands::Command; use self::line::parse_lines; +mod error; mod line; +mod parser; pub fn parse(file: &Path) -> anyhow::Result> { let content = fs::read_to_string(file)?; diff --git a/src/parse/error.rs b/src/parse/error.rs new file mode 100644 index 0000000..208cf31 --- /dev/null +++ b/src/parse/error.rs @@ -0,0 +1,32 @@ +use std::error; + +#[derive(Debug, thiserror::Error)] +#[error("line {line}: {reason}")] +pub struct ParseError { + line: usize, + reason: Box, +} + +impl ParseError { + #[must_use] + pub fn new(line: usize, reason: impl error::Error + 'static) -> Self { + Self { + line, + reason: Box::new(reason), + } + } + + #[must_use] + pub fn pack(line: usize, reason: impl error::Error + 'static) -> Result { + Err(Self::new(line, reason)) + } +} + +pub trait ToParseError: error::Error + 'static + Sized { + #[must_use] + fn at(self, line: usize) -> ParseError { + ParseError::new(line, self) + } +} + +impl ToParseError for E {} diff --git a/src/parse/line.rs b/src/parse/line.rs index 12f3bea..f855afe 100644 --- a/src/parse/line.rs +++ b/src/parse/line.rs @@ -4,6 +4,8 @@ use chrono::NaiveDate; use crate::commands::{BirthdaySpec, Done, Spec}; +use super::error::ParseError; + #[derive(Debug)] pub enum Line { Empty, @@ -21,14 +23,16 @@ pub enum Line { } #[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("line {line}: unknown command {name:?}")] - UnknownCommand { line: usize, name: String }, - #[error("line {line}: unknown format")] - UnknownFormat { line: usize }, +pub enum Reason { + #[error("unknown format")] + UnknownFormat, + #[error("unknown command {0:?}")] + UnknownCommand(String), + #[error("empty command body")] + EmptyCommand, } -type Result = result::Result; +type Result = result::Result; pub fn parse_lines(content: &str) -> Result> { content @@ -46,6 +50,10 @@ fn parse_line(line: usize, content: &str) -> Result { } else if content.starts_with('\t') || content.starts_with(' ') { Ok(Line::Indented(content.to_string())) } else if let Some((name, rest)) = parse_command(content) { + let rest = rest.trim(); + if rest.is_empty() { + return ParseError::pack(line, Reason::EmptyCommand); + } match name { "TASK" => Ok(Line::Task(rest.to_string())), "NOTE" => Ok(Line::Note(rest.to_string())), @@ -56,13 +64,10 @@ fn parse_line(line: usize, content: &str) -> Result { "UNTIL" => parse_datum(rest).map(Line::Until), "EXCEPT" => parse_datum(rest).map(Line::Except), "DONE" => parse_done(rest), - _ => Err(Error::UnknownCommand { - line, - name: name.to_string(), - }), + _ => ParseError::pack(line, Reason::UnknownCommand(name.to_string())), } } else { - Err(Error::UnknownFormat { line }) + ParseError::pack(line, Reason::UnknownFormat) } } diff --git a/src/parse/parser.rs b/src/parse/parser.rs new file mode 100644 index 0000000..03550d0 --- /dev/null +++ b/src/parse/parser.rs @@ -0,0 +1,88 @@ +pub struct Parser<'d> { + data: &'d str, + index: usize, +} + +#[derive(Debug, thiserror::Error)] +pub enum Reason { + #[error("expected character {expected:?} at {rest:?}")] + ExpectedChar { expected: char, rest: String }, + #[error("expected string {expected:?} at {rest:?}")] + ExpectedStr { expected: String, rest: String }, + #[error("expected whitespace at {rest:?}")] + ExpectedWhitespace { rest: String }, +} + +impl<'d> Parser<'d> { + pub fn new(data: &'d str) -> Self { + Self { data, index: 0 } + } + + fn rest(&self) -> &'d str { + &self.data[self.index..] + } + + pub fn peek(&self) -> Option { + self.rest().chars().next() + } + + pub fn take(&mut self) -> Option { + if let Some(c) = self.peek() { + self.index += c.len_utf8(); + Some(c) + } else { + None + } + } + + pub fn take_exact(&mut self, c: char) -> Result<(), Reason> { + if self.peek() == Some(c) { + self.take(); + Ok(()) + } else { + Err(Reason::ExpectedChar { + expected: c, + rest: self.rest().to_string(), + }) + } + } + + pub fn take_any_whitespace(&mut self) { + while let Some(c) = self.peek() { + if c.is_whitespace() { + self.take(); + } else { + break; + } + } + } + + pub fn take_some_whitespace(&mut self) -> Result<(), Reason> { + match self.peek() { + Some(c) if c.is_whitespace() => { + self.take(); + self.take_any_whitespace(); + Ok(()) + } + _ => Err(Reason::ExpectedWhitespace { + rest: self.rest().to_string(), + }), + } + } + + pub fn starts_with(&self, pattern: &str) -> bool { + self.data.starts_with(pattern) + } + + pub fn take_starting_with(&mut self, pattern: &str) -> Result<(), Reason> { + if self.starts_with(pattern) { + self.index += pattern.len(); + Ok(()) + } else { + Err(Reason::ExpectedStr { + expected: pattern.to_string(), + rest: self.rest().to_string(), + }) + } + } +}