Consolidate config functions and keep `Config` values loose.

pull/3/head
Joshua Potter 2022-01-01 09:24:01 -05:00
parent 223dfaf8a0
commit d4183f2b52
7 changed files with 159 additions and 198 deletions

View File

@ -1,5 +1,6 @@
---
remote: https://github.com/jrpotter/home-config.git
local: ""
remote: "https://github.com/jrpotter/home-config.git"
packages:
homesync:
configs:

View File

@ -1,142 +0,0 @@
use super::config::PathConfig;
use super::{config, git, path};
use ansi_term::Colour::{Green, Yellow};
use std::env::VarError;
use std::io::Write;
use std::path::PathBuf;
use std::{error, fmt, io};
use url::{ParseError, Url};
// TODO(jrpotter): Use curses to make this module behave nicer.
// ========================================
// Error
// ========================================
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
pub enum Error {
ConfigError(config::Error),
IOError(io::Error),
ParseError(ParseError),
VarError(VarError),
}
impl From<config::Error> for Error {
fn from(err: config::Error) -> Error {
Error::ConfigError(err)
}
}
impl From<git::Error> for Error {
fn from(err: git::Error) -> Error {
match err {
git::Error::IOError(e) => Error::IOError(e),
git::Error::VarError(e) => Error::VarError(e),
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::IOError(err)
}
}
impl From<path::Error> for Error {
fn from(err: path::Error) -> Error {
match err {
path::Error::IOError(e) => Error::IOError(e),
path::Error::VarError(e) => Error::VarError(e),
}
}
}
impl From<ParseError> for Error {
fn from(err: ParseError) -> Error {
Error::ParseError(err)
}
}
impl From<VarError> for Error {
fn from(err: VarError) -> Error {
Error::VarError(err)
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::ConfigError(e) => write!(f, "{}", e),
Error::IOError(e) => write!(f, "{}", e),
Error::ParseError(e) => write!(f, "{}", e),
Error::VarError(e) => write!(f, "{}", e),
}
}
}
impl error::Error for Error {}
// ========================================
// Prompts
// ========================================
fn prompt_local(config: &PathConfig) -> Result<PathBuf> {
print!(
"Local git repository <{}> (enter to continue): ",
Yellow.paint(
config
.1
.local
.as_ref()
.map_or("".to_owned(), |v| v.display().to_string())
)
);
io::stdout().flush()?;
let mut local = String::new();
io::stdin().read_line(&mut local)?;
Ok(PathBuf::from(path::expand_env(&local.trim())?))
}
fn prompt_remote(config: &PathConfig) -> Result<Url> {
print!(
"Remote git repository <{}> (enter to continue): ",
Yellow.paint(config.1.remote.to_string())
);
io::stdout().flush()?;
let mut remote = String::new();
io::stdin().read_line(&mut remote)?;
Ok(Url::parse(&remote)?)
}
// ========================================
// CLI
// ========================================
pub fn write_config(mut pending: PathConfig) -> Result<()> {
println!(
"Generating config at {}...\n",
Green.paint(pending.0.unresolved().display().to_string())
);
let local = prompt_local(&pending)?;
let remote = prompt_remote(&pending)?;
// Try to initialize the local respository if we can.
let resolved = git::init(&local, &pending)?;
pending.1.local = Some(resolved);
pending.1.remote = remote;
pending.write()?;
println!("\nFinished writing configuration file.");
Ok(())
}
pub fn list_packages(config: PathConfig) {
println!(
"Listing packages in {}...\n",
Green.paint(config.0.unresolved().display().to_string())
);
// Alphabetical ordered ensured by B-tree implementation.
for (k, _) in config.1.packages {
println!("{}", k);
}
}

View File

