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,
|
2024-06-26 16:46:58 -07:00
|
|
|
identifiers: Vec<String>,
|
2024-06-26 01:46:51 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
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()?,
|
2024-06-26 16:46:58 -07:00
|
|
|
identifiers: self.identifiers.clone(),
|
2024-06-26 01:46:51 -07:00
|
|
|
})
|
|
|
|
}
|
|
|
|
// TODO: improve error handling for parsing from raw TOML
|
|
|
|
|
|
|
|
/// Create a new `DotfileRaw` from a `toml::Table`, which is a `Vec<Value>`.
|
2024-06-27 13:30:29 -07:00
|
|
|
/// Will panic if the table does not contain the fields specified, except for 'strategy',
|
|
|
|
/// which is `symlink` by default.
|
2024-06-26 01:46:51 -07:00
|
|
|
fn from_table(table: Table) -> DotfileRaw {
|
|
|
|
let location = table
|
|
|
|
.get("location")
|
|
|
|
.ok_or("Missing 'location' field.")
|
|
|
|
.unwrap()
|
|
|
|
.as_str()
|
2024-06-27 13:30:29 -07:00
|
|
|
.expect("Location field must be a string pointing to a valid path!")
|
2024-06-26 01:46:51 -07:00
|
|
|
.to_string();
|
|
|
|
let destination = table
|
|
|
|
.get("destination")
|
|
|
|
.ok_or("Missing 'destination' field.")
|
|
|
|
.unwrap()
|
|
|
|
.as_str()
|
2024-06-27 13:30:29 -07:00
|
|
|
.expect("Destination field must be a string pointing to a valid path!")
|
2024-06-26 01:46:51 -07:00
|
|
|
.to_string();
|
2024-06-27 13:30:29 -07:00
|
|
|
let strategy = match table.get("strategy") {
|
|
|
|
Some(val) => val
|
|
|
|
.as_str()
|
|
|
|
.expect("Strategy field must be a string!")
|
|
|
|
.to_string(),
|
|
|
|
None => "symlink".to_string(),
|
|
|
|
};
|
2024-06-26 16:46:58 -07:00
|
|
|
let identifiers = table
|
|
|
|
.get("identifiers")
|
|
|
|
.ok_or("Missing 'identifiers' field.")
|
2024-06-26 01:46:51 -07:00
|
|
|
.unwrap()
|
|
|
|
.as_array()
|
2024-06-27 13:30:29 -07:00
|
|
|
.expect("Identifiers field must be an array of strings!")
|
2024-06-26 01:46:51 -07:00
|
|
|
.iter()
|
|
|
|
.map(|x| x.as_str().unwrap().to_string())
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
DotfileRaw {
|
|
|
|
location,
|
|
|
|
destination,
|
|
|
|
strategy,
|
2024-06-26 16:46:58 -07:00
|
|
|
identifiers,
|
2024-06-26 01:46:51 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// The configuration object parsed from the `config` field in `dartgun.toml`
|
|
|
|
#[derive(Debug)]
|
2024-06-26 16:46:58 -07:00
|
|
|
pub struct Machine {
|
2024-06-26 17:30:52 -07:00
|
|
|
pub identifiers: Vec<String>,
|
2024-06-26 01:46:51 -07:00
|
|
|
}
|
|
|
|
|
2024-06-26 16:46:58 -07:00
|
|
|
impl Machine {
|
2024-06-26 01:46:51 -07:00
|
|
|
// TODO: improve error handling for parsing config
|
|
|
|
|
|
|
|
/// Generate a `Config` from a raw `dartfile.toml`.
|
|
|
|
/// Will panic! if the format is invalid.
|
2024-06-26 16:46:58 -07:00
|
|
|
fn from_table(table: Table) -> Machine {
|
|
|
|
let identifiers = table
|
|
|
|
.get("identifiers")
|
2024-06-26 01:46:51 -07:00
|
|
|
.unwrap()
|
|
|
|
.as_array()
|
|
|
|
.unwrap()
|
|
|
|
.iter()
|
|
|
|
.map(|x| x.as_str().unwrap().to_string())
|
|
|
|
.collect();
|
2024-06-26 16:46:58 -07:00
|
|
|
Machine { identifiers }
|
2024-06-26 01:46:51 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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 {
|
2024-06-26 16:46:58 -07:00
|
|
|
pub machine: Machine,
|
2024-06-26 01:46:51 -07:00
|
|
|
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,
|
2024-06-26 16:46:58 -07:00
|
|
|
pub identifiers: Vec<String>,
|
2024-06-26 01:46:51 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
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.
|
2024-06-26 16:46:58 -07:00
|
|
|
pub fn parse(dartgun_path: &Path, machine_path: &Path) -> Dartfile {
|
|
|
|
let dartgun_toml = fs::read_to_string(dartgun_path)
|
|
|
|
.expect("Couldn't read dartgun.toml")
|
|
|
|
.parse::<Table>()
|
|
|
|
.expect("Couldn't parse the TOML.");
|
|
|
|
let machine_toml = fs::read_to_string(machine_path)
|
|
|
|
.expect("Couldn't read machine.toml")
|
|
|
|
.parse::<Table>()
|
|
|
|
.expect("Couldn't parse the TOML.");
|
|
|
|
|
|
|
|
let dots_raw = dartgun_toml.get("dots").unwrap().as_array().unwrap();
|
|
|
|
|
|
|
|
let machine = Machine::from_table(machine_toml);
|
2024-06-26 01:46:51 -07:00
|
|
|
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>>();
|
|
|
|
|
2024-06-26 16:46:58 -07:00
|
|
|
Dartfile { machine, dots }
|
2024-06-26 01:46:51 -07:00
|
|
|
}
|