From 3b7ea51e047139a7ca565454a5d9545d2730c239 Mon Sep 17 00:00:00 2001 From: Valerie Date: Tue, 8 Aug 2023 10:59:55 -0400 Subject: [PATCH] initial implementation of functionality --- .gitignore | 2 ++ Cargo.toml | 12 +++++++ README.md | 25 ++++++++++++++ src/config.rs | 83 ++++++++++++++++++++++++++++++++++++++++++++ src/error.rs | 7 ++++ src/main.rs | 65 +++++++++++++++++++++++++++++++++++ src/operation.rs | 89 ++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 283 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/config.rs create mode 100644 src/error.rs create mode 100644 src/main.rs create mode 100644 src/operation.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f768a7b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "oink" +version = "0.0.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +#copy_dir = "0.1.3" +pico-args = "0.5.0" +tera = "1.19.0" +toml = "0.7.6" diff --git a/README.md b/README.md new file mode 100644 index 0000000..17a656e --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ + +# oink 🐷 + +A configuration file preprocessor written in Rust. + +## Usage + +```sh + +# process config files +oink build +# apply processed configs +oink apply + +# build and apply +oink full + +``` + +## Libraries + +- [pico-args](https://crates.io/crates/pico-args) — argument parsing +- [tera](https://crates.io/crates/tera) — template replacement +- [toml](https://crates.io/crates/toml) — configuration parsing + diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..a33950b --- /dev/null +++ b/src/config.rs @@ -0,0 +1,83 @@ +//! configuration struct and implementations + +use std::{ + collections::HashMap, + env::var, + fs::read_to_string, + path::PathBuf +}; + +use tera::Context; +use toml::{ + map::Map, + Value +}; + +/// configuration struct +pub struct Config { + pub dir: String, + inner: Map +} + +impl Config { + /// create a new Config using the file at '$HOME/.config/oink/oink.toml' + pub fn new() -> Config { + // get base config dir + let home = var("HOME").unwrap(); + let mut dir = PathBuf::from(&home); + dir.push(".config/oink"); + + // read toml file + let mut toml_path = dir.clone(); + toml_path.push("oink.toml"); + let raw = read_to_string(toml_path).unwrap(); + let toml_conf: Value = toml::from_str(&raw).unwrap(); + let inner = toml_conf.as_table().unwrap(); + + // return struct + Config { + dir: dir.to_string_lossy().to_string(), + inner: inner.to_owned() + } + } + + /// build tera context from "vars" and "colors" config sections + pub fn context(&self) -> Context { + let mut output = Context::new(); + + let vars = self.inner.get("vars"); + if vars.is_some() { + let vars = vars.unwrap().as_table().unwrap(); + for (key, value) in vars.iter() { + output.insert(key, value.as_str().unwrap()); + } + } + let colors = self.inner.get("colors"); + if colors.is_some() { + let colors = colors.unwrap().as_table().unwrap(); + let mut map = HashMap::<&str, &str>::new(); + for (key, value) in colors.iter() { + map.insert(key, value.as_str().unwrap()); + } + output.insert("colors", &map); + } + + output + } + + /// build array of targets from "target" array + pub fn targets(&self) -> Vec> { + let mut output = Vec::new(); + + let target_array: Option<&Value> = self.inner.get("target"); + if target_array.is_some() { + let target_array = target_array.unwrap().as_array().unwrap().to_owned(); + for target in target_array { + output.push(target.as_table().unwrap().to_owned()); + } + } + + output + } +} + diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..b2662a2 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,7 @@ +use std::process::exit; + +pub fn no_targets() { + println!("oink: configuration has no targets"); + exit(2); +} + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..159dc6a --- /dev/null +++ b/src/main.rs @@ -0,0 +1,65 @@ +use std::process::exit; + +use pico_args::Arguments; +use tera::Tera; + +mod config; +mod error; +mod operation; +use crate::config::Config; + +fn main() { + // get args + let mut args = Arguments::from_env(); + + let flag_help = args.contains(["-h", "--help"]); + let operation = args.subcommand().unwrap(); + if flag_help && operation.is_none() { help_text(); } + + // init configuration + let config = Config::new(); + + // tera init + let context = config.context(); + let template_dir = format!("{}/templates/*", &(config.dir)); + let mut tera = Tera::new(&template_dir).unwrap(); + + let targets = config.targets(); + if targets.len() == 0 { error::no_targets(); } + + match operation.as_deref() { + Some("apply") + => { + operation::apply(&targets); + }, + Some("build") + => { + operation::build(&targets, &mut tera, &context); + }, + Some("full") + => { + operation::build(&targets, &mut tera, &context); + operation::apply(&targets); + }, + _ + => { + help_text(); + exit(1); + } + } +} + +pub fn help_text() { + let version = env!("CARGO_PKG_VERSION"); + println!("oink v{version} +Valerie Wolfe +A configuration preprocessor written in Rust. + +usage: oink [operation] + +oink operations: + apply Copies the last built files to their destination paths. + build Runs oink preprocessing on configuration files. + full Runs 'build' and 'apply'"); +} + diff --git a/src/operation.rs b/src/operation.rs new file mode 100644 index 0000000..49dfc13 --- /dev/null +++ b/src/operation.rs @@ -0,0 +1,89 @@ +//! higher-level operation functions +use std::{ + env::var, + fs::{ self, File }, + io::Write, + path::Path +}; + +use toml::{ map::Map, Value }; +use tera::{ Context, Tera }; + +pub fn apply(targets: &Vec>) { + let home = var("HOME").unwrap(); + println!("running apply:"); + for target in targets { + // get path and name + let path = target.get("path"); + let i_name = target.get("name"); + + // handle empty properties gracefully + if path.is_none() { + if i_name.is_some() { + let name = i_name.unwrap().as_str().unwrap(); + println!(" \"{name}\" is missing its path property; skipping"); + } else { println!(" skipping empty target"); } + continue; + } + if i_name.is_none() { + println!(" target missing name; skipping"); + continue; + } + + // print applying text + let name = i_name.unwrap().as_str().unwrap(); + println!(" applying \"{name}\":"); + + // set up paths + let destination = Path::new(path.unwrap().as_str().unwrap()); + let source_path = format!("{home}/.config/oink/generated/{name}"); + let source = Path::new(&source_path); + + // copy and print + let result = fs::copy(source, destination); + if result.is_err() { println!(" failed to copy!"); } + else { println!(" completed successfully"); } + } +} + +pub fn build(targets: &Vec>, tera: &mut Tera, context: &Context) { + let home = var("HOME").unwrap(); + println!("running build:"); + for target in targets { + // get name property + let i_name = target.get("name"); + // handle empty names gracefully + if i_name.is_none() { + println!(" target missing name; skipping"); + continue; + } + + // print building text + let name = i_name.unwrap().as_str().unwrap(); + println!(" building \"{name}\":"); + + // render template + let render = tera.render(name, context); + // handle rendering errors gracefully + if render.is_err() { + println!(" failed to render template"); + continue; + } + + // get rendered text and open destination file + let output = render.unwrap(); + let destination = format!("{home}/.config/oink/generated/{name}"); + let path = Path::new(&destination); + let i_file = File::create(path); + if i_file.is_err() { + println!(" failed to create destination file at {path:?}"); + continue; + } + let mut file = i_file.unwrap(); + // write to destination file + let written = write!(&mut file, "{output}"); + if written.is_err() { println!(" failed to write to destination file at {path:?}"); } + else { println!(" completed successfully"); } + } +} +