dartgun/src/dartfile.rs

197 lines
5.9 KiB
Rust
Raw Normal View History

2024-06-26 01:46:51 -07:00
use std::fs;
use std::path::{Path, PathBuf};
use toml::Table;
/// The dartfile module handles parsing the `dotfile.toml` into
/// a well-typed and self-validating data structure.
/// The raw form of a dotfile entry. Paths are stored as strings.
///
/// Should not be used outside of module internals. Its primary purpose
/// is to provide an intermediary between the raw parsed TOML and
/// the strongly typed Dotfile struct
#[derive(Debug)]
struct DotfileRaw {
location: String,
destination: String,
strategy: String,
machines: Vec<String>,
applies_to: String,
}
impl DotfileRaw {
fn determine_strategy(&self) -> Result<Strategy, String> {
match self.strategy.as_str() {
"hardlink" => Ok(Strategy::Hardlink),
"symlink" => Ok(Strategy::Symlink),
_ => Err(String::from(
"Strategy must be either 'symlink' or 'hardlink'",
)),
}
}
fn to_dotfile(&self) -> Result<Dotfile, String> {
let location_path = PathBuf::from(&self.location);
let destination_path = PathBuf::from(&self.destination);
Ok(Dotfile {
location: location_path,
destination: destination_path,
strategy: self.determine_strategy()?,
machines: self.machines.clone(),
applies_to: self.applies_to.clone(),
})
}
// TODO: improve error handling for parsing from raw TOML
/// Create a new `DotfileRaw` from a `toml::Table`, which is a `Vec<Value>`.
/// Will panic if the table does not contain the fields specified.
fn from_table(table: Table) -> DotfileRaw {
let location = table
.get("location")
.ok_or("Missing 'location' field.")
.unwrap()
.as_str()
.unwrap()
.to_string();
let destination = table
.get("destination")
.ok_or("Missing 'destination' field.")
.unwrap()
.as_str()
.unwrap()
.to_string();
let strategy = table
.get("strategy")
.ok_or("Missing 'strategy' field.")
.unwrap()
.as_str()
.unwrap()
.to_string();
let machines = table
.get("machines")
.ok_or("Missing 'machines' field.")
.unwrap()
.as_array()
.unwrap()
.iter()
.map(|x| x.as_str().unwrap().to_string())
.collect();
let applies_to = table
.get("applies_to")
.ok_or("Missing 'applies_to' field.")
.unwrap()
.as_str()
.unwrap()
.to_string();
DotfileRaw {
location,
destination,
strategy,
machines,
applies_to,
}
}
}
/// The configuration object parsed from the `config` field in `dartgun.toml`
#[derive(Debug)]
pub struct Config {
machine: String,
available: Vec<String>,
}
impl Config {
// TODO: improve error handling for parsing config
/// Generate a `Config` from a raw `dartfile.toml`.
/// Will panic! if the format is invalid.
fn from_table(table: Table) -> Config {
let machine = table.get("machine").unwrap().as_str().unwrap().to_string();
let available = table
.get("available")
.unwrap()
.as_array()
.unwrap()
.iter()
.map(|x| x.as_str().unwrap().to_string())
.collect();
Config { available, machine }
}
}
/// A strongly typed Dartfile (aka `dartgun.toml`). Users of this
/// struct can assume that the dartfile is at least semantically valid
#[derive(Debug)]
pub struct Dartfile {
pub config: Config,
pub dots: Vec<Dotfile>,
}
impl Dartfile {
/// Validates the Dartfile by checking each Dotfile entry to ensure
/// the paths specified by `location` are accessible.
pub fn validate(&self) -> Result<(), String> {
for dotfile in self.dots.iter() {
if dotfile.validate().is_err() {
return Err("Invalid dotfile.".to_string());
}
}
Ok(())
}
}
/// Represents which strategy to use when deploying dotfiles across system.
#[derive(Debug)]
pub enum Strategy {
Hardlink,
Symlink,
}
/// A strongly-typed Dotfile entry
#[derive(Debug)]
pub struct Dotfile {
pub location: PathBuf,
pub destination: PathBuf,
pub strategy: Strategy,
pub machines: Vec<String>,
pub applies_to: String,
}
impl Dotfile {
/// Validates the entry by checking whether the `location` paths
/// specified are accessible.
pub fn validate(&self) -> Result<(), String> {
let path_exists = self.location.try_exists();
match path_exists {
Ok(true) => Ok(()),
Ok(false) => Err("Could not follow broken symlink.".to_string()),
Err(_) => Err("An error occurred. Does the path exist?".to_string()),
}
}
}
/// Takes a path to a `dartgun.toml` and produces a well-typed Dartfile object.
/// Currently crashes on any parse errors, but this behavior will likely change in the future.
pub fn parse(path: &Path) -> Dartfile {
let raw_data = fs::read_to_string(path).expect("Couldn't read the file.");
let value: Table = raw_data.parse::<Table>().expect("Couldn't parse the TOML.");
let config_raw = value.get("config").unwrap().as_table().unwrap();
let dots_raw = value.get("dots").unwrap().as_array().unwrap();
let config = Config::from_table(config_raw.clone());
let dots = dots_raw
.iter()
.map(|x| {
match DotfileRaw::from_table(x.as_table().unwrap().clone()).to_dotfile() {
Ok(dotfile) => dotfile,
Err(_) => panic!("An error has occurred parsing the `dartgun.toml` file. Please make sure it is in the correct format.")
}
})
.collect::<Vec<Dotfile>>();
Dartfile { config, dots }
}