@ -1,10 +1,13 @@
use super::path;
use super::path::ResPathBuf;
use ansi_term::Colour::{Green, Yellow};
use serde_derive::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::env::VarError;
use std::io::Write;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::{error, fmt, fs, io};
use url::Url;
use url::{ParseError, Url};
// ========================================
// Error
@ -14,14 +17,16 @@ pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
pub enum Error {
FileError(io::Error),
IOError(io::Error),
MissingConfig,
ParseError(ParseError),
SerdeError(serde_yaml::Error),
VarError(VarError),
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::FileError(err)
Error::IOError(err)
}
}
@ -31,12 +36,35 @@ impl From<serde_yaml::Error> for Error {
}
}
impl From<path::Error> for Error {
fn from(err: path::Error) -> Error {
match err {
path::Error::IOError(e) => Error::IOError(e),
path::Error::VarError(e) => Error::VarError(e),
}
}
}
impl From<ParseError> for Error {
fn from(err: ParseError) -> Error {
Error::ParseError(err)
}
}
impl From<VarError> for Error {
fn from(err: VarError) -> Error {
Error::VarError(err)
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::FileError(e) => write!(f, "{}", e),
Error::IOError(e) => write!(f, "{}", e),
Error::MissingConfig => write!(f, "Could not find configuration file"),
Error::ParseError(e) => write!(f, "{}", e),
Error::SerdeError(e) => write!(f, "{}", e),
Error::VarError(e) => write!(f, "{}", e),
}
}
}
@ -54,7 +82,7 @@ pub struct Package {
#[derive(Debug, Deserialize, Serialize)]
pub struct Config {
pub local: Option<ResPathBuf>,
pub local: PathBuf,
pub remote: Url,
pub packages: BTreeMap<String, Package>,
}
@ -69,15 +97,8 @@ impl Config {
pub struct PathConfig(pub ResPathBuf, pub Config);
impl PathConfig {
pub fn new(path: &ResPathBuf, config: Option<Config>) -> Self {
PathConfig(
path.clone(),
config.unwrap_or(Config {
local: None,
remote: Url::parse("http://github.com/user/repo.git").unwrap(),
packages: BTreeMap::new(),
}),
)
pub fn new(path: &ResPathBuf, config: Config) -> Self {
PathConfig(path.clone(), config)
}
// TODO(jrpotter): Create backup file before overwriting.
@ -110,10 +131,10 @@ pub fn load(candidates: &Vec<ResPathBuf>) -> Result<PathConfig> {
for candidate in candidates {
match fs::read_to_string(candidate) {
Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
Err(err) => return Err(Error::FileError(err)),
Err(err) => Err(Error::IOError(err))?,
Ok(contents) => {
let config = Config::new(&contents)?;
return Ok(PathConfig::new(candidate, Some(config)));
return Ok(PathConfig::new(candidate, config));
}
}
}
@ -125,3 +146,85 @@ pub fn reload(config: &PathConfig) -> Result<PathConfig> {
println!("Configuration reloaded.");
load(&vec![config.0.clone()])
}
// ========================================
// Creation
// ========================================
fn prompt_local(path: Option<&Path>) -> Result<PathBuf> {
let default = path.map_or("$HOME/.homesync".to_owned(), |p| p.display().to_string());
print!(
"Local git repository <{}> (enter to continue): ",
Yellow.paint(&default)
);
io::stdout().flush()?;
let mut local = String::new();
io::stdin().read_line(&mut local)?;
// Defer validation this path until initialization of the repository.
let local = local.trim();
if local.is_empty() {
Ok(PathBuf::from(default))
} else {
Ok(PathBuf::from(local))
}
}
fn prompt_remote(url: Option<&Url>) -> Result<Url> {
let default = url.map_or("https://github.com/owner/repo.git".to_owned(), |u| {
u.to_string()
});
print!(
"Remote git repository <{}> (enter to continue): ",
Yellow.paint(&default)
);
io::stdout().flush()?;
let mut remote = String::new();
io::stdin().read_line(&mut remote)?;
let remote = remote.trim();
if remote.is_empty() {
Ok(Url::parse(&default)?)
} else {
Ok(Url::parse(&remote)?)
}
}
pub fn write(path: &ResPathBuf, loaded: Option<Config>) -> Result<PathConfig> {
println!(
"Generating config at {}...\n",
Green.paint(path.unresolved().display().to_string())
);
let local = prompt_local(match &loaded {
Some(c) => Some(c.local.as_ref()),
None => None,
})?;
let remote = prompt_remote(match &loaded {
Some(c) => Some(&c.remote),
None => None,
})?;
let generated = PathConfig(
path.clone(),
Config {
local,
remote,
packages: loaded.map_or(BTreeMap::new(), |c| c.packages),
},
);
generated.write()?;
println!("\nFinished writing configuration file.");
Ok(generated)
}
// ========================================
// Listing
// ========================================
pub fn list_packages(config: PathConfig) {
println!(
"Listing packages in {}...\n",
Green.paint(config.0.unresolved().display().to_string())
);
// Alphabetical ordered ensured by B-tree implementation.
for (k, _) in config.1.packages {
println!("{}", k);
}
}

View File

@ -53,33 +53,9 @@ impl error::Error for Error {}
// Validation
// ========================================
fn validate_is_file(path: &Path) -> Result<()> {
let metadata = fs::metadata(path)?;
if !metadata.is_file() {
// TODO(jrpotter): Use `IsADirectory` when stable.
Err(io::Error::new(
io::ErrorKind::Other,
format!("'{}' is not a file.", path.display()),
))?;
}
Ok(())
}
fn validate_is_dir(path: &Path) -> Result<()> {
let metadata = fs::metadata(path)?;
if !metadata.is_dir() {
// TODO(jrpotter): Use `NotADirectory` when stable.
Err(io::Error::new(
io::ErrorKind::Other,
format!("'{}' is not a directory.", path.display()),
))?;
}
Ok(())
}
pub fn validate_local(path: &Path) -> Result<()> {
let resolved = path::resolve(path)?;
validate_is_dir(resolved.as_ref())?;
path::validate_is_dir(resolved.as_ref())?;
let mut local: PathBuf = resolved.into();
local.push(".git");
@ -92,7 +68,7 @@ pub fn validate_local(path: &Path) -> Result<()> {
),
)
})?;
validate_is_dir(local.as_ref())?;
path::validate_is_dir(local.as_ref())?;
local.pop();
local.push(".homesync");
@ -105,7 +81,7 @@ pub fn validate_local(path: &Path) -> Result<()> {
),
)
})?;
validate_is_file(local.as_ref())?;
path::validate_is_file(local.as_ref())?;
// TODO(jrpotter): Verify git repository is pointing to remote.

View File

@ -1,4 +1,3 @@
pub mod cli;
pub mod config;
pub mod daemon;
pub mod git;
@ -22,7 +21,7 @@ pub fn run_daemon(config: PathConfig, freq_secs: u64) -> Result<(), Box<dyn Erro
pub fn run_init(candidates: Vec<ResPathBuf>) -> Result<(), Box<dyn Error>> {
debug_assert!(!candidates.is_empty(), "Empty candidates found in `init`.");
if candidates.is_empty() {
Err(config::Error::FileError(io::Error::new(
Err(config::Error::IOError(io::Error::new(
io::ErrorKind::NotFound,
"No suitable config file found.",
)))?;
@ -31,8 +30,8 @@ pub fn run_init(candidates: Vec<ResPathBuf>) -> 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.
Ok(pending) => {
cli::write_config(pending)?;
Ok(loaded) => {
config::write(&loaded.0, Some(loaded.1))?;
Ok(())
}
// Otherwise create a new config file at the given location. We always
@ -42,8 +41,7 @@ pub fn run_init(candidates: Vec<ResPathBuf>) -> Result<(), Box<dyn Error>> {
// TODO(jrpotter): Verify I have permission to write at specified path.
// Make directories if necessary.
Err(config::Error::MissingConfig) if !candidates.is_empty() => {
let pending = PathConfig::new(&candidates[0], None);
cli::write_config(pending)?;
config::write(&candidates[0], None)?;
Ok(())
}
Err(e) => Err(e)?,
@ -51,7 +49,7 @@ pub fn run_init(candidates: Vec<ResPathBuf>) -> Result<(), Box<dyn Error>> {
}
pub fn run_list(config: PathConfig) -> Result<(), config::Error> {
cli::list_packages(config);
config::list_packages(config);
Ok(())
}

View File

@ -60,9 +60,6 @@ fn dispatch(matches: clap::ArgMatches) -> Result<(), Box<dyn Error>> {
// used, even if one of higher priority is eventually defined.
subcommand => {
let config = homesync::config::load(&candidates)?;
if let Some(local) = &config.1.local {
homesync::git::validate_local(local.as_ref())?;
}
match subcommand {
Some(("add", _)) => Ok(homesync::run_add(config)?),
Some(("daemon", matches)) => {

View File

@ -6,7 +6,7 @@ use std::env::VarError;
use std::ffi::OsString;
use std::hash::{Hash, Hasher};
use std::path::{Component, Path, PathBuf};
use std::{env, error, fmt, io, result, str};
use std::{env, error, fmt, fs, io, result, str};
// ========================================
// Error
@ -171,6 +171,34 @@ impl<'de> Deserialize<'de> for ResPathBuf {
}
}
// ========================================
// Validation
// ========================================
pub fn validate_is_file(path: &Path) -> Result<()> {
let metadata = fs::metadata(path)?;
if !metadata.is_file() {
// TODO(jrpotter): Use `IsADirectory` when stable.
Err(io::Error::new(
io::ErrorKind::Other,
format!("'{}' is not a file.", path.display()),
))?;
}
Ok(())
}
pub fn validate_is_dir(path: &Path) -> Result<()> {
let metadata = fs::metadata(path)?;
if !metadata.is_dir() {
// TODO(jrpotter): Use `NotADirectory` when stable.
Err(io::Error::new(
io::ErrorKind::Other,
format!("'{}' is not a directory.", path.display()),
))?;
}
Ok(())
}
// ========================================
// Resolution
// ========================================