initial implementation of functionality

This commit is contained in:
Valerie Wolfe 2023-08-08 10:59:55 -04:00
commit 3b7ea51e04
7 changed files with 283 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
Cargo.lock

12
Cargo.toml Normal file
View file

@ -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"

25
README.md Normal file
View file

@ -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

83
src/config.rs Normal file
View file

@ -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<String, Value>
}
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<Map<String, Value>> {
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
}
}

7
src/error.rs Normal file
View file

@ -0,0 +1,7 @@
use std::process::exit;
pub fn no_targets() {
println!("oink: configuration has no targets");
exit(2);
}

65
src/main.rs Normal file
View file

@ -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 <sleeplessval@gmail.com>
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'");
}

89
src/operation.rs Normal file
View file

@ -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<Map<String, Value>>) {
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<Map<String, Value>>, 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"); }
}
}