diff --git a/src/archive/fetcher.rs b/src/archive/fetcher.rs index 4a3aff0..db7c1b1 100644 --- a/src/archive/fetcher.rs +++ b/src/archive/fetcher.rs @@ -1,6 +1,7 @@ //! Archive fetcher. use std::fs::File; +use std::io::ErrorKind as IoErrorKind; use std::io::{BufReader, Read, Seek}; use std::path::Path; use std::sync::Mutex; @@ -8,9 +9,9 @@ use std::sync::Mutex; use zip::read::ZipArchive; use zip::result::ZipError; -use crate::error::{Error, Result}; use super::parser::parse; use super::story::Story; +use crate::error::{Error, Result}; pub struct Fetcher where @@ -22,10 +23,11 @@ where impl Fetcher> { pub fn from(path: impl AsRef) -> Result { - use Error::*; + use IoErrorKind::*; - let file = File::open(path).map_err(|e| match e { - _ => SourceError("Could not open archive file."), + let file = File::open(path).map_err(|e| match e.kind() { + NotFound => Error::archive("File not found"), + _ => Error::archive("Could not open file"), })?; Self::from_reader(BufReader::with_capacity(8_000_000, file)) @@ -45,26 +47,24 @@ where } fn open(archive: T) -> Result> { - use Error::*; use ZipError::*; ZipArchive::new(archive).map_err(|e| match e { - InvalidArchive(e) => SourceError(e), - UnsupportedArchive(e) => SourceError(e), - _ => SourceError("Could not read archive."), + InvalidArchive(e) => Error::archive(e), + UnsupportedArchive(e) => Error::archive(e), + _ => Error::archive("Unknown ZIP-file issue"), }) } fn load(archive: &mut ZipArchive) -> Result> { - use Error::*; use ZipError::*; let file = archive.by_name("index.json").map_err(|e| match e { - FileNotFound => SourceError("Missing archive index."), - _ => SourceError("Could not open archive index."), + FileNotFound => Error::archive("Missing story index"), + _ => Error::archive("Could not open story index"), })?; - parse(BufReader::with_capacity(8_000_000, file)) + parse(BufReader::with_capacity(8_000_000, file)).map_err(Error::index) } pub fn fetch(&self, key: i64) -> Option<&Story> { @@ -75,23 +75,23 @@ where } pub fn read(&self, path: &str) -> Result> { - use Error::*; use ZipError::*; let mut archive = self.archive.lock().map_err(|e| match e { - _ => SourceError("Could not acquire archive lock."), + _ => Error::archive("Could not acquire fetcher lock"), })?; let mut file = archive.by_name(path).map_err(|e| match e { - FileNotFound => SourceError("File not found."), - _ => SourceError("Could not open file."), + FileNotFound => Error::archive("Missing story data"), + _ => Error::archive("Could not open story data"), })?; let size = file.size() as usize; let mut buf = Vec::with_capacity(size); - file.read_to_end(&mut buf) - .map_err(|_| SourceError("Could not read file."))?; + file.read_to_end(&mut buf).map_err(|e| match e { + _ => Error::archive("Could not read story data"), + })?; Ok(buf) } diff --git a/src/archive/parser.rs b/src/archive/parser.rs index 162ee21..088073d 100644 --- a/src/archive/parser.rs +++ b/src/archive/parser.rs @@ -4,22 +4,21 @@ use std::io::BufRead; use std::sync::mpsc::{channel, Receiver}; use std::thread::spawn; +use serde::de::Error; +use serde_json::error::Result; use serde_json::from_str; -use crate::error::{Error, Result}; use super::story::Story; const TRIM: &[char] = &['"', ',', ' ', '\t', '\n', '\r']; pub fn parse(reader: impl BufRead) -> Result> { - use Error::*; - let (tx, rx) = channel(); let rx = spawn_parser(rx); for line in reader.lines() { let line = line.map_err(|e| match e { - _ => SourceError("Could not read index line."), + _ => Error::custom("Could not read line"), })?; if tx.send(line).is_ok() { @@ -27,8 +26,8 @@ pub fn parse(reader: impl BufRead) -> Result> { } return Err(match rx.recv() { - Err(_) => SourceError("Parser disappeared unexpectedly."), - Ok(Ok(_)) => SourceError("Parser returned unexpectedly."), + Err(_) => Error::custom("Parser disappeared unexpectedly"), + Ok(Ok(_)) => Error::custom("Parser returned unexpectedly"), Ok(Err(error)) => error, }); } @@ -36,13 +35,11 @@ pub fn parse(reader: impl BufRead) -> Result> { drop(tx); rx.recv().map_err(|e| match e { - _ => SourceError("Missing parser result."), + _ => Error::custom("Missing parser result"), })? } fn spawn_parser(stream: Receiver) -> Receiver>> { - use Error::*; - let (tx, rx) = channel(); spawn(move || { @@ -68,11 +65,11 @@ fn spawn_parser(stream: Receiver) -> Receiver>> { stories.shrink_to_fit(); if wrappers != "{}" { - return tx.send(Err(SourceError("Invalid index structure."))); + return tx.send(Err(Error::custom("Invalid file structure"))); } if count != stories.len() { - return tx.send(Err(SourceError("Index contains duplicates."))); + return tx.send(Err(Error::custom("Found duplicate story"))); } tx.send(Ok(stories)) @@ -82,8 +79,6 @@ fn spawn_parser(stream: Receiver) -> Receiver>> { } fn deserialize(line: String) -> Result { - use Error::*; - let split = line .splitn(2, ':') .map(|value| value.trim_matches(TRIM)) @@ -91,19 +86,17 @@ fn deserialize(line: String) -> Result { let (skey, json) = match split[..] { [skey, json] => Ok((skey, json)), - _ => Err(SourceError("Invalid line format.")), + _ => Err(Error::custom("Invalid line format")), }?; - let key: i64 = skey.parse().map_err(|e| match e { - _ => SourceError("Invalid meta key."), - })?; + let story: Story = from_str(json)?; - let story: Story = from_str(json).map_err(|e| match e { - _ => SourceError("Invalid meta value."), + let key: i64 = skey.parse().map_err(|e| match e { + _ => Error::custom("Invalid line key"), })?; if key != story.id { - return Err(SourceError("Meta key mismatch.")); + return Err(Error::custom("Line key mismatch")); } Ok(story) diff --git a/src/archive/story.rs b/src/archive/story.rs index 2eaf17d..1d4a13c 100644 --- a/src/archive/story.rs +++ b/src/archive/story.rs @@ -182,10 +182,10 @@ where Ok(value.as_i64().unwrap()) } else if value.is_string() { value.as_str().unwrap().parse().map_err(|e| match e { - _ => Error::custom("Could not parse ID string."), + _ => Error::custom("Could not parse ID string"), }) } else { - Err(Error::custom("Invalid type for ID value.")) + Err(Error::custom("Invalid type for ID value")) } } @@ -199,15 +199,15 @@ impl<'de> Deserialize<'de> for Color { let text = object .get("hex") .and_then(|value| value.as_str()) - .ok_or_else(|| Error::custom("Color is missing hex value."))?; + .ok_or_else(|| Error::custom("Color is missing hex value"))?; let array = hex::decode(text).map_err(|e| match e { - _ => Error::custom("Color hex has invalid value."), + _ => Error::custom("Color hex has invalid value"), })?; match array[..] { [red, green, blue] => Ok(Color { red, green, blue }), - _ => Err(Error::custom("Color hex has invalid length.")), + _ => Err(Error::custom("Color hex has invalid length")), } } } diff --git a/src/error.rs b/src/error.rs index 753c596..3102d5e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,10 +1,126 @@ //! Error types. -#[derive(Debug)] -pub enum Error { - InvalidStory(), - SourceError(&'static str), - UserError(&'static str), +use std::error::Error as StdError; +use std::fmt::Result as FmtResult; +use std::fmt::{Display, Formatter}; +use std::result::Result as StdResult; + +use serde_json::error::Error as SerdeError; + +use self::ErrorKind::*; + +pub type Result = StdResult; + +#[derive(Clone, Debug)] +pub enum ErrorKind { + ArchiveError, + IndexError, + InvalidStory, + UsageError, } -pub type Result = std::result::Result; +#[derive(Debug)] +pub struct Error { + kind: ErrorKind, + message: Option, + source: Option>, +} + +pub struct ErrorBuilder(Error); + +impl ErrorBuilder { + pub fn new(kind: ErrorKind) -> Self { + ErrorBuilder(Error { + kind, + message: None, + source: None, + }) + } + + pub fn message(mut self, message: impl ToString) -> Self { + self.0.message = Some(message.to_string()); + self + } + + pub fn source(mut self, source: impl StdError + 'static) -> Self { + self.0.source = Some(Box::new(source)); + self + } + + pub fn build(self) -> Error { + self.0 + } +} + +impl Error { + pub fn archive(message: impl ToString) -> Self { + ErrorBuilder::new(ArchiveError).message(message).build() + } + + pub fn index(error: SerdeError) -> Self { + ErrorBuilder::new(IndexError) + .message(&error) + .source(error) + .build() + } + + pub fn invalid() -> Self { + ErrorBuilder::new(InvalidStory).build() + } + + pub fn usage(message: impl ToString) -> Self { + ErrorBuilder::new(UsageError).message(message).build() + } + + pub fn kind(&self) -> ErrorKind { + self.kind.clone() + } + + pub fn message(&self) -> Option<&String> { + self.message.as_ref() + } +} + +fn lower(message: &str) -> String { + let mut chars = message.chars(); + + let head: String = match chars.next() { + Some(c) => c.to_lowercase().collect(), + None => return String::from(message), + }; + + format!("{}{}", head, chars.as_str()) +} + +impl Display for ErrorKind { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + let message = match self { + ArchiveError => "Archive error", + IndexError => "Index error", + InvalidStory => "Invalid story", + UsageError => "Usage error", + }; + + write!(f, "{}", message) + } +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + let message = match &self.message { + Some(message) => message, + None => "Unknown cause", + }; + + let kind = self.kind(); + let info = lower(message); + + write!(f, "{}, {}.", kind, info) + } +} + +impl StdError for Error { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + self.source.as_ref().map(Box::as_ref) + } +} diff --git a/src/main.rs b/src/main.rs index f95f08d..78619d0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,26 +7,30 @@ use std::env::args; use std::time::Instant; use crate::archive::Fetcher; -use crate::error::{Error, Result}; +use crate::error::Error; -fn main() -> Result<()> { - use Error::*; +fn exit(error: Error) -> ! { + eprintln!("{}", error); + std::process::exit(1) +} + +fn main() { let argv = args().collect::>(); - let path = match argv.len() { - 2 => Ok(argv.get(1).unwrap()), - _ => Err(UserError("Usage: fimfareader ")), - }?; + if argv.len() != 2 { + eprintln!("Usage: fimfareader "); + std::process::exit(1); + } println!("Hellopaca, World!"); let start = Instant::now(); - let fetcher = Fetcher::from(path)?; + let result = Fetcher::from(&argv[1]); let finish = Instant::now() - start; + let fetcher = result.map_err(exit).unwrap(); + println!("Finished loading in {} milliseconds.", finish.as_millis()); println!("The archive contains {} stories.", fetcher.iter().count()); - - Ok(()) }