Use struct for error handling

This commit is contained in:
Joakim Soderlund 2019-07-27 16:04:38 +00:00
parent b250e8e1cb
commit dbebf22d42
5 changed files with 172 additions and 59 deletions

View file

@ -1,6 +1,7 @@
//! Archive fetcher. //! Archive fetcher.
use std::fs::File; use std::fs::File;
use std::io::ErrorKind as IoErrorKind;
use std::io::{BufReader, Read, Seek}; use std::io::{BufReader, Read, Seek};
use std::path::Path; use std::path::Path;
use std::sync::Mutex; use std::sync::Mutex;
@ -8,9 +9,9 @@ use std::sync::Mutex;
use zip::read::ZipArchive; use zip::read::ZipArchive;
use zip::result::ZipError; use zip::result::ZipError;
use crate::error::{Error, Result};
use super::parser::parse; use super::parser::parse;
use super::story::Story; use super::story::Story;
use crate::error::{Error, Result};
pub struct Fetcher<T> pub struct Fetcher<T>
where where
@ -22,10 +23,11 @@ where
impl Fetcher<BufReader<File>> { impl Fetcher<BufReader<File>> {
pub fn from(path: impl AsRef<Path>) -> Result<Self> { pub fn from(path: impl AsRef<Path>) -> Result<Self> {
use Error::*; use IoErrorKind::*;
let file = File::open(path).map_err(|e| match e { let file = File::open(path).map_err(|e| match e.kind() {
_ => SourceError("Could not open archive file."), NotFound => Error::archive("File not found"),
_ => Error::archive("Could not open file"),
})?; })?;
Self::from_reader(BufReader::with_capacity(8_000_000, file)) Self::from_reader(BufReader::with_capacity(8_000_000, file))
@ -45,26 +47,24 @@ where
} }
fn open(archive: T) -> Result<ZipArchive<T>> { fn open(archive: T) -> Result<ZipArchive<T>> {
use Error::*;
use ZipError::*; use ZipError::*;
ZipArchive::new(archive).map_err(|e| match e { ZipArchive::new(archive).map_err(|e| match e {
InvalidArchive(e) => SourceError(e), InvalidArchive(e) => Error::archive(e),
UnsupportedArchive(e) => SourceError(e), UnsupportedArchive(e) => Error::archive(e),
_ => SourceError("Could not read archive."), _ => Error::archive("Unknown ZIP-file issue"),
}) })
} }
fn load(archive: &mut ZipArchive<T>) -> Result<Vec<Story>> { fn load(archive: &mut ZipArchive<T>) -> Result<Vec<Story>> {
use Error::*;
use ZipError::*; use ZipError::*;
let file = archive.by_name("index.json").map_err(|e| match e { let file = archive.by_name("index.json").map_err(|e| match e {
FileNotFound => SourceError("Missing archive index."), FileNotFound => Error::archive("Missing story index"),
_ => SourceError("Could not open archive 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> { pub fn fetch(&self, key: i64) -> Option<&Story> {
@ -75,23 +75,23 @@ where
} }
pub fn read(&self, path: &str) -> Result<Vec<u8>> { pub fn read(&self, path: &str) -> Result<Vec<u8>> {
use Error::*;
use ZipError::*; use ZipError::*;
let mut archive = self.archive.lock().map_err(|e| match e { 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 { let mut file = archive.by_name(path).map_err(|e| match e {
FileNotFound => SourceError("File not found."), FileNotFound => Error::archive("Missing story data"),
_ => SourceError("Could not open file."), _ => Error::archive("Could not open story data"),
})?; })?;
let size = file.size() as usize; let size = file.size() as usize;
let mut buf = Vec::with_capacity(size); let mut buf = Vec::with_capacity(size);
file.read_to_end(&mut buf) file.read_to_end(&mut buf).map_err(|e| match e {
.map_err(|_| SourceError("Could not read file."))?; _ => Error::archive("Could not read story data"),
})?;
Ok(buf) Ok(buf)
} }

View file

@ -4,22 +4,21 @@ use std::io::BufRead;
use std::sync::mpsc::{channel, Receiver}; use std::sync::mpsc::{channel, Receiver};
use std::thread::spawn; use std::thread::spawn;
use serde::de::Error;
use serde_json::error::Result;
use serde_json::from_str; use serde_json::from_str;
use crate::error::{Error, Result};
use super::story::Story; use super::story::Story;
const TRIM: &[char] = &['"', ',', ' ', '\t', '\n', '\r']; const TRIM: &[char] = &['"', ',', ' ', '\t', '\n', '\r'];
pub fn parse(reader: impl BufRead) -> Result<Vec<Story>> { pub fn parse(reader: impl BufRead) -> Result<Vec<Story>> {
use Error::*;
let (tx, rx) = channel(); let (tx, rx) = channel();
let rx = spawn_parser(rx); let rx = spawn_parser(rx);
for line in reader.lines() { for line in reader.lines() {
let line = line.map_err(|e| match e { 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() { if tx.send(line).is_ok() {
@ -27,8 +26,8 @@ pub fn parse(reader: impl BufRead) -> Result<Vec<Story>> {
} }
return Err(match rx.recv() { return Err(match rx.recv() {
Err(_) => SourceError("Parser disappeared unexpectedly."), Err(_) => Error::custom("Parser disappeared unexpectedly"),
Ok(Ok(_)) => SourceError("Parser returned unexpectedly."), Ok(Ok(_)) => Error::custom("Parser returned unexpectedly"),
Ok(Err(error)) => error, Ok(Err(error)) => error,
}); });
} }
@ -36,13 +35,11 @@ pub fn parse(reader: impl BufRead) -> Result<Vec<Story>> {
drop(tx); drop(tx);
rx.recv().map_err(|e| match e { rx.recv().map_err(|e| match e {
_ => SourceError("Missing parser result."), _ => Error::custom("Missing parser result"),
})? })?
} }
fn spawn_parser(stream: Receiver<String>) -> Receiver<Result<Vec<Story>>> { fn spawn_parser(stream: Receiver<String>) -> Receiver<Result<Vec<Story>>> {
use Error::*;
let (tx, rx) = channel(); let (tx, rx) = channel();
spawn(move || { spawn(move || {
@ -68,11 +65,11 @@ fn spawn_parser(stream: Receiver<String>) -> Receiver<Result<Vec<Story>>> {
stories.shrink_to_fit(); stories.shrink_to_fit();
if wrappers != "{}" { if wrappers != "{}" {
return tx.send(Err(SourceError("Invalid index structure."))); return tx.send(Err(Error::custom("Invalid file structure")));
} }
if count != stories.len() { 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)) tx.send(Ok(stories))
@ -82,8 +79,6 @@ fn spawn_parser(stream: Receiver<String>) -> Receiver<Result<Vec<Story>>> {
} }
fn deserialize(line: String) -> Result<Story> { fn deserialize(line: String) -> Result<Story> {
use Error::*;
let split = line let split = line
.splitn(2, ':') .splitn(2, ':')
.map(|value| value.trim_matches(TRIM)) .map(|value| value.trim_matches(TRIM))
@ -91,19 +86,17 @@ fn deserialize(line: String) -> Result<Story> {
let (skey, json) = match split[..] { let (skey, json) = match split[..] {
[skey, json] => Ok((skey, json)), [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 { let story: Story = from_str(json)?;
_ => SourceError("Invalid meta key."),
})?;
let story: Story = from_str(json).map_err(|e| match e { let key: i64 = skey.parse().map_err(|e| match e {
_ => SourceError("Invalid meta value."), _ => Error::custom("Invalid line key"),
})?; })?;
if key != story.id { if key != story.id {
return Err(SourceError("Meta key mismatch.")); return Err(Error::custom("Line key mismatch"));
} }
Ok(story) Ok(story)

View file

@ -182,10 +182,10 @@ where
Ok(value.as_i64().unwrap()) Ok(value.as_i64().unwrap())
} else if value.is_string() { } else if value.is_string() {
value.as_str().unwrap().parse().map_err(|e| match e { 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 { } 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 let text = object
.get("hex") .get("hex")
.and_then(|value| value.as_str()) .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 { 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[..] { match array[..] {
[red, green, blue] => Ok(Color { red, green, blue }), [red, green, blue] => Ok(Color { red, green, blue }),
_ => Err(Error::custom("Color hex has invalid length.")), _ => Err(Error::custom("Color hex has invalid length")),
} }
} }
} }

View file

@ -1,10 +1,126 @@
//! Error types. //! Error types.
#[derive(Debug)] use std::error::Error as StdError;
pub enum Error { use std::fmt::Result as FmtResult;
InvalidStory(), use std::fmt::{Display, Formatter};
SourceError(&'static str), use std::result::Result as StdResult;
UserError(&'static str),
use serde_json::error::Error as SerdeError;
use self::ErrorKind::*;
pub type Result<T> = StdResult<T, Error>;
#[derive(Clone, Debug)]
pub enum ErrorKind {
ArchiveError,
IndexError,
InvalidStory,
UsageError,
} }
pub type Result<T> = std::result::Result<T, Error>; #[derive(Debug)]
pub struct Error {
kind: ErrorKind,
message: Option<String>,
source: Option<Box<dyn StdError + 'static>>,
}
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)
}
}

View file

@ -7,26 +7,30 @@ use std::env::args;
use std::time::Instant; use std::time::Instant;
use crate::archive::Fetcher; use crate::archive::Fetcher;
use crate::error::{Error, Result}; use crate::error::Error;
fn main() -> Result<()> { fn exit(error: Error) -> ! {
use Error::*; eprintln!("{}", error);
std::process::exit(1)
}
fn main() {
let argv = args().collect::<Vec<String>>(); let argv = args().collect::<Vec<String>>();
let path = match argv.len() { if argv.len() != 2 {
2 => Ok(argv.get(1).unwrap()), eprintln!("Usage: fimfareader <ARCHIVE>");
_ => Err(UserError("Usage: fimfareader <ARCHIVE>")), std::process::exit(1);
}?; }
println!("Hellopaca, World!"); println!("Hellopaca, World!");
let start = Instant::now(); let start = Instant::now();
let fetcher = Fetcher::from(path)?; let result = Fetcher::from(&argv[1]);
let finish = Instant::now() - start; let finish = Instant::now() - start;
let fetcher = result.map_err(exit).unwrap();
println!("Finished loading in {} milliseconds.", finish.as_millis()); println!("Finished loading in {} milliseconds.", finish.as_millis());
println!("The archive contains {} stories.", fetcher.iter().count()); println!("The archive contains {} stories.", fetcher.iter().count());
Ok(())
} }