diff --git a/Cargo.toml b/Cargo.toml index 4361c44..e1bc662 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,4 +12,7 @@ edition = "2021" [dependencies] clap = { version = "3.0.0-rc.9", features = ["derive"] } notify = "4.0.16" +serde = "1.0" +serde_derive = "1.0.132" +serde_yaml = "0.8" yaml-rust = "0.4.4" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..c22a762 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,118 @@ +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::env; +use std::error; +use std::fmt; +use std::fs; +use std::io; +use std::path::PathBuf; + +// ======================================== +// Error +// ======================================== + +type Result = std::result::Result; + +#[derive(Debug)] +pub enum Error { + FileError(io::Error), + MissingConfig, + SerdeError(serde_yaml::Error), +} + +impl From for Error { + fn from(err: io::Error) -> Error { + Error::FileError(err) + } +} + +impl From for Error { + fn from(err: serde_yaml::Error) -> Error { + Error::SerdeError(err) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::FileError(e) => write!(f, "{}", e), + Error::MissingConfig => write!(f, "Could not find configuration file"), + Error::SerdeError(e) => write!(f, "{}", e), + } + } +} + +impl error::Error for Error {} + +// ======================================== +// Config +// ======================================== + +#[derive(Debug, Deserialize, Serialize)] +pub struct Remote { + pub owner: String, + pub name: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Package { + pub configs: Vec, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Config { + pub remote: Remote, + pub packages: HashMap, +} + +impl Config { + pub fn new(contents: &str) -> Result { + Ok(serde_yaml::from_str(&contents)?) + } +} + +// ======================================== +// Public +// ======================================== + +/// Returns the default configuration files `homesync` looks for. +/// +/// - `$HOME/.homesync.yml` +/// - `$HOME/.config/homesync/homesync.yml` +/// - `$XDG_CONFIG_HOME/homesync.yml` +/// - `$XDG_CONFIG_HOME/homesync/homesync.yml` +/// +/// Returned `PathBuf`s are looked for in the above order. +pub fn default_configs() -> Vec { + let mut paths: Vec = Vec::new(); + if let Ok(home) = env::var("HOME") { + paths.extend_from_slice(&[ + [&home, ".homesync.yml"].iter().collect(), + [&home, ".config", "homesync", "homesync.yml"] + .iter() + .collect(), + ]); + } + if let Ok(xdg_config_home) = env::var("XDG_CONFIG_HOME") { + paths.extend_from_slice(&[ + [&xdg_config_home, "homesync.yml"].iter().collect(), + [&xdg_config_home, "homesync", "homesync.yml"] + .iter() + .collect(), + ]); + } + paths +} + +pub fn read_config(paths: &Vec) -> Result { + // When trying our paths, the only acceptable error is a `NotFound` file. + // Anything else should be surfaced to the end user. + for path in paths { + match fs::read_to_string(path) { + Err(err) if err.kind() == io::ErrorKind::NotFound => continue, + Err(err) => return Err(Error::FileError(err)), + Ok(contents) => return Ok(Config::new(&contents)?), + } + } + Err(Error::MissingConfig) +} diff --git a/src/homesync.rs b/src/homesync.rs deleted file mode 100644 index ef68c36..0000000 --- a/src/homesync.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod config; diff --git a/src/homesync/config.rs b/src/homesync/config.rs deleted file mode 100644 index 8412b63..0000000 --- a/src/homesync/config.rs +++ /dev/null @@ -1,259 +0,0 @@ -use std::env; -use std::error; -use std::fmt; -use std::fs; -use std::io; -use std::path::{Path, PathBuf}; -use yaml_rust::{ScanError, Yaml, YamlLoader}; - -// ======================================== -// Error -// ======================================== - -pub type Result = std::result::Result; - -#[derive(Debug, Clone, Copy)] -pub enum Key { - Packages, // OPTIONAL - Remote, // REQUIRED - RemoteName, // REQUIRED - RemoteOwner, // REQUIRED -} - -impl Key { - fn to_yaml(&self) -> Yaml { - Yaml::String(self.to_string()) - } -} - -impl fmt::Display for Key { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Key::Packages => write!(f, "packages"), - Key::Remote => write!(f, "remote"), - Key::RemoteName => write!(f, "name"), - Key::RemoteOwner => write!(f, "owner"), - } - } -} - -#[derive(Debug, Clone)] -pub enum ErrorKind { - // Indicates our top-level data structure isn't a dictionary. - InvalidHash, - // Indicates a required key was not found. - MissingKey(Key), - // Indicates multiple YAML documents were found within our file. - MultipleDocuments, - // Indicates no YAML documents were found within our file. - NoDocument, - // Indicates there was a scan error when parsing the YAML. - ScanError(ScanError), -} - -impl fmt::Display for ErrorKind { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - ErrorKind::InvalidHash => write!(f, "expected dictionary"), - ErrorKind::MissingKey(k) => write!(f, "missing key '{}'", k), - ErrorKind::MultipleDocuments => write!(f, "has multiple YAML documents"), - ErrorKind::NoDocument => write!(f, "has no YAML document"), - ErrorKind::ScanError(s) => s.fmt(f), - } - } -} - -#[derive(Debug, Clone)] -pub struct ErrorWithFile { - path: PathBuf, - kind: ErrorKind, -} - -impl ErrorWithFile { - fn new(path: &Path, kind: ErrorKind) -> Self { - ErrorWithFile { - path: path.to_path_buf(), - kind, - } - } -} - -impl fmt::Display for ErrorWithFile { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let ErrorWithFile { path, kind } = self; - write!( - f, - "File {} failed with error: {}", - path.to_str().ok_or(fmt::Error)?, - kind - ) - } -} - -#[derive(Debug, Clone)] -pub enum Error { - // Indicates we could not find the configuration file at all. - MissingConfig, - // Indicates an error occurred when reading the configuration file. - WithFile(ErrorWithFile), -} - -impl Error { - pub fn new(path: &Path, kind: ErrorKind) -> Self { - Error::WithFile(ErrorWithFile::new(path, kind)) - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Error::MissingConfig => write!( - f, - "\ - Could not find a valid configuration file. Looked in \ - \n\n- `$HOME/.homesync.yml` \ - \n- `$HOME/.config/homesync/homesync.yml` \ - \n- `$XDG_CONFIG_HOME/homesync.yml` \ - \n- `$XDG_CONFIG_HOME/homesync/homesync.yml` \ - \nin order." - ), - Error::WithFile(e) => write!(f, "{}", e), - } - } -} - -impl error::Error for Error {} - -// ======================================== -// Config -// ======================================== - -pub struct Remote { - pub owner: String, - pub name: String, -} - -pub struct Package { - pub name: String, - pub configs: Vec, -} - -pub struct Config { - pub path: PathBuf, - pub remote: Remote, - pub packages: Vec, -} - -impl Config { - pub fn new(path: &Path, contents: &str) -> Result { - if let Yaml::Hash(pairs) = get_document(path, contents)? { - let remote = pairs - .get(&Key::Remote.to_yaml()) - .ok_or(Error::new(path, ErrorKind::MissingKey(Key::Remote)))?; - let remote = parseRemote(path, remote)?; - let packages = pairs.get(&Key::Packages.to_yaml()).unwrap_or(&Yaml::Null); - let packages = parsePackages(path, packages)?; - // We intentionally ignore any other keys we may encounter. - Ok(Config { - path: path.to_path_buf(), - remote, - packages, - }) - } else { - Err(Error::new(path, ErrorKind::InvalidHash)) - } - } -} - -fn get_document(path: &Path, contents: &str) -> Result { - match YamlLoader::load_from_str(contents) { - Ok(mut docs) => { - if docs.len() > 1 { - Err(Error::new(path, ErrorKind::MultipleDocuments)) - } else if docs.is_empty() { - Err(Error::new(path, ErrorKind::NoDocument)) - } else { - Ok(docs.swap_remove(0)) - } - } - Err(e) => Err(Error::new(path, ErrorKind::ScanError(e))), - } -} - -// ======================================== -// Parsers -// ======================================== - -fn parseRemote(path: &Path, value: &Yaml) -> Result { - Ok(Remote { - owner: String::new(), - name: String::new(), - }) -} - -fn parsePackages(path: &Path, value: &Yaml) -> Result> { - Ok(Vec::new()) -} - -// ======================================== -// Public -// ======================================== - -/// Attempt to read in the project config in the following priorities: -/// -/// - `$HOME/.homesync.yml` -/// - `$HOME/.config/homesync/homesync.yml` -/// - `$XDG_CONFIG_HOME/homesync.yml` -/// - `$XDG_CONFIG_HOME/homesync/homesync.yml` -/// -/// Returns an error if a file does not exist in any of these locations or a -/// found file contains invalid YAML. -pub fn find_config() -> Result { - let mut paths: Vec = Vec::new(); - if let Ok(home) = env::var("HOME") { - paths.extend_from_slice(&[ - [&home, ".homesync.yml"].iter().collect(), - [&home, ".config", "homesync", "homesync.yml"] - .iter() - .collect(), - ]); - } - if let Ok(xdg_config_home) = env::var("XDG_CONFIG_HOME") { - paths.extend_from_slice(&[ - [&xdg_config_home, "homesync.yml"].iter().collect(), - [&xdg_config_home, "homesync", "homesync.yml"] - .iter() - .collect(), - ]); - } - // When trying our paths, the only acceptable error is a `NotFound` file. - // Anything else should be surfaced to the end user. - for path in paths { - if let Ok(Some(contents)) = read_optional_config(&path) { - return Ok(Config::new(&path, &contents)?); - } - } - Err(Error::MissingConfig) -} - -fn read_optional_config(path: &Path) -> io::Result> { - match fs::read_to_string(path) { - Err(err) => match err.kind() { - // Ignore `NotFound` since we want to try multiple paths. - io::ErrorKind::NotFound => Ok(None), - _ => Err(err), - }, - Ok(contents) => Ok(Some(contents)), - } -} - -pub fn generate_config() -> Config { - Config { - path: PathBuf::from(""), - remote: Remote { - owner: "".to_owned(), - name: "".to_owned(), - }, - packages: Vec::new(), - } -} diff --git a/src/lib.rs b/src/lib.rs index 70b5032..878a8fc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,18 +1,26 @@ -mod homesync; +pub mod config; -use homesync::config; use std::error::Error; +use std::path::PathBuf; -pub fn run_configure(_matches: &clap::ArgMatches) -> Result<(), Box> { +pub fn run_configure( + paths: Vec, + _matches: &clap::ArgMatches, +) -> Result<(), Box> { // Check if we already have a local config somewhere. If so, reprompt the // same configuration options and override the values present in the current // YAML file. - let _config = match config::find_config() { - Ok(conf) => Ok(conf), - Err(config::Error::MissingConfig) => Ok(config::generate_config()), - Err(config::Error::WithFile(e)) => Err(e), - }; - Ok(()) + match config::read_config(&paths) { + Ok(_) => { + print!("successfully read\n"); + Ok(()) + } + Err(config::Error::MissingConfig) => { + print!("missing config\n"); + Ok(()) + } + Err(e) => Err(Box::new(e)), + } } pub fn run_push(_matches: &clap::ArgMatches) -> Result<(), Box> { diff --git a/src/main.rs b/src/main.rs index 926ad88..4ec5dd9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,22 +1,37 @@ -use clap::{App, AppSettings}; +use clap::{App, AppSettings, Arg}; +use std::error::Error; +use std::path::PathBuf; -fn main() { +fn main() -> Result<(), Box> { let matches = App::new("homesync") .about("Cross desktop configuration sync tool.") .version("0.1.0") .setting(AppSettings::SubcommandRequiredElseHelp) .author("Joshua Potter ") - .subcommand(App::new("configure").about("Initialize the homesync local repository.")) - .subcommand(App::new("push").about("Push local repository to remote repository.")) - .subcommand(App::new("pull").about("Pull remote repository into local repository.")) - .subcommand(App::new("add").about("Add new configuration to local repository.")) + .arg( + Arg::new("config") + .short('c') + .long("config") + .value_name("FILE") + .help("Specify a configuration file to use in place of defaults") + .takes_value(true), + ) + .subcommand(App::new("configure").about("Initialize the homesync local repository")) + .subcommand(App::new("push").about("Push local repository to remote repository")) + .subcommand(App::new("pull").about("Pull remote repository into local repository")) + .subcommand(App::new("add").about("Add new configuration to local repository")) .get_matches(); + let configs = match matches.value_of("config") { + Some(path) => vec![PathBuf::from(path)], + None => homesync::config::default_configs(), + }; + match matches.subcommand() { - Some(("configure", ms)) => homesync::run_configure(ms).unwrap(), - Some(("push", ms)) => homesync::run_push(ms).unwrap(), - Some(("pull", ms)) => homesync::run_pull(ms).unwrap(), - Some(("add", ms)) => homesync::run_add(ms).unwrap(), + Some(("configure", ms)) => homesync::run_configure(configs, ms), + Some(("push", ms)) => homesync::run_push(ms), + Some(("pull", ms)) => homesync::run_pull(ms), + Some(("add", ms)) => homesync::run_add(ms), _ => unreachable!(), } }