Idea on how `configure` might work.

pull/3/head
Joshua Potter 2021-12-29 11:49:24 -05:00
parent 071c461074
commit 8f3acba5e9
7 changed files with 314 additions and 77 deletions

View File

@ -12,4 +12,4 @@ 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"
yaml-rust = "0.4" yaml-rust = "0.4.4"

View File

@ -18,16 +18,16 @@ TODO
## Configuration ## Configuration
Homesync uses a YAML file, to be found in anyone of the following locations. Homesync uses a YAML file, to be found in anyone of the following locations.
Locations are searched in the following order: Locations are searched in the following priority:
- `$XDG_CONFIG_HOME/homesync/homesync.yml`
- `$XDG_CONFIG_HOME/homesync.yml`
- `$HOME/.config/homesync/homesync.yml`
- `$HOME/.homesync.yml` - `$HOME/.homesync.yml`
- `$HOME/.config/homesync/homesync.yml`
- `$XDG_CONFIG_HOME/homesync.yml`
- `$XDG_CONFIG_HOME/homesync/homesync.yml`
That said, it is recommended to modify this config solely from the exposed That said, it is recommended to modify this config solely from the exposed
homesync CLI. Homesync will take responsibility ensuring how the config is homesync CLI. Homesync will take responsibility ensuring the generated
modified based on your package manager, platform, etc. configuration is according to package manager, platform, etc.
## Usage ## Usage

View File

@ -1,9 +1,10 @@
remote: remote:
system: owner: jrpotter
name: home-config
packages: packages:
homesync: homesync:
configs: configs:
- $XDG_CONFIG_HOME/homesync/homesync.yml
- $XDG_CONFIG_HOME/homesync.yml
- $HOME/.config/homesync/homesync.yml
- $HOME/.homesync.yml - $HOME/.homesync.yml
- $HOME/.config/homesync/homesync.yml
- $XDG_CONFIG_HOME/homesync.yml
- $XDG_CONFIG_HOME/homesync/homesync.yml

1
src/homesync.rs Normal file
View File

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

259
src/homesync/config.rs Normal file
View File

@ -0,0 +1,259 @@
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,57 +1,28 @@
use std::env; mod homesync;
use homesync::config;
use std::error::Error; use std::error::Error;
use std::fs;
use std::io;
use std::path::PathBuf;
use yaml_rust::{Yaml, YamlLoader};
fn read_config(path: &PathBuf) -> io::Result<Option<String>> { pub fn run_configure(_matches: &clap::ArgMatches) -> Result<(), Box<dyn Error>> {
match fs::read_to_string(path) { // Check if we already have a local config somewhere. If so, reprompt the
Err(err) => match err.kind() { // same configuration options and override the values present in the current
// Ignore not found since we may try multiple paths. // YAML file.
io::ErrorKind::NotFound => Ok(None), let _config = match config::find_config() {
_ => Err(err), Ok(conf) => Ok(conf),
}, Err(config::Error::MissingConfig) => Ok(config::generate_config()),
Ok(contents) => Ok(Some(contents)), Err(config::Error::WithFile(e)) => Err(e),
}
}
fn find_config() -> Result<Vec<Yaml>, Box<dyn Error>> {
let mut paths: Vec<PathBuf> = Vec::new();
if let Ok(xdg_config_home) = env::var("XDG_CONFIG_HOME") {
paths.push(
[&xdg_config_home, "homesync", "homesync.yml"]
.iter()
.collect(),
);
paths.push([&xdg_config_home, "homesync.yml"].iter().collect());
}
if let Ok(home) = env::var("HOME") {
paths.push(
[&home, ".config", "homesync", "homesync.yml"]
.iter()
.collect(),
);
paths.push([&home, ".homesync.yml"].iter().collect());
}
for path in paths {
if let Ok(Some(contents)) = read_config(&path) {
return Ok(YamlLoader::load_from_str(&contents)?);
}
}
Err(Box::new(io::Error::new(
io::ErrorKind::NotFound,
"Could not find a homesync config.",
)))
}
pub fn run(config: Option<String>) -> Result<(), Box<dyn Error>> {
let _loaded = match config {
Some(path) => {
let contents = fs::read_to_string(path)?;
YamlLoader::load_from_str(&contents)?
}
None => find_config()?,
}; };
Ok(()) Ok(())
} }
pub fn run_push(_matches: &clap::ArgMatches) -> Result<(), Box<dyn Error>> {
Ok(())
}
pub fn run_pull(_matches: &clap::ArgMatches) -> Result<(), Box<dyn Error>> {
Ok(())
}
pub fn run_add(_matches: &clap::ArgMatches) -> Result<(), Box<dyn Error>> {
Ok(())
}

View File

@ -1,17 +1,22 @@
use clap::Parser; use clap::{App, AppSettings};
use std::process;
#[derive(Parser, Debug)]
#[clap(about, version, author)]
struct Args {
#[clap(short, long)]
config: Option<String>,
}
fn main() { fn main() {
let args = Args::parse(); let matches = App::new("homesync")
homesync::run(args.config).unwrap_or_else(|err| { .about("Cross desktop configuration sync tool.")
eprintln!("Problem parsing arguments: {}", err); .version("0.1.0")
process::exit(1); .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."))
.get_matches();
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(),
_ => unreachable!(),
}
} }