diff --git a/.travis.yml b/.travis.yml index 9dfed4a..7f993b9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,10 +7,10 @@ sudo: false script: - chmod +x mktestconfig.sh - ./mktestconfig.sh + - cargo build --verbose --features "toml yaml" + - cargo test --verbose --features "toml yaml" - cargo build --verbose --no-default-features - - cargo build --verbose - cargo test --verbose --no-default-features - - cargo test --verbose notifications: email: false irc: diff --git a/Cargo.toml b/Cargo.toml index 7627c85..903dcb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,11 @@ repository = "https://github.com/aatxe/irc" readme = "README.md" [features] -default = ["ctcp"] +default = ["ctcp", "json"] ctcp = [] nochanlists = [] +json = ["serde_json"] +yaml = ["serde_yaml"] [dependencies] bufstream = "0.1" @@ -25,9 +27,15 @@ futures = "0.1" native-tls = "0.1" serde = "1.0" serde_derive = "1.0" -serde_json = "1.0" +serde_json = { version = "1.0", optional = true } +serde_yaml = { version = "0.7", optional = true } tokio-core = "0.1" tokio-io = "0.1" tokio-mockstream = "1.1" tokio-timer = "0.1" tokio-tls = "0.1" +toml = { version = "0.4", optional = true } + +[dev-dependencies] +args = "2.0" +getopts = "0.2" diff --git a/examples/convertconf.rs b/examples/convertconf.rs new file mode 100644 index 0000000..b13eb63 --- /dev/null +++ b/examples/convertconf.rs @@ -0,0 +1,62 @@ +extern crate args; +extern crate getopts; +extern crate irc; + +use std::env; +use std::process::exit; + +use args::{Args, ArgsError}; +use getopts::Occur; +use irc::client::data::Config; + +const PROGRAM_DESC: &'static str = "Use this program to convert configs between {JSON, TOML, YAML}."; +const PROGRAM_NAME: &'static str = "convertconf"; + +fn main() { + let args: Vec<_> = env::args().collect(); + match parse(&args) { + Ok(Some((ref input, ref output))) => { + let cfg = Config::load(input).unwrap(); + cfg.save(output).unwrap(); + println!("Converted {} to {}.", input, output); + } + Ok(None) => { + println!("Failed to provide required arguments."); + exit(1); + } + Err(err) => { + println!("{}", err); + exit(1); + } + } +} + +fn parse(input: &Vec) -> Result, ArgsError> { + let mut args = Args::new(PROGRAM_NAME, PROGRAM_DESC); + args.flag("h", "help", "Print the usage menu"); + args.option("i", + "input", + "The path to the input config", + "FILE", + Occur::Req, + None); + args.option("o", + "output", + "The path to output the new config to", + "FILE", + Occur::Req, + None); + + args.parse(input)?; + + let help = args.value_of("help")?; + if help { + args.full_usage(); + return Ok(None); + } + + Ok(Some(( + args.value_of("input")?, + args.value_of("output")?, + ))) +} diff --git a/mktestconfig.sh b/mktestconfig.sh index 112af8e..b6f5fa8 100755 --- a/mktestconfig.sh +++ b/mktestconfig.sh @@ -1 +1,3 @@ echo "{\"owners\": [\"test\"],\"nickname\": \"test\",\"username\": \"test\",\"realname\": \"test\",\"password\": \"\",\"server\": \"irc.test.net\",\"port\": 6667,\"use_ssl\": false,\"encoding\": \"UTF-8\",\"channels\": [\"#test\", \"#test2\"],\"umodes\": \"+BR\",\"options\": {}}" > client_config.json +cargo run --example convertconf --features "toml yaml" -- -i client_config.json -o client_config.toml +cargo run --example convertconf --features "toml yaml" -- -i client_config.json -o client_config.yaml diff --git a/src/client/data/config.rs b/src/client/data/config.rs index 65656ec..95229b2 100644 --- a/src/client/data/config.rs +++ b/src/client/data/config.rs @@ -7,7 +7,12 @@ use std::io::{Error, ErrorKind}; use std::net::{SocketAddr, ToSocketAddrs}; use std::path::Path; +#[cfg(feature = "json")] use serde_json; +#[cfg(feature = "yaml")] +use serde_yaml; +#[cfg(feature = "toml")] +use toml; use error::Result; @@ -80,30 +85,157 @@ pub struct Config { } impl Config { - /// Loads a JSON configuration from the desired path. + /// 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. pub fn load>(path: P) -> Result { - let mut file = File::open(path)?; + let mut file = File::open(&path)?; 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) => return Err(Error::new( + ErrorKind::InvalidInput, + format!("Failed to decode configuration of unknown format {}", ext), + ).into()), + None => return Err(Error::new( + ErrorKind::InvalidInput, + "Failed to decode configuration of missing or non-unicode format.", + ).into()), + } + } + + #[cfg(feature = "json")] + fn load_json(data: &str) -> Result { serde_json::from_str(&data[..]).map_err(|_| { Error::new( ErrorKind::InvalidInput, - "Failed to decode configuration file.", + "Failed to decode JSON configuration file.", ).into() }) } - /// Saves a JSON configuration to the desired path. + #[cfg(not(feature = "json"))] + fn load_json(_: &str) -> Result { + Err(Error::new( + ErrorKind::InvalidInput, + "JSON file decoding is disabled.", + ).into()) + } + + #[cfg(feature = "toml")] + fn load_toml(data: &str) -> Result { + toml::from_str(&data[..]).map_err(|_| { + Error::new( + ErrorKind::InvalidInput, + "Failed to decode TOML configuration file.", + ).into() + }) + } + + #[cfg(not(feature = "toml"))] + fn load_toml(_: &str) -> Result { + Err(Error::new( + ErrorKind::InvalidInput, + "TOML file decoding is disabled.", + ).into()) + } + + #[cfg(feature = "yaml")] + fn load_yaml(data: &str) -> Result { + serde_yaml::from_str(&data[..]).map_err(|_| { + Error::new( + ErrorKind::InvalidInput, + "Failed to decode YAML configuration file.", + ).into() + }) + } + + #[cfg(not(feature = "yaml"))] + fn load_yaml(_: &str) -> Result { + Err(Error::new( + ErrorKind::InvalidInput, + "YAML file decoding is disabled.", + ).into()) + } + + /// 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<()> { - let mut file = File::create(path)?; - file.write_all( - serde_json::to_string(self).map_err(|_| { - Error::new( - ErrorKind::InvalidInput, - "Failed to encode configuration file.", - ) - })?.as_bytes(), - ).map_err(|e| e.into()) + 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()), + }; + file.write_all(data.as_bytes())?; + Ok(()) + } + + #[cfg(feature = "json")] + fn save_json(&self) -> Result { + serde_json::to_string(self).map_err(|_| { + Error::new( + ErrorKind::InvalidInput, + "Failed to encode JSON configuration file.", + ).into() + }) + } + + #[cfg(not(feature = "json"))] + fn save_json(&self) -> Result { + Err(Error::new( + ErrorKind::InvalidInput, + "JSON file encoding is disabled.", + ).into()) + } + + #[cfg(feature = "toml")] + fn save_toml(&self) -> Result { + toml::to_string(self).map_err(|_| { + Error::new( + ErrorKind::InvalidInput, + "Failed to encode TOML configuration file.", + ).into() + }) + } + + #[cfg(not(feature = "toml"))] + fn save_toml(&self) -> Result { + Err(Error::new( + ErrorKind::InvalidInput, + "TOML file encoding is disabled.", + ).into()) + } + + #[cfg(feature = "yaml")] + fn save_yaml(&self) -> Result { + serde_yaml::to_string(self).map_err(|_| { + Error::new( + ErrorKind::InvalidInput, + "Failed to encode YAML configuration file.", + ).into() + }) + } + + #[cfg(not(feature = "yaml"))] + fn save_yaml(&self) -> Result { + Err(Error::new( + ErrorKind::InvalidInput, + "YAML file encoding is disabled.", + ).into()) } /// Determines whether or not the nickname provided is the owner of the bot. @@ -308,78 +440,66 @@ impl Config { mod test { use std::collections::HashMap; use std::default::Default; + #[cfg(feature = "json")] use std::path::Path; use super::Config; + fn test_config() -> Config { + Config { + owners: Some(vec![format!("test")]), + nickname: Some(format!("test")), + nick_password: None, + alt_nicks: None, + username: Some(format!("test")), + realname: Some(format!("test")), + password: Some(String::new()), + umodes: Some(format!("+BR")), + server: Some(format!("irc.test.net")), + port: Some(6667), + use_ssl: Some(false), + cert_path: None, + encoding: Some(format!("UTF-8")), + channels: Some(vec![format!("#test"), format!("#test2")]), + channel_keys: None, + user_info: None, + version: None, + source: None, + ping_time: None, + ping_timeout: None, + burst_window_length: None, + max_messages_in_burst: None, + should_ghost: None, + ghost_sequence: None, + options: Some(HashMap::new()), + use_mock_connection: None, + mock_initial_value: None, + } + } + #[test] + #[cfg(feature = "json")] fn load() { - let cfg = Config { - owners: Some(vec![format!("test")]), - nickname: Some(format!("test")), - nick_password: None, - alt_nicks: None, - username: Some(format!("test")), - realname: Some(format!("test")), - password: Some(String::new()), - umodes: Some(format!("+BR")), - server: Some(format!("irc.test.net")), - port: Some(6667), - use_ssl: Some(false), - cert_path: None, - encoding: Some(format!("UTF-8")), - channels: Some(vec![format!("#test"), format!("#test2")]), - channel_keys: None, - user_info: None, - version: None, - source: None, - ping_time: None, - ping_timeout: None, - burst_window_length: None, - max_messages_in_burst: None, - should_ghost: None, - ghost_sequence: None, - options: Some(HashMap::new()), - use_mock_connection: None, - mock_initial_value: None, - }; - assert_eq!(Config::load(Path::new("client_config.json")).unwrap(), cfg); + assert_eq!(Config::load(Path::new("client_config.json")).unwrap(), test_config()); } #[test] + #[cfg(feature = "json")] fn load_from_str() { - let cfg = Config { - owners: Some(vec![format!("test")]), - nickname: Some(format!("test")), - nick_password: None, - alt_nicks: None, - username: Some(format!("test")), - realname: Some(format!("test")), - umodes: Some(format!("+BR")), - password: Some(String::new()), - server: Some(format!("irc.test.net")), - port: Some(6667), - use_ssl: Some(false), - cert_path: None, - encoding: Some(format!("UTF-8")), - channels: Some(vec![format!("#test"), format!("#test2")]), - channel_keys: None, - user_info: None, - version: None, - source: None, - ping_time: None, - ping_timeout: None, - burst_window_length: None, - max_messages_in_burst: None, - should_ghost: None, - ghost_sequence: None, - options: Some(HashMap::new()), - use_mock_connection: None, - mock_initial_value: None, - }; - assert_eq!(Config::load("client_config.json").unwrap(), cfg); + assert_eq!(Config::load("client_config.json").unwrap(), test_config()); } + #[test] + #[cfg(feature = "toml")] + fn load_from_toml() { + assert_eq!(Config::load("client_config.toml").unwrap(), test_config()); + } + + #[test] + #[cfg(feature = "yaml")] + fn load_from_yaml() { + assert_eq!(Config::load("client_config.yaml").unwrap(), test_config()); + } #[test] fn is_owner() { diff --git a/src/lib.rs b/src/lib.rs index 467d6ef..823ab96 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,12 +15,17 @@ extern crate native_tls; extern crate serde; #[macro_use] extern crate serde_derive; +#[cfg(feature = "json")] extern crate serde_json; +#[cfg(feature = "yaml")] +extern crate serde_yaml; extern crate tokio_core; extern crate tokio_io; extern crate tokio_mockstream; extern crate tokio_timer; extern crate tokio_tls; +#[cfg(feature = "toml")] +extern crate toml; pub mod client; pub mod error;