use clap::Parser;
use serde_yaml::{Mapping, Value};
use std::{fs, io::ErrorKind, path::Path};
const SUPPORTED_ENDINGS: [&str; 2] = ["yaml", "yml"];
#[derive(Debug, Parser)]
#[clap(author, version, about)]
pub struct Command {
#[clap()]
pub root: String,
#[clap()]
pub output: Option<String>,
}
impl Command {
pub fn run(&self) -> Result<(), Error> {
let path = self.root.clone();
let output = get_yaml_for_dir(path)?;
match &self.output {
None => println!("{}", output),
Some(path) => {
fs::write(path, output)
.map_err(|error| Error::handle_io_error(path.clone(), error))?;
}
};
Ok(())
}
}
pub fn get_yaml_for_dir(path: String) -> Result<String, Error> {
check_valid_dir(path.clone())?;
let value = get_value_for_dir(path)?;
Ok(serde_yaml::to_string(&value).unwrap())
}
fn check_valid_dir(string_path: String) -> Result<(), Error> {
let path = Path::new(&string_path);
let metadata = path
.metadata()
.map_err(|error| Error::handle_io_error(string_path.clone(), error))?;
if !metadata.is_dir() {
return Err(Error::IsNotADirectory(string_path.clone()));
}
Ok(())
}
fn get_value_for_dir(path: String) -> Result<Value, Error> {
let map = get_map_for_dir(path)?;
Ok(Value::Mapping(map))
}
fn get_map_for_dir(path: String) -> Result<Mapping, Error> {
let paths: Vec<std::path::PathBuf> = fs::read_dir(path)
.unwrap()
.map(|res| res.unwrap())
.map(|entry| entry.path())
.collect();
let mut map = Mapping::new();
for path in paths {
let metadata = path
.metadata()
.map_err(|error| Error::handle_io_error(path.display().to_string(), error))?;
if let Some(raw_file_name) = path.file_stem() {
let file_name = Value::String(raw_file_name.to_str().unwrap().to_string());
if metadata.is_dir() {
let value = get_value_for_dir(path.display().to_string())?;
map.insert(file_name, value);
continue;
} else if metadata.is_file() {
if let Some(extension) = path.extension() {
for ending in SUPPORTED_ENDINGS {
if extension.eq(ending) {
let raw_content =
fs::read_to_string(path.clone()).map_err(|error| {
Error::handle_io_error(path.display().to_string(), error)
})?;
let value: Value =
serde_yaml::from_str(&raw_content).map_err(|error| {
Error::handle_deserialize_error(
path.display().to_string(),
error,
)
})?;
map.insert(file_name, value);
break;
}
}
}
}
}
}
Ok(map)
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("`{0}` does not exist")]
DoesNotExist(String),
#[error("Insufficient permissions to read `{0}`")]
InsufficientPermissions(String),
#[error("`{0}` is not a directory")]
IsNotADirectory(String),
#[error("Experienced IO Error when trying to access `{0}`: {1}")]
IOError(String, String),
#[error("Experienced an error while deserializing `{0}`: {1}")]
DeserializeError(String, String),
}
impl Error {
pub fn handle_io_error(path: String, error: std::io::Error) -> Error {
match error.kind() {
ErrorKind::NotFound => Error::DoesNotExist(path),
ErrorKind::PermissionDenied => Error::InsufficientPermissions(path),
_ => Error::IOError(path, error.to_string()),
}
}
pub fn handle_deserialize_error(path: String, error: serde_yaml::Error) -> Error {
Error::DeserializeError(path, format!("{}", error))
}
}