Use serde to avoid manual deserializing.
parent
8f3acba5e9
commit
00dbfab415
|
@ -12,4 +12,7 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "3.0.0-rc.9", features = ["derive"] }
|
clap = { version = "3.0.0-rc.9", features = ["derive"] }
|
||||||
notify = "4.0.16"
|
notify = "4.0.16"
|
||||||
|
serde = "1.0"
|
||||||
|
serde_derive = "1.0.132"
|
||||||
|
serde_yaml = "0.8"
|
||||||
yaml-rust = "0.4.4"
|
yaml-rust = "0.4.4"
|
||||||
|
|
|
@ -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<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
FileError(io::Error),
|
||||||
|
MissingConfig,
|
||||||
|
SerdeError(serde_yaml::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for Error {
|
||||||
|
fn from(err: io::Error) -> Error {
|
||||||
|
Error::FileError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_yaml::Error> 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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct Config {
|
||||||
|
pub remote: Remote,
|
||||||
|
pub packages: HashMap<String, Package>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn new(contents: &str) -> Result<Self> {
|
||||||
|
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<PathBuf> {
|
||||||
|
let mut paths: Vec<PathBuf> = 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<PathBuf>) -> Result<Config> {
|
||||||
|
// 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)
|
||||||
|
}
|
|
@ -1 +0,0 @@
|
||||||
pub mod config;
|
|
|
@ -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<T> = std::result::Result<T, Error>;
|
|
||||||
|
|
||||||
#[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<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Config {
|
|
||||||
pub path: PathBuf,
|
|
||||||
pub remote: Remote,
|
|
||||||
pub packages: Vec<Package>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
pub fn new(path: &Path, contents: &str) -> Result<Self> {
|
|
||||||
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<Yaml> {
|
|
||||||
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<Remote> {
|
|
||||||
Ok(Remote {
|
|
||||||
owner: String::new(),
|
|
||||||
name: String::new(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parsePackages(path: &Path, value: &Yaml) -> Result<Vec<Package>> {
|
|
||||||
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<Config> {
|
|
||||||
let mut paths: Vec<PathBuf> = 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<Option<String>> {
|
|
||||||
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(),
|
|
||||||
}
|
|
||||||
}
|
|
26
src/lib.rs
26
src/lib.rs
|
@ -1,18 +1,26 @@
|
||||||
mod homesync;
|
pub mod config;
|
||||||
|
|
||||||
use homesync::config;
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub fn run_configure(_matches: &clap::ArgMatches) -> Result<(), Box<dyn Error>> {
|
pub fn run_configure(
|
||||||
|
paths: Vec<PathBuf>,
|
||||||
|
_matches: &clap::ArgMatches,
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
// Check if we already have a local config somewhere. If so, reprompt the
|
// Check if we already have a local config somewhere. If so, reprompt the
|
||||||
// same configuration options and override the values present in the current
|
// same configuration options and override the values present in the current
|
||||||
// YAML file.
|
// YAML file.
|
||||||
let _config = match config::find_config() {
|
match config::read_config(&paths) {
|
||||||
Ok(conf) => Ok(conf),
|
Ok(_) => {
|
||||||
Err(config::Error::MissingConfig) => Ok(config::generate_config()),
|
print!("successfully read\n");
|
||||||
Err(config::Error::WithFile(e)) => Err(e),
|
Ok(())
|
||||||
};
|
}
|
||||||
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<dyn Error>> {
|
pub fn run_push(_matches: &clap::ArgMatches) -> Result<(), Box<dyn Error>> {
|
||||||
|
|
35
src/main.rs
35
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<dyn Error>> {
|
||||||
let matches = App::new("homesync")
|
let matches = App::new("homesync")
|
||||||
.about("Cross desktop configuration sync tool.")
|
.about("Cross desktop configuration sync tool.")
|
||||||
.version("0.1.0")
|
.version("0.1.0")
|
||||||
.setting(AppSettings::SubcommandRequiredElseHelp)
|
.setting(AppSettings::SubcommandRequiredElseHelp)
|
||||||
.author("Joshua Potter <jrpotter.github.io>")
|
.author("Joshua Potter <jrpotter.github.io>")
|
||||||
.subcommand(App::new("configure").about("Initialize the homesync local repository."))
|
.arg(
|
||||||
.subcommand(App::new("push").about("Push local repository to remote repository."))
|
Arg::new("config")
|
||||||
.subcommand(App::new("pull").about("Pull remote repository into local repository."))
|
.short('c')
|
||||||
.subcommand(App::new("add").about("Add new configuration to local repository."))
|
.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();
|
.get_matches();
|
||||||
|
|
||||||
|
let configs = match matches.value_of("config") {
|
||||||
|
Some(path) => vec![PathBuf::from(path)],
|
||||||
|
None => homesync::config::default_configs(),
|
||||||
|
};
|
||||||
|
|
||||||
match matches.subcommand() {
|
match matches.subcommand() {
|
||||||
Some(("configure", ms)) => homesync::run_configure(ms).unwrap(),
|
Some(("configure", ms)) => homesync::run_configure(configs, ms),
|
||||||
Some(("push", ms)) => homesync::run_push(ms).unwrap(),
|
Some(("push", ms)) => homesync::run_push(ms),
|
||||||
Some(("pull", ms)) => homesync::run_pull(ms).unwrap(),
|
Some(("pull", ms)) => homesync::run_pull(ms),
|
||||||
Some(("add", ms)) => homesync::run_add(ms).unwrap(),
|
Some(("add", ms)) => homesync::run_add(ms),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue