Use serde to avoid manual deserializing.

pull/3/head
Joshua Potter 2021-12-29 21:47:34 -05:00
parent 8f3acba5e9
commit 00dbfab415
6 changed files with 163 additions and 279 deletions

View File

@ -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"

118
src/config.rs Normal file
View File

@ -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)
}

View File

@ -1 +0,0 @@
pub mod config;

View File

@ -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(),
}
}

View File

@ -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<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
// 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<dyn Error>> {

View File

@ -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")
.about("Cross desktop configuration sync tool.")
.version("0.1.0")
.setting(AppSettings::SubcommandRequiredElseHelp)
.author("Joshua Potter <jrpotter.github.io>")
.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!(),
}
}