Compare commits

..

4 commits

10 changed files with 78 additions and 228 deletions

View file

@ -1,15 +1,16 @@
[package] [package]
name = "oink" name = "oink"
version = "0.2.3" version = "0.0.2"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
#copy_dir = "0.1.3"
pico-args = "0.5.0" pico-args = "0.5.0"
tera = "1.19.0"
termion = "2.0.1" termion = "2.0.1"
toml = "0.7.6" toml = "0.7.6"
upon = "0.8.1"
[profile.release] [profile.release]
opt-level = "s" opt-level = "s"

View file

@ -41,6 +41,6 @@ path = "/home/test/.config/oink/oink.toml"
## Libraries ## Libraries
- [pico-args](https://crates.io/crates/pico-args) — argument parsing - [pico-args](https://crates.io/crates/pico-args) — argument parsing
- [termion](https://crates.io/crates/termion) — ANSI formatting - [tera](https://crates.io/crates/tera) — template engine
- [toml](https://crates.io/crates/toml) — configuration parsing - [toml](https://crates.io/crates/toml) — configuration parsing
- [upon](https://crates.io/crates/upon) — template engine

View file

@ -1,34 +0,0 @@
.Dd $Mdocdate$
.Dt OINK 1
.Os
.Sh NAME
.Nm oink
.Nd a configuration file preprocessor
.Sh SYNOPSIS
.Nm
.Ar command
.Sh DESCRIPTION
.Nm
is a configuration file preprocessor focused on centralizing configuration changes.
.Ss COMMANDS
.Bl -tag -width Ds
.It apply
Copies the last generated files to their target paths.
.It build
Generates files from their templates.
.It full
Runs 'build', then 'apply'.
.El
.Sh EXIT STATUS
.Bl -tag -width Ds
.It 1
No command was given.
.It 2
The configuration file has no items in the target array.
.It 3
Failed to create the configuration directories.
.El
.Sh AUTHORS
.An -nosplit
.An Valerie Wolfe Aq Mt sleeplessval@gmail.com

4
rust-toolchain.toml Normal file
View file

@ -0,0 +1,4 @@
[toolchain]
channel = "nightly"

View file

@ -1,7 +1,7 @@
//! configuration struct and implementations //! configuration struct and implementations
use std::{ use std::{
collections::BTreeMap, collections::HashMap,
env::var, env::var,
fs::{ create_dir, read_to_string, File }, fs::{ create_dir, read_to_string, File },
io::Error, io::Error,
@ -9,16 +9,13 @@ use std::{
process::exit process::exit
}; };
use upon::Value as ContextValue; use tera::Context;
use toml::{ use toml::{
map::Map, map::Map,
Value Value
}; };
use crate::{ error, util }; use crate::error;
pub type Context = BTreeMap<String, ContextValue>;
pub type Table = toml::map::Map<String, Value>;
/// configuration struct /// configuration struct
pub struct Config { pub struct Config {
@ -56,53 +53,26 @@ impl Config {
} }
} }
/// build context from "vars" and "colors" config sections /// build tera context from "vars" and "colors" config sections
pub fn context(&self, target: &Table) -> Context { pub fn context(&self) -> Context {
let mut output = Context::new(); let mut output = Context::new();
let mut def: Vec<ContextValue> = Vec::new();
// pull global vars let vars = self.inner.get("vars");
if let Some(Value::Table(vars)) = self.inner.get("vars") { if vars.is_some() {
let vars = vars.unwrap().as_table().unwrap();
for (key, value) in vars.iter() { for (key, value) in vars.iter() {
let key = key.to_owned(); output.insert(key, value.as_str().unwrap());
if let Some(value) = util::convert(value) {
output.insert(key.clone(), value);
def.push(ContextValue::String(key));
} }
} }
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);
// pull target values
for (key, value) in target.iter() {
if key.to_uppercase() == *key {
if let Some(value) = util::convert(value) {
let key = key.to_owned();
output.insert(key.clone(), value);
def.push(ContextValue::String(key));
} }
}
}
// pull palette
let palette_name: Option<String> =
if let Some(Value::String(name)) = target.get("use_palette") { Some(name.clone()) }
else if let Some(Value::String(name)) = self.inner.get("use_palette") { Some(name.clone()) }
else { None };
if let Some(Value::Array(array)) = self.inner.get("palette") {
let palette = util::matches(array.to_owned(), palette_name.unwrap_or("default".to_string()));
if let Some(Value::Table(palette)) = palette {
let colors = Context::new();
for(key, value) in palette.iter() {
output.insert(key.to_owned(), value.as_str().unwrap().into());
}
let key = "palette".to_string();
output.insert(key.clone(), colors.into());
def.push(ContextValue::String(key));
}
}
// insert set vars list
output.insert("def".to_string(), def.into());
output output
} }

View file

@ -3,11 +3,6 @@ use std::{
process::exit process::exit
}; };
pub fn no_command() {
crate::help_text();
exit(1);
}
pub fn no_targets() { pub fn no_targets() {
println!("oink: configuration has no targets"); println!("oink: configuration has no targets");
exit(2); exit(2);

View file

@ -1,7 +0,0 @@
use upon::Value;
pub fn has(list: Vec<Value>, key: String) -> bool {
return list.contains(&Value::String(key));
}

View file

@ -1,12 +1,12 @@
#![feature(error_in_core)]
use std::process::exit;
use pico_args::Arguments; use pico_args::Arguments;
use tera::Tera;
mod config; mod config;
mod error; mod error;
mod filter;
mod operation; mod operation;
mod util;
use crate::config::Config; use crate::config::Config;
fn main() { fn main() {
@ -20,27 +20,33 @@ fn main() {
// init configuration // init configuration
let config = Config::new(); let config = Config::new();
// build template dir // tera init
let template_dir = format!("{}/templates/", &(config.dir)); let context = config.context();
let template_dir = format!("{}/templates/*", &(config.dir));
let mut tera = Tera::new(&template_dir).unwrap();
let targets = config.targets(); let targets = config.targets();
if targets.len() == 0 { error::no_targets(); } if targets.len() == 0 { error::no_targets(); }
match operation.as_deref() { match operation.as_deref() {
Some("apply") Some("apply")
=> operation::apply(&targets),
Some("build")
=> operation::build(&targets, template_dir, &config),
Some("full")
=> { => {
operation::build(&targets, template_dir, &config);
println!();
operation::apply(&targets); operation::apply(&targets);
}, },
Some("build")
_ => error::no_command() => {
operation::build(&targets, &mut tera, &context);
},
Some("full")
=> {
operation::build(&targets, &mut tera, &context);
operation::apply(&targets);
},
_
=> {
help_text();
exit(1);
}
} }
} }

View file

@ -1,41 +1,28 @@
//! higher-level operation functions //! higher-level operation functions
use std::{ use std::{
env::var, env::var,
fs::{self, read_to_string, File }, fs::{ self, File },
io::Write, io::Write,
path::{ Path, PathBuf }, path::Path
time::SystemTime
}; };
use core::error::Error;
use tera::{ Context, Tera };
use termion::{ use termion::{
color::{ self, Fg }, color::{ self, Fg },
style::{ style
Bold as BOLD,
Faint as FAINT,
Italic as ITALIC,
Reset as RESET
}
}; };
use toml::{ map::Map, Value }; use toml::{ map::Map, Value };
use upon::Engine;
use crate::{ static FAIL: Fg<color::LightRed> = Fg(color::LightRed);
config::Config, static SUCCESS: Fg<color::LightGreen> = Fg(color::LightGreen);
filter, static WARN: Fg<color::Yellow> = Fg(color::Yellow);
util::time static RESET: Fg<color::Reset> = Fg(color::Reset);
};
static SUCCESS: Fg<color::Green> = Fg(color::Green);
static WARNING: Fg<color::Yellow> = Fg(color::Yellow);
static FAILURE: Fg<color::Red> = Fg(color::Red);
pub fn apply(targets: &Vec<Map<String, Value>>) { pub fn apply(targets: &Vec<Map<String, Value>>) {
let start = SystemTime::now();
let home = var("HOME").unwrap(); let home = var("HOME").unwrap();
println!("running apply:"); println!("{}running apply{}", style::Bold, style::Reset);
for target in targets { for target in targets {
let start = SystemTime::now();
// get path and name // get path and name
let path = target.get("path"); let path = target.get("path");
let i_name = target.get("name"); let i_name = target.get("name");
@ -44,12 +31,12 @@ pub fn apply(targets: &Vec<Map<String, Value>>) {
if path.is_none() { if path.is_none() {
if i_name.is_some() { if i_name.is_some() {
let name = i_name.unwrap().as_str().unwrap(); let name = i_name.unwrap().as_str().unwrap();
println!(" {WARNING}\"{name}\" is missing its path property; skipping{RESET}"); println!(" {WARN}\"{name}\" is missing its path property; skipping{RESET}");
} else { println!(" {WARNING}skipping empty target{RESET}"); } } else { println!(" {WARN}skipping empty target{RESET}"); }
continue; continue;
} }
if i_name.is_none() { if i_name.is_none() {
println!(" {WARNING}target missing name; skipping{RESET}"); println!(" {WARN}target missing name; skipping{RESET}");
continue; continue;
} }
@ -64,36 +51,20 @@ pub fn apply(targets: &Vec<Map<String, Value>>) {
// copy and print // copy and print
let result = fs::copy(source, destination); let result = fs::copy(source, destination);
let time = time(start); if result.is_err() { println!(" {FAIL}failed to copy!{RESET}"); }
if result.is_err() { else { println!(" {SUCCESS}completed successfully{RESET}"); }
print!(" {BOLD}{FAILURE}failed to copy{RESET}");
} }
else {
print!(" {BOLD}{SUCCESS}completed{RESET}");
}
println!(" {FAINT}({time}){RESET}");
}
let time = time(start);
println!("{FAINT}(apply: {time}){RESET}");
} }
pub fn build(targets: &Vec<Map<String, Value>>, template_dir: String, config: &Config) { pub fn build(targets: &Vec<Map<String, Value>>, tera: &mut Tera, context: &Context) {
let start = SystemTime::now();
let home = var("HOME").unwrap(); let home = var("HOME").unwrap();
println!("running build:"); println!("{}running build{}", style::Bold, style::Reset);
let mut engine = Engine::new();
engine.add_filter("has", filter::has);
for target in targets { for target in targets {
let start = SystemTime::now();
let context = config.context(target);
// get name property // get name property
let i_name = target.get("name"); let i_name = target.get("name");
// handle empty names gracefully // handle empty names gracefully
if i_name.is_none() { if i_name.is_none() {
println!(" {WARNING}target missing name; skipping{RESET}"); println!(" {WARN}target missing name; skipping{RESET}");
continue; continue;
} }
@ -101,32 +72,16 @@ pub fn build(targets: &Vec<Map<String, Value>>, template_dir: String, config: &C
let name = i_name.unwrap().as_str().unwrap(); let name = i_name.unwrap().as_str().unwrap();
println!(" building \"{name}\":"); println!(" building \"{name}\":");
// compile // render template
let compile_start = SystemTime::now(); let render = tera.render(name, context);
print!(" {ITALIC}compiling{RESET}"); // handle rendering errors gracefully
let mut path = PathBuf::from(&template_dir); if render.is_err() {
if let Some(Value::String(base)) = target.get("base") { path.push(base); } let error = render.err().unwrap();
else { path.push(name); } let message = error.source().unwrap();
let content = read_to_string(path).unwrap(); println!(" {FAIL}failed to render template:\n {message}{RESET}");
let template = engine.compile(&content);
let compile_time = time(compile_start);
print!(" {FAINT}({compile_time}){RESET}");
if let Err(error) = template {
println!("\n {BOLD}{FAILURE}failed to compile template:{RESET}\n {FAILURE}{error}\n {BOLD}skipping{RESET}");
continue; continue;
} else { println!(); } }
// render
let render_start = SystemTime::now();
print!(" {ITALIC}rendering{RESET}");
let render = template.unwrap().render(&engine, &context).to_string();
let render_time = time(render_start);
print!(" {FAINT}({render_time}){RESET}");
if let Err(error) = render {
println!("\n {BOLD}{FAILURE}failed to render template:{RESET}\n {FAILURE}{error}\n {BOLD}skipping{RESET}");
continue;
} else { println!(); }
// get rendered text and open destination file // get rendered text and open destination file
let output = render.unwrap(); let output = render.unwrap();
@ -134,19 +89,14 @@ pub fn build(targets: &Vec<Map<String, Value>>, template_dir: String, config: &C
let path = Path::new(&destination); let path = Path::new(&destination);
let i_file = File::create(path); let i_file = File::create(path);
if i_file.is_err() { if i_file.is_err() {
println!(" {BOLD}{FAILURE}failed to create destination file at {path:?}{RESET}"); println!(" {FAIL}failed to create destination file at {path:?}{RESET}");
continue; continue;
} }
let mut file = i_file.unwrap(); let mut file = i_file.unwrap();
// write to destination file // write to destination file
let written = write!(&mut file, "{output}"); let written = write!(&mut file, "{output}");
if written.is_err() { println!(" {FAILURE}failed to write to destination file at {path:?}{RESET}"); } if written.is_err() { println!(" {FAIL}failed to write to destination file at {path:?}{RESET}"); }
else { else { println!(" {SUCCESS}completed successfully{RESET}"); }
let time = time(start);
println!(" {BOLD}{SUCCESS}completed{RESET} {FAINT}({time}){RESET}");
} }
} }
let time = time(start);
println!("{FAINT}(build: {time}){RESET}");
}

View file

@ -1,35 +0,0 @@
use std::time::SystemTime;
use upon::Value as ContextValue;
use toml::{ value::Array, Value };
pub fn matches(array: Array, to_match: String) -> Option<Value> {
array.iter().filter(|value| {
if let Value::Table(table) = value {
if let Some(Value::String(name)) = table.get("name") {
return *name == to_match;
}
}
return false;
}).map(|value| value.to_owned()).nth(0)
}
pub fn convert(value: &Value) -> Option<ContextValue> {
match value.clone() {
Value::Boolean(bool) => Some(bool.into()),
Value::Float(float) => Some(float.into()),
Value::Integer(int) => Some(int.into()),
Value::String(string) => Some(string.into()),
_ => None
}
}
pub fn time(start: SystemTime) -> String {
let now = SystemTime::now();
if let Ok(duration) = now.duration_since(start) {
let ms = duration.as_millis();
if ms > 0 { format!("{ms} ms") }
else { "< 1 ms".to_owned() }
} else { String::new() }
}