From 5266e4098d5a84c73cad6fd76ca292914c74eeed Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sun, 28 Jan 2018 00:52:11 +0100 Subject: [PATCH] Refactored the whole crate to use failure. --- Cargo.toml | 2 +- examples/convertconf.rs | 2 +- src/client/conn.rs | 50 +++---- src/client/data/config.rs | 266 ++++++++++++++++++++---------------- src/client/reactor.rs | 6 +- src/client/server/mod.rs | 36 +++-- src/client/transport.rs | 26 ++-- src/error.rs | 276 ++++++++++++++++++++++++++++++-------- src/lib.rs | 3 +- src/proto/command.rs | 29 ++-- src/proto/irc.rs | 4 +- src/proto/line.rs | 4 +- src/proto/message.rs | 37 +++-- src/proto/mode.rs | 42 ++++-- 14 files changed, 513 insertions(+), 270 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ebef181..2d69270 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ bufstream = "0.1" bytes = "0.4" chrono = "0.4" encoding = "0.2" -error-chain = "0.10" +failure = "0.1" futures = "0.1" log = "0.3" native-tls = "0.1" diff --git a/examples/convertconf.rs b/examples/convertconf.rs index 97f3b81..24af879 100644 --- a/examples/convertconf.rs +++ b/examples/convertconf.rs @@ -16,7 +16,7 @@ fn main() { let args: Vec<_> = env::args().collect(); match parse(&args) { Ok(Some((ref input, ref output))) => { - let cfg = Config::load(input).unwrap(); + let mut cfg = Config::load(input).unwrap(); cfg.save(output).unwrap(); println!("Converted {} to {}.", input, output); } diff --git a/src/client/conn.rs b/src/client/conn.rs index 8dbdc0e..56c0697 100644 --- a/src/client/conn.rs +++ b/src/client/conn.rs @@ -1,6 +1,6 @@ //! A module providing IRC connections for use by `IrcServer`s. use std::fs::File; -use std::{fmt, io}; +use std::fmt; use std::io::Read; use encoding::EncoderTrap; @@ -43,7 +43,7 @@ impl fmt::Debug for Connection { } /// A convenient type alias representing the `TlsStream` future. -type TlsFuture = Box> + Send>; +type TlsFuture = Box> + Send>; /// A future representing an eventual `Connection`. pub enum ConnectionFuture<'a> { @@ -76,7 +76,7 @@ impl<'a> fmt::Debug for ConnectionFuture<'a> { impl<'a> Future for ConnectionFuture<'a> { type Item = Connection; - type Error = error::Error; + type Error = error::IrcError; fn poll(&mut self) -> Poll { match *self { @@ -95,21 +95,18 @@ impl<'a> Future for ConnectionFuture<'a> { ConnectionFuture::Mock(ref config) => { let enc: error::Result<_> = encoding_from_whatwg_label( config.encoding() - ).ok_or_else(|| io::Error::new( - io::ErrorKind::InvalidInput, - &format!("Attempted to use unknown codec {}.", config.encoding())[..], - ).into()); + ).ok_or_else(|| error::IrcError::UnknownCodec { + codec: config.encoding().to_owned(), + }); let encoding = enc?; let init_str = config.mock_initial_value(); let initial: error::Result<_> = { - encoding.encode(init_str, EncoderTrap::Replace).map_err( - |data| { - io::Error::new( - io::ErrorKind::InvalidInput, - &format!("Failed to encode {} as {}.", data, encoding.name())[..], - ).into() - }, - ) + encoding.encode(init_str, EncoderTrap::Replace).map_err(|data| { + error::IrcError::CodecFailed { + codec: encoding.name(), + data: data.into_owned(), + } + }) }; let framed = MockStream::new(&initial?).framed(IrcCodec::new(config.encoding())?); @@ -139,17 +136,14 @@ impl Connection { info!("Added {} to trusted certificates.", cert_path); } let connector = builder.build()?; - let stream = Box::new(TcpStream::connect(&config.socket_addr()?, handle) - .map_err(|e| { - let res: error::Error = e.into(); - res - }) - .and_then(move |socket| { - connector.connect_async(&domain, socket).map_err( - |e| e.into(), - ) - } - )); + let stream = Box::new(TcpStream::connect(&config.socket_addr()?, handle).map_err(|e| { + let res: error::IrcError = e.into(); + res + }).and_then(move |socket| { + connector.connect_async(&domain, socket).map_err( + |e| e.into(), + ) + })); Ok(ConnectionFuture::Secured(config, stream)) } else { info!("Connecting to {}.", config.server()?); @@ -172,7 +166,7 @@ impl Connection { impl Stream for Connection { type Item = Message; - type Error = error::Error; + type Error = error::IrcError; fn poll(&mut self) -> Poll, Self::Error> { match *self { @@ -185,7 +179,7 @@ impl Stream for Connection { impl Sink for Connection { type SinkItem = Message; - type SinkError = error::Error; + type SinkError = error::IrcError; fn start_send(&mut self, item: Self::SinkItem) -> StartSend { match *self { diff --git a/src/client/data/config.rs b/src/client/data/config.rs index 2d34c3f..fe2767d 100644 --- a/src/client/data/config.rs +++ b/src/client/data/config.rs @@ -3,9 +3,8 @@ use std::borrow::ToOwned; use std::collections::HashMap; use std::fs::File; use std::io::prelude::*; -use std::io::{Error, ErrorKind}; use std::net::{SocketAddr, ToSocketAddrs}; -use std::path::Path; +use std::path::{Path, PathBuf}; #[cfg(feature = "json")] use serde_json; @@ -14,8 +13,10 @@ use serde_yaml; #[cfg(feature = "toml")] use toml; -use error; -use error::{Result, ResultExt}; +#[cfg(feature = "toml")] +use error::TomlError; +use error::{ConfigError, Result}; +use error::IrcError::InvalidConfig; /// Configuration data. #[derive(Clone, Deserialize, Serialize, Default, PartialEq, Debug)] @@ -88,9 +89,25 @@ pub struct Config { pub channel_keys: Option>, /// A map of additional options to be stored in config. pub options: Option>, + + /// The path that this configuration was loaded from. + /// + /// This should not be specified in any configuration. It will automatically be handled by the library. + pub path: Option, } impl Config { + fn with_path>(mut self, path: P) -> Config { + self.path = Some(path.as_ref().to_owned()); + self + } + + fn path(&self) -> String { + self.path.as_ref().map(|buf| buf.to_string_lossy().into_owned()).unwrap_or_else(|| { + "".to_owned() + }) + } + /// Loads a configuration from the desired path. This will use the file extension to detect /// which format to parse the file as (json, toml, or yaml). Using each format requires having /// its respective crate feature enabled. Only json is available by default. @@ -99,155 +116,171 @@ impl Config { let mut data = String::new(); file.read_to_string(&mut data)?; - match path.as_ref().extension().and_then(|s| s.to_str()) { - Some("json") => Config::load_json(&data), - Some("toml") => Config::load_toml(&data), - Some("yaml") | Some("yml") => Config::load_yaml(&data), - Some(ext) => Err(Error::new( - ErrorKind::InvalidInput, - format!("Failed to decode configuration of unknown format {}", ext), - ).into()), - None => Err(Error::new( - ErrorKind::InvalidInput, - "Failed to decode configuration of missing or non-unicode format.", - ).into()), - } + let res = match path.as_ref().extension().and_then(|s| s.to_str()) { + Some("json") => Config::load_json(&path, &data), + Some("toml") => Config::load_toml(&path, &data), + Some("yaml") | Some("yml") => Config::load_yaml(&path, &data), + Some(ext) => Err(InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::UnknownConfigFormat { + format: ext.to_owned(), + }, + }), + None => Err(InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::MissingExtension, + }), + }; + + res.map(|config| { + config.with_path(path) + }) } #[cfg(feature = "json")] - fn load_json(data: &str) -> Result { - serde_json::from_str(&data[..]).chain_err(|| { - let e: error::Error = Error::new( - ErrorKind::InvalidInput, - "Failed to decode JSON configuration file.", - ).into(); - e + fn load_json>(path: &P, data: &str) -> Result { + serde_json::from_str(&data[..]).map_err(|e| { + InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::InvalidJson(e), + } }) } #[cfg(not(feature = "json"))] - fn load_json(_: &str) -> Result { - Err(Error::new( - ErrorKind::InvalidInput, - "JSON file decoding is disabled.", - ).into()) + fn load_json>(path: &P, _: &str) -> Result { + Err(InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::ConfigFormatDisabled { + format: "JSON" + } + }) } #[cfg(feature = "toml")] - fn load_toml(data: &str) -> Result { - toml::from_str(&data[..]).chain_err(|| { - let e: error::Error = Error::new( - ErrorKind::InvalidInput, - "Failed to decode TOML configuration file.", - ).into(); - e + fn load_toml>(path: &P, data: &str) -> Result { + toml::from_str(&data[..]).map_err(|e| { + InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::InvalidToml(TomlError::Read(e)), + } }) } #[cfg(not(feature = "toml"))] - fn load_toml(_: &str) -> Result { - Err(Error::new( - ErrorKind::InvalidInput, - "TOML file decoding is disabled.", - ).into()) + fn load_toml>(path: &P, _: &str) -> Result { + Err(InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::ConfigFormatDisabled { + format: "TOML" + } + }) } #[cfg(feature = "yaml")] - fn load_yaml(data: &str) -> Result { - serde_yaml::from_str(&data[..]).chain_err(|| { - let e: error::Error = Error::new( - ErrorKind::InvalidInput, - "Failed to decode YAML configuration file.", - ).into(); - e + fn load_yaml>(path: &P, data: &str) -> Result { + serde_yaml::from_str(&data[..]).map_err(|e| { + InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::InvalidYaml(e), + } }) } #[cfg(not(feature = "yaml"))] - fn load_yaml(_: &str) -> Result { - Err(Error::new( - ErrorKind::InvalidInput, - "YAML file decoding is disabled.", - ).into()) + fn load_yaml>(path: &P, _: &str) -> Result { + Err(InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::ConfigFormatDisabled { + format: "YAML" + } + }) } /// Saves a configuration to the desired path. This will use the file extension to detect /// which format to parse the file as (json, toml, or yaml). Using each format requires having /// its respective crate feature enabled. Only json is available by default. - pub fn save>(&self, path: P) -> Result<()> { + pub fn save>(&mut self, path: P) -> Result<()> { + let _ = self.path.take(); let mut file = File::create(&path)?; let data = match path.as_ref().extension().and_then(|s| s.to_str()) { - Some("json") => self.save_json()?, - Some("toml") => self.save_toml()?, - Some("yaml") | Some("yml") => self.save_yaml()?, - Some(ext) => return Err(Error::new( - ErrorKind::InvalidInput, - format!("Failed to encode configuration of unknown format {}", ext), - ).into()), - None => return Err(Error::new( - ErrorKind::InvalidInput, - "Failed to encode configuration of missing or non-unicode format.", - ).into()), + Some("json") => self.save_json(&path)?, + Some("toml") => self.save_toml(&path)?, + Some("yaml") | Some("yml") => self.save_yaml(&path)?, + Some(ext) => return Err(InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::UnknownConfigFormat { + format: ext.to_owned(), + }, + }), + None => return Err(InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::MissingExtension, + }), }; file.write_all(data.as_bytes())?; + self.path = Some(path.as_ref().to_owned()); Ok(()) } #[cfg(feature = "json")] - fn save_json(&self) -> Result { - serde_json::to_string(self).chain_err(|| { - let e: error::Error = Error::new( - ErrorKind::InvalidInput, - "Failed to encode JSON configuration file.", - ).into(); - e + fn save_json>(&self, path: &P) -> Result { + serde_json::to_string(self).map_err(|e| { + InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::InvalidJson(e), + } }) } #[cfg(not(feature = "json"))] - fn save_json(&self) -> Result { - Err(Error::new( - ErrorKind::InvalidInput, - "JSON file encoding is disabled.", - ).into()) + fn save_json>(&self, path: &P) -> Result { + Err(InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::ConfigFormatDisabled { + format: "JSON" + } + }) } #[cfg(feature = "toml")] - fn save_toml(&self) -> Result { - toml::to_string(self).chain_err(|| { - let e: error::Error = Error::new( - ErrorKind::InvalidInput, - "Failed to encode TOML configuration file.", - ).into(); - e + fn save_toml>(&self, path: &P) -> Result { + toml::to_string(self).map_err(|e| { + InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::InvalidToml(TomlError::Write(e)), + } }) } #[cfg(not(feature = "toml"))] - fn save_toml(&self) -> Result { - Err(Error::new( - ErrorKind::InvalidInput, - "TOML file encoding is disabled.", - ).into()) + fn save_toml>(&self, path: &P) -> Result { + Err(InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::ConfigFormatDisabled { + format: "TOML" + } + }) } #[cfg(feature = "yaml")] - fn save_yaml(&self) -> Result { - serde_yaml::to_string(self).chain_err(|| { - let e: error::Error = Error::new( - ErrorKind::InvalidInput, - "Failed to encode YAML configuration file.", - ).into(); - e + fn save_yaml>(&self, path: &P) -> Result { + serde_yaml::to_string(self).map_err(|e| { + InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::InvalidYaml(e), + } }) } #[cfg(not(feature = "yaml"))] - fn save_yaml(&self) -> Result { - Err(Error::new( - ErrorKind::InvalidInput, - "YAML file encoding is disabled.", - ).into()) + fn save_yaml>(&self, path: &P) -> Result { + Err(InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::ConfigFormatDisabled { + format: "YAML" + } + }) } /// Determines whether or not the nickname provided is the owner of the bot. @@ -260,7 +293,12 @@ impl Config { /// Gets the nickname specified in the configuration. pub fn nickname(&self) -> Result<&str> { - self.nickname.as_ref().map(|s| &s[..]).ok_or(error::ErrorKind::NicknameNotSpecified.into()) + self.nickname.as_ref().map(|s| &s[..]).ok_or_else(|| { + InvalidConfig { + path: self.path(), + cause: ConfigError::NicknameNotSpecified, + } + }) } /// Gets the bot's nickserv password specified in the configuration. @@ -292,8 +330,12 @@ impl Config { /// Gets the address of the server specified in the configuration. pub fn server(&self) -> Result<&str> { - self.server.as_ref().map(|s| &s[..]).ok_or(error::ErrorKind::NicknameNotSpecified.into()) - + self.server.as_ref().map(|s| &s[..]).ok_or_else(|| { + InvalidConfig { + path: self.path(), + cause: ConfigError::ServerNotSpecified, + } + }) } /// Gets the port of the server specified in the configuration. @@ -487,31 +529,27 @@ mod test { options: Some(HashMap::new()), use_mock_connection: None, mock_initial_value: None, + + ..Default::default() } } #[test] #[cfg(feature = "json")] - fn load() { - assert_eq!(Config::load(Path::new("client_config.json")).unwrap(), test_config()); - } - - #[test] - #[cfg(feature = "json")] - fn load_from_str() { - assert_eq!(Config::load("client_config.json").unwrap(), test_config()); + fn load_from_json() { + assert_eq!(Config::load("client_config.json").unwrap(), test_config().with_path("client_config.json")); } #[test] #[cfg(feature = "toml")] fn load_from_toml() { - assert_eq!(Config::load("client_config.toml").unwrap(), test_config()); + assert_eq!(Config::load("client_config.toml").unwrap(), test_config().with_path("client_config.toml")); } #[test] #[cfg(feature = "yaml")] fn load_from_yaml() { - assert_eq!(Config::load("client_config.yaml").unwrap(), test_config()); + assert_eq!(Config::load("client_config.yaml").unwrap(), test_config().with_path("client_config.yaml")); } #[test] diff --git a/src/client/reactor.rs b/src/client/reactor.rs index dca3fe9..5717164 100644 --- a/src/client/reactor.rs +++ b/src/client/reactor.rs @@ -41,7 +41,7 @@ use proto::Message; /// For a full example usage, see [irc::client::reactor](./index.html). pub struct IrcReactor { inner: Core, - handlers: Vec>>, + handlers: Vec>>, } impl IrcReactor { @@ -139,7 +139,7 @@ impl IrcReactor { pub fn register_server_with_handler( &mut self, server: IrcServer, handler: F ) where F: Fn(&IrcServer, Message) -> U + 'static, - U: IntoFuture + 'static { + U: IntoFuture + 'static { self.handlers.push(Box::new(server.stream().for_each(move |message| { handler(&server, message) }))); @@ -151,7 +151,7 @@ impl IrcReactor { /// be sufficient for most use cases. pub fn register_future( &mut self, future: F - ) where F: IntoFuture + 'static { + ) where F: IntoFuture + 'static { self.handlers.push(Box::new(future.into_future())) } diff --git a/src/client/server/mod.rs b/src/client/server/mod.rs index 5d09319..3c92e59 100644 --- a/src/client/server/mod.rs +++ b/src/client/server/mod.rs @@ -97,7 +97,7 @@ pub mod utils; /// }).unwrap(); /// # } /// ``` -pub trait EachIncomingExt: Stream { +pub trait EachIncomingExt: Stream { /// Blocks on the stream, running the given function on each incoming message as they arrive. fn for_each_incoming(self, mut f: F) -> error::Result<()> where @@ -111,7 +111,7 @@ pub trait EachIncomingExt: Stream { } } -impl EachIncomingExt for T where T: Stream {} +impl EachIncomingExt for T where T: Stream {} /// An interface for communicating with an IRC server. pub trait Server { @@ -211,7 +211,7 @@ pub struct ServerStream { impl Stream for ServerStream { type Item = Message; - type Error = error::Error; + type Error = error::IrcError; fn poll(&mut self) -> Poll, Self::Error> { match try_ready!(self.stream.poll()) { @@ -352,7 +352,6 @@ impl ServerState { trace!("[RECV] {}", msg.to_string()); match msg.command { JOIN(ref chan, _, _) => self.handle_join(msg.source_nickname().unwrap_or(""), chan), - /// This will panic if not specified. PART(ref chan, _) => self.handle_part(msg.source_nickname().unwrap_or(""), chan), QUIT(_) => self.handle_quit(msg.source_nickname().unwrap_or("")), NICK(ref new_nick) => { @@ -442,7 +441,16 @@ impl ServerState { if self.config().umodes().is_empty() { Ok(()) } else { - self.send_mode(self.current_nickname(), &Mode::as_user_modes(self.config().umodes())?) + self.send_mode( + self.current_nickname(), &Mode::as_user_modes(self.config().umodes()).map_err(|e| { + error::IrcError::InvalidMessage { + string: format!( + "MODE {} {}", self.current_nickname(), self.config().umodes() + ), + cause: e, + } + })? + ) } } @@ -721,9 +729,8 @@ impl IrcServer { tx_view.send(conn.log_view()).unwrap(); let (sink, stream) = conn.split(); - let outgoing_future = sink.send_all(rx_outgoing.map_err(|_| { - let res: error::Error = error::ErrorKind::ChannelError.into(); - res + let outgoing_future = sink.send_all(rx_outgoing.map_err::(|_| { + unreachable!("futures::sync::mpsc::Receiver should never return Err"); })).map(|_| ()).map_err(|e| panic!("{}", e)); // Send the stream half back to the original thread. @@ -821,7 +828,7 @@ pub struct IrcServerFuture<'a> { impl<'a> Future for IrcServerFuture<'a> { type Item = PackedIrcServer; - type Error = error::Error; + type Error = error::IrcError; fn poll(&mut self) -> Poll { let conn = try_ready!(self.conn.poll()); @@ -829,10 +836,11 @@ impl<'a> Future for IrcServerFuture<'a> { let view = conn.log_view(); let (sink, stream) = conn.split(); - let outgoing_future = sink.send_all(self.rx_outgoing.take().unwrap().map_err(|()| { - let res: error::Error = error::ErrorKind::ChannelError.into(); - res - })).map(|_| ()); + let outgoing_future = sink.send_all( + self.rx_outgoing.take().unwrap().map_err::(|()| { + unreachable!("futures::sync::mpsc::Receiver should never return Err"); + }) + ).map(|_| ()); let server = IrcServer { state: Arc::new(ServerState::new( @@ -850,7 +858,7 @@ impl<'a> Future for IrcServerFuture<'a> { /// This type should only be used by advanced users who are familiar with the implementation of this /// crate. An easy to use abstraction that does not require this knowledge is available via /// [IrcReactors](../reactor/struct.IrcReactor.html). -pub struct PackedIrcServer(pub IrcServer, pub Box>); +pub struct PackedIrcServer(pub IrcServer, pub Box>); #[cfg(test)] mod test { diff --git a/src/client/transport.rs b/src/client/transport.rs index ca52c91..dde82b2 100644 --- a/src/client/transport.rs +++ b/src/client/transport.rs @@ -88,12 +88,12 @@ where T: AsyncRead + AsyncWrite, { type Item = Message; - type Error = error::Error; + type Error = error::IrcError; fn poll(&mut self) -> Poll, Self::Error> { if self.ping_timed_out() { self.close()?; - return Err(error::ErrorKind::PingTimeout.into()) + return Err(error::IrcError::PingTimeout) } let timer_poll = self.ping_timer.poll()?; @@ -144,12 +144,12 @@ where T: AsyncRead + AsyncWrite, { type SinkItem = Message; - type SinkError = error::Error; + type SinkError = error::IrcError; fn start_send(&mut self, item: Self::SinkItem) -> StartSend { if self.ping_timed_out() { self.close()?; - Err(error::ErrorKind::PingTimeout.into()) + Err(error::IrcError::PingTimeout) } else { // Check if the oldest message in the rolling window is discounted. if let Async::Ready(()) = self.rolling_burst_window_front()? { @@ -180,7 +180,7 @@ where fn poll_complete(&mut self) -> Poll<(), Self::SinkError> { if self.ping_timed_out() { self.close()?; - Err(error::ErrorKind::PingTimeout.into()) + Err(error::IrcError::PingTimeout) } else { Ok(self.inner.poll_complete()?) } @@ -201,16 +201,12 @@ pub struct LogView { impl LogView { /// Gets a read guard for all the messages sent on the transport. pub fn sent(&self) -> error::Result>> { - self.sent.read().map_err( - |_| error::ErrorKind::PoisonedLog.into(), - ) + self.sent.read().map_err(|_| error::IrcError::PoisonedLog) } /// Gets a read guard for all the messages received on the transport. pub fn received(&self) -> error::Result>> { - self.received.read().map_err( - |_| error::ErrorKind::PoisonedLog.into(), - ) + self.received.read().map_err(|_| error::IrcError::PoisonedLog) } } @@ -250,13 +246,13 @@ where T: AsyncRead + AsyncWrite, { type Item = Message; - type Error = error::Error; + type Error = error::IrcError; fn poll(&mut self) -> Poll, Self::Error> { match try_ready!(self.inner.poll()) { Some(msg) => { let recv: error::Result<_> = self.view.received.write().map_err(|_| { - error::ErrorKind::PoisonedLog.into() + error::IrcError::PoisonedLog }); recv?.push(msg.clone()); Ok(Async::Ready(Some(msg))) @@ -271,12 +267,12 @@ where T: AsyncRead + AsyncWrite, { type SinkItem = Message; - type SinkError = error::Error; + type SinkError = error::IrcError; fn start_send(&mut self, item: Self::SinkItem) -> StartSend { let res = self.inner.start_send(item.clone())?; let sent: error::Result<_> = self.view.sent.write().map_err(|_| { - error::ErrorKind::PoisonedLog.into() + error::IrcError::PoisonedLog }); sent?.push(item); Ok(res) diff --git a/src/error.rs b/src/error.rs index e4fea6b..47c0f1a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,67 +1,233 @@ -//! Errors for `irc` crate using `error_chain`. -#![allow(missing_docs)] +//! Errors for `irc` crate using `failure`. -error_chain! { - foreign_links { - Io(::std::io::Error); - Tls(::native_tls::Error); - Recv(::std::sync::mpsc::RecvError); - SendMessage(::futures::sync::mpsc::SendError<::proto::Message>); - OneShotCancelled(::futures::sync::oneshot::Canceled); - Timer(::tokio_timer::TimerError); - } +use std::io::Error as IoError; +use std::sync::mpsc::RecvError; - errors { - /// A parsing error for empty strings as messages. - ParseEmpty { - description("Cannot parse an empty string as a message.") - display("Cannot parse an empty string as a message.") - } +use futures::sync::mpsc::SendError; +use futures::sync::oneshot::Canceled; +use native_tls::Error as TlsError; +#[cfg(feature = "json")] +use serde_json::Error as JsonError; +#[cfg(feature = "yaml")] +use serde_yaml::Error as YamlError; +use tokio_timer::TimerError; +#[cfg(feature = "toml")] +use toml::de::Error as TomlReadError; +#[cfg(feature = "toml")] +use toml::ser::Error as TomlWriteError; - /// A parsing error for invalid or missing commands in messages. - InvalidCommand { - description("Message contained a missing or invalid Command.") - display("Message contained a missing or invalid Command.") - } +use proto::Message; - /// A parsing error for failures in subcommand parsing (e.g. CAP and metadata). - SubCommandParsingFailed { - description("Failed to parse an IRC subcommand.") - display("Failed to parse an IRC subcommand.") - } +/// A specialized `Result` type for the `irc` crate. +pub type Result = ::std::result::Result; - /// Failed to parse a mode correctly. - ModeParsingFailed { - description("Failed to parse a mode correctly.") - display("Failed to parse a mode correctly.") - } +/// The main crate-wide error type. +#[derive(Debug, Fail)] +pub enum IrcError { + /// An internal I/O error. + #[fail(display = "an io error occurred")] + Io(#[cause] IoError), - /// An error occurred on one of the internal channels of the `IrcServer`. - ChannelError { - description("An error occured on one of the IrcServer's internal channels.") - display("An error occured on one of the IrcServer's internal channels.") - } + /// An internal TLS error. + #[fail(display = "a TLS error occurred")] + Tls(#[cause] TlsError), - /// An error occured causing a mutex for a logged transport to be poisoned. - PoisonedLog { - description("An error occured causing a mutex for a logged transport to be poisoned.") - display("An error occured causing a mutex for a logged transport to be poisoned.") - } + /// An internal synchronous channel closed. + #[fail(display = "a sync channel closed")] + SyncChannelClosed(#[cause] RecvError), - /// Connection timed out due to no ping response. - PingTimeout { - description("The connection timed out due to no ping response.") - display("The connection timed out due to no ping response.") - } + /// An internal asynchronous channel closed. + #[fail(display = "an async channel closed")] + AsyncChannelClosed(#[cause] SendError), - NicknameNotSpecified { - description("No nickname was specified for use with this IrcServer.") - display("No nickname was specified for use with this IrcServer.") - } + /// An internal oneshot channel closed. + #[fail(display = "a oneshot channel closed")] + OneShotCanceled(#[cause] Canceled), - ServerNotSpecified { - description("No server was specified to connect to.") - display("No server was specified to connect to.") - } + /// An internal timer error. + #[fail(display = "timer failed")] + Timer(#[cause] TimerError), + + /// Error for invalid configurations. + #[fail(display = "invalid config: {}", path)] + InvalidConfig { + /// The path to the configuration, or "" if none specified. + path: String, + /// The detailed configuration error. + #[cause] + cause: ConfigError, + }, + + /// Error for invalid messages. + #[fail(display = "invalid message: {}", string)] + InvalidMessage { + /// The string that failed to parse. + string: String, + /// The detailed message parsing error. + #[cause] + cause: MessageParseError, + }, + + /// Mutex for a logged transport was poisoned making the log inaccessible. + #[fail(display = "mutex for a logged transport was poisoned")] + PoisonedLog, + + /// Ping timed out due to no response. + #[fail(display = "connection reset: no ping response")] + PingTimeout, + + /// Failed to lookup an unknown codec. + #[fail(display = "unknown codec: {}", codec)] + UnknownCodec { + /// The attempted codec. + codec: String, + }, + + /// Failed to encode or decode something with the given codec. + #[fail(display = "codec {} failed: {}", codec, data)] + CodecFailed { + /// The canonical codec name. + codec: &'static str, + /// The data that failed to encode or decode. + data: String, + }, +} + +/// Errors that occur when parsing messages. +#[derive(Debug, Fail)] +pub enum MessageParseError { + /// The message was empty. + #[fail(display = "empty message")] + EmptyMessage, + + /// The command was invalid (i.e. missing). + #[fail(display = "invalid command")] + InvalidCommand, + + /// The mode string was malformed. + #[fail(display = "invalid mode string: {}", string)] + InvalidModeString { + /// The invalid mode string. + string: String, + /// The detailed mode parsing error. + #[cause] + cause: ModeParseError, + }, + + /// The subcommand used was invalid. + #[fail(display = "invalid {} subcommand: {}", cmd, sub)] + InvalidSubcommand { + /// The command whose invalid subcommand was referenced. + cmd: &'static str, + /// The invalid subcommand. + sub: String, + } +} + +/// Errors that occur while parsing mode strings. +#[derive(Debug, Fail)] +pub enum ModeParseError { + /// Invalid modifier used in a mode string (only + and - are valid). + #[fail(display = "invalid mode modifier: {}", modifier)] + InvalidModeModifier { + /// The invalid mode modifier. + modifier: char, + }, + + /// Missing modifier used in a mode string. + #[fail(display = "missing mode modifier")] + MissingModeModifier, +} + +/// Errors that occur with configurations. +#[derive(Debug, Fail)] +pub enum ConfigError { + /// Failed to parse as TOML. + #[cfg(feature = "toml")] + #[fail(display = "invalid toml")] + InvalidToml(#[cause] TomlError), + + /// Failed to parse as JSON. + #[cfg(feature = "json")] + #[fail(display = "invalid json")] + InvalidJson(#[cause] JsonError), + + /// Failed to parse as YAML. + #[cfg(feature = "yaml")] + #[fail(display = "invalid yaml")] + InvalidYaml(#[cause] YamlError), + + /// Failed to parse the given format because it was disabled at compile-time. + #[fail(display = "config format disabled: {}", format)] + ConfigFormatDisabled { + /// The disabled file format. + format: &'static str, + }, + + /// Could not identify the given file format. + #[fail(display = "config format unknown: {}", format)] + UnknownConfigFormat { + /// The unknown file extension. + format: String, + }, + + /// File was missing an extension to identify file format. + #[fail(display = "missing format extension")] + MissingExtension, + + /// Configuration does not specify a nickname. + #[fail(display = "nickname not specified")] + NicknameNotSpecified, + + /// Configuration does not specify a server. + #[fail(display = "server not specified")] + ServerNotSpecified, +} + +/// A wrapper that combines toml's serialization and deserialization errors. +#[cfg(feature = "toml")] +#[derive(Debug, Fail)] +pub enum TomlError { + /// A TOML deserialization error. + #[fail(display = "deserialization failed")] + Read(#[cause] TomlReadError), + /// A TOML serialization error. + #[fail(display = "serialization failed")] + Write(#[cause] TomlWriteError), +} + +impl From for IrcError { + fn from(e: IoError) -> IrcError { + IrcError::Io(e) + } +} + +impl From for IrcError { + fn from(e: TlsError) -> IrcError { + IrcError::Tls(e) + } +} + +impl From for IrcError { + fn from(e: RecvError) -> IrcError { + IrcError::SyncChannelClosed(e) + } +} + +impl From> for IrcError { + fn from(e: SendError) -> IrcError { + IrcError::AsyncChannelClosed(e) + } +} + +impl From for IrcError { + fn from(e: Canceled) -> IrcError { + IrcError::OneShotCanceled(e) + } +} + +impl From for IrcError { + fn from(e: TimerError) -> IrcError { + IrcError::Timer(e) } } diff --git a/src/lib.rs b/src/lib.rs index 0fbff4c..b384c6b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,13 +40,12 @@ //! ``` #![warn(missing_docs)] -#![recursion_limit="128"] extern crate bufstream; extern crate bytes; extern crate chrono; #[macro_use] -extern crate error_chain; +extern crate failure; extern crate encoding; #[macro_use] extern crate futures; diff --git a/src/proto/command.rs b/src/proto/command.rs index 3737f3c..7c0b70a 100644 --- a/src/proto/command.rs +++ b/src/proto/command.rs @@ -2,7 +2,7 @@ use std::ascii::AsciiExt; use std::str::FromStr; -use error; +use error::MessageParseError; use proto::{ChannelExt, ChannelMode, Mode, Response, UserMode}; /// List of all client commands as defined in [RFC 2812](http://tools.ietf.org/html/rfc2812). This @@ -446,7 +446,7 @@ impl<'a> From<&'a Command> for String { impl Command { /// Constructs a new Command. - pub fn new(cmd: &str, args: Vec<&str>, suffix: Option<&str>) -> error::Result { + pub fn new(cmd: &str, args: Vec<&str>, suffix: Option<&str>) -> Result { Ok(if cmd.eq_ignore_ascii_case("PASS") { match suffix { Some(suffix) => { @@ -1653,8 +1653,9 @@ impl CapSubCommand { } impl FromStr for CapSubCommand { - type Err = error::Error; - fn from_str(s: &str) -> error::Result { + type Err = MessageParseError; + + fn from_str(s: &str) -> Result { if s.eq_ignore_ascii_case("LS") { Ok(CapSubCommand::LS) } else if s.eq_ignore_ascii_case("LIST") { @@ -1672,7 +1673,10 @@ impl FromStr for CapSubCommand { } else if s.eq_ignore_ascii_case("DEL") { Ok(CapSubCommand::DEL) } else { - Err(error::ErrorKind::SubCommandParsingFailed.into()) + Err(MessageParseError::InvalidSubcommand { + cmd: "CAP", + sub: s.to_owned(), + }) } } } @@ -1704,8 +1708,9 @@ impl MetadataSubCommand { } impl FromStr for MetadataSubCommand { - type Err = error::Error; - fn from_str(s: &str) -> error::Result { + type Err = MessageParseError; + + fn from_str(s: &str) -> Result { if s.eq_ignore_ascii_case("GET") { Ok(MetadataSubCommand::GET) } else if s.eq_ignore_ascii_case("LIST") { @@ -1715,7 +1720,10 @@ impl FromStr for MetadataSubCommand { } else if s.eq_ignore_ascii_case("CLEAR") { Ok(MetadataSubCommand::CLEAR) } else { - Err(error::ErrorKind::SubCommandParsingFailed.into()) + Err(MessageParseError::InvalidSubcommand { + cmd: "METADATA", + sub: s.to_owned(), + }) } } } @@ -1743,8 +1751,9 @@ impl BatchSubCommand { } impl FromStr for BatchSubCommand { - type Err = error::Error; - fn from_str(s: &str) -> error::Result { + type Err = MessageParseError; + + fn from_str(s: &str) -> Result { if s.eq_ignore_ascii_case("NETSPLIT") { Ok(BatchSubCommand::NETSPLIT) } else if s.eq_ignore_ascii_case("NETJOIN") { diff --git a/src/proto/irc.rs b/src/proto/irc.rs index b01270f..e0b2fca 100644 --- a/src/proto/irc.rs +++ b/src/proto/irc.rs @@ -20,7 +20,7 @@ impl IrcCodec { impl Decoder for IrcCodec { type Item = Message; - type Error = error::Error; + type Error = error::IrcError; fn decode(&mut self, src: &mut BytesMut) -> error::Result> { self.inner.decode(src).and_then(|res| { @@ -31,7 +31,7 @@ impl Decoder for IrcCodec { impl Encoder for IrcCodec { type Item = Message; - type Error = error::Error; + type Error = error::IrcError; fn encode(&mut self, msg: Message, dst: &mut BytesMut) -> error::Result<()> { diff --git a/src/proto/line.rs b/src/proto/line.rs index ce59c58..2e1ade2 100644 --- a/src/proto/line.rs +++ b/src/proto/line.rs @@ -28,7 +28,7 @@ impl LineCodec { impl Decoder for LineCodec { type Item = String; - type Error = error::Error; + type Error = error::IrcError; fn decode(&mut self, src: &mut BytesMut) -> error::Result> { if let Some(n) = src.as_ref().iter().position(|b| *b == b'\n') { @@ -53,7 +53,7 @@ impl Decoder for LineCodec { impl Encoder for LineCodec { type Item = String; - type Error = error::Error; + type Error = error::IrcError; fn encode(&mut self, msg: String, dst: &mut BytesMut) -> error::Result<()> { // Encode the message using the codec's encoding. diff --git a/src/proto/message.rs b/src/proto/message.rs index 8605cc2..1c2a3af 100644 --- a/src/proto/message.rs +++ b/src/proto/message.rs @@ -4,7 +4,7 @@ use std::fmt::{Display, Formatter, Result as FmtResult}; use std::str::FromStr; use error; -use error::{Error, ErrorKind}; +use error::{IrcError, MessageParseError}; use proto::{Command, ChannelExt}; /// A data structure representing an IRC message according to the protocol specification. It @@ -43,7 +43,7 @@ impl Message { command: &str, args: Vec<&str>, suffix: Option<&str>, - ) -> error::Result { + ) -> Result { Message::with_tags(None, prefix, command, args, suffix) } @@ -56,7 +56,7 @@ impl Message { command: &str, args: Vec<&str>, suffix: Option<&str>, - ) -> error::Result { + ) -> Result { Ok(Message { tags: tags, prefix: prefix.map(|s| s.to_owned()), @@ -170,13 +170,18 @@ impl From for Message { } impl FromStr for Message { - type Err = Error; + type Err = IrcError; fn from_str(s: &str) -> Result { - let mut state = s; if s.is_empty() { - return Err(ErrorKind::ParseEmpty.into()); + return Err(IrcError::InvalidMessage { + string: s.to_owned(), + cause: MessageParseError::EmptyMessage, + }) } + + let mut state = s; + let tags = if state.starts_with('@') { let tags = state.find(' ').map(|i| &state[1..i]); state = state.find(' ').map_or("", |i| &state[i + 1..]); @@ -193,6 +198,7 @@ impl FromStr for Message { } else { None }; + let prefix = if state.starts_with(':') { let prefix = state.find(' ').map(|i| &state[1..i]); state = state.find(' ').map_or("", |i| &state[i + 1..]); @@ -200,6 +206,7 @@ impl FromStr for Message { } else { None }; + let line_ending_len = if state.ends_with("\r\n") { "\r\n" } else if state.ends_with('\r') { @@ -209,6 +216,7 @@ impl FromStr for Message { } else { "" }.len(); + let suffix = if state.contains(" :") { let suffix = state.find(" :").map(|i| &state[i + 2..state.len() - line_ending_len]); state = state.find(" :").map_or("", |i| &state[..i + 1]); @@ -217,24 +225,33 @@ impl FromStr for Message { state = &state[..state.len() - line_ending_len]; None }; + let command = match state.find(' ').map(|i| &state[..i]) { Some(cmd) => { state = state.find(' ').map_or("", |i| &state[i + 1..]); cmd } // If there's no arguments but the "command" starts with colon, it's not a command. - None if state.starts_with(":") => return Err(ErrorKind::InvalidCommand.into()), + None if state.starts_with(":") => return Err(IrcError::InvalidMessage { + string: s.to_owned(), + cause: MessageParseError::InvalidCommand, + }), // If there's no arguments following the command, the rest of the state is the command. None => { let cmd = state; state = ""; cmd }, - }; + let args: Vec<_> = state.splitn(14, ' ').filter(|s| !s.is_empty()).collect(); - Message::with_tags(tags, prefix, command, args, suffix) - .map_err(|_| ErrorKind::InvalidCommand.into()) + + Message::with_tags(tags, prefix, command, args, suffix).map_err(|e| { + IrcError::InvalidMessage { + string: s.to_owned(), + cause: e, + } + }) } } diff --git a/src/proto/mode.rs b/src/proto/mode.rs index ecf8bce..806104f 100644 --- a/src/proto/mode.rs +++ b/src/proto/mode.rs @@ -1,7 +1,9 @@ //! A module defining an API for IRC user and channel modes. use std::fmt; -use error; +use error::MessageParseError; +use error::MessageParseError::InvalidModeString; +use error::ModeParseError::*; use proto::Command; /// A marker trait for different kinds of Modes. @@ -48,10 +50,10 @@ impl ModeType for UserMode { } impl UserMode { - fn from_char(c: char) -> error::Result { + fn from_char(c: char) -> UserMode { use self::UserMode::*; - Ok(match c { + match c { 'a' => Away, 'i' => Invisible, 'w' => Wallops, @@ -61,7 +63,7 @@ impl UserMode { 's' => ServerNotices, 'x' => MaskedHost, _ => Unknown(c), - }) + } } } @@ -141,10 +143,10 @@ impl ModeType for ChannelMode { } impl ChannelMode { - fn from_char(c: char) -> error::Result { + fn from_char(c: char) -> ChannelMode { use self::ChannelMode::*; - Ok(match c { + match c { 'b' => Ban, 'e' => Exception, 'l' => Limit, @@ -162,7 +164,7 @@ impl ChannelMode { 'h' => Halfop, 'v' => Voice, _ => Unknown(c), - }) + } } } @@ -242,7 +244,7 @@ enum PlusMinus { impl Mode { // TODO: turning more edge cases into errors. /// Parses the specified mode string as user modes. - pub fn as_user_modes(s: &str) -> error::Result>> { + pub fn as_user_modes(s: &str) -> Result>, MessageParseError> { use self::PlusMinus::*; let mut res = vec![]; @@ -255,11 +257,18 @@ impl Mode { let init = match chars.next() { Some('+') => Plus, Some('-') => Minus, - _ => return Err(error::ErrorKind::ModeParsingFailed.into()), + Some(c) => return Err(InvalidModeString { + string: s.to_owned(), + cause: InvalidModeModifier { modifier: c }, + }), + None => return Err(InvalidModeString { + string: s.to_owned(), + cause: MissingModeModifier, + }), }; for c in chars { - let mode = UserMode::from_char(c)?; + let mode = UserMode::from_char(c); let arg = if mode.takes_arg() { pieces.next() } else { @@ -281,7 +290,7 @@ impl Mode { impl Mode { // TODO: turning more edge cases into errors. /// Parses the specified mode string as channel modes. - pub fn as_channel_modes(s: &str) -> error::Result>> { + pub fn as_channel_modes(s: &str) -> Result>, MessageParseError> { use self::PlusMinus::*; let mut res = vec![]; @@ -294,11 +303,18 @@ impl Mode { let init = match chars.next() { Some('+') => Plus, Some('-') => Minus, - _ => return Err(error::ErrorKind::ModeParsingFailed.into()), + Some(c) => return Err(InvalidModeString { + string: s.to_owned(), + cause: InvalidModeModifier { modifier: c }, + }), + None => return Err(InvalidModeString { + string: s.to_owned(), + cause: MissingModeModifier, + }), }; for c in chars { - let mode = ChannelMode::from_char(c)?; + let mode = ChannelMode::from_char(c); let arg = if mode.takes_arg() { pieces.next() } else {