use clap::{Parser, Subcommand};
use serde::Deserialize;
use serde_yaml::{
self,
Value,
};
use std::{
collections::HashMap,
convert::From,
env::current_dir,
hash::Hash,
path::PathBuf,
fs
};
use thiserror::Error;
#[derive(Parser)]
#[command()]
struct Cli {
#[command(subcommand)]
command: Command
}
#[derive(Subcommand)]
enum Command {
Build {
path: Vec<String>,
}
}
#[derive(Error, Debug)]
enum Error {
#[error(transparent)]
StdIo(#[from] std::io::Error),
#[error("{0} is not a valid karbonfile path or directory")]
InvalidBuildPath(PathBuf),
#[error(transparent)]
SerdeError(#[from] serde_yaml::Error),
}
#[derive(Clone, Debug, Deserialize)]
#[serde(untagged)]
enum Document {
Karbonfile {
apiVersion: String,
resources: HashMap<String, Option<ResourceOpts>>,
#[serde(default)]
transformations: Vec<Transformation>,
},
Other(Value),
}
#[derive(Clone, Debug, Deserialize)]
#[serde(untagged)]
enum ResourceOpts {}
#[derive(Clone, Debug)]
struct File {
path: PathBuf,
document: Document,
}
#[derive(Clone, Debug)]
struct Variant {
branch: Vec<File>,
result: Value,
}
impl Variant {
pub fn new() -> Self {
Self::default()
}
pub fn render(&mut self) {
let mut files = self.branch.clone();
if let Some(File { document: Document::Other(mut doc), ..}) = files.pop() {
files
.iter()
.filter_map(
|f| {
match f {
File { document: Document::Karbonfile { transformations: ts, .. }, .. } => {
return Some(ts);
},
_ => {
return None;
},
}
}
)
.flatten()
.for_each(|t| t.apply(&mut doc));
self.result = doc;
}
}
}
impl Default for Variant {
fn default() -> Self {
return Self {
branch: Vec::new(),
result: Value::Null,
};
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(untagged, rename_all = "lowercase")]
enum Transformation {
PatchSet {
#[serde(default)]
filters: Vec<Filter>,
patches: HashMap<JsonPointer, JsonPatch>,
}
}
impl Transformation {
pub fn apply(&self, doc: &mut Value) {
match self {
Transformation::PatchSet {
filters: fs,
patches: ps,
} => {
if fs.iter().any(|f| f.matches(doc)) {
for (ptr, patch) in ps.into_iter() {
match patch {
JsonPatch::Add(_) => {
ptr.new_mut(doc);
},
JsonPatch::Replace(v) => {
*ptr.get_mut(doc) = v.clone();
},
JsonPatch::Remove => {
},
}
}
}
},
}
}
}
#[derive(Clone, Debug, Deserialize)]
struct Filter(HashMap<JsonPointer, Value>);
impl Filter {
pub fn matches(&self, doc: &mut Value) -> bool {
let Filter(map) = self;
return map
.into_iter()
.all(|(ptr, val)| {
*ptr.get_mut(doc) == val.clone()
});
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
enum JsonPatch {
Add(Value),
Replace(Value),
Remove,
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq)]
#[serde(try_from = "String")]
struct JsonPointer(Vec<String>);
impl JsonPointer {
pub fn get_mut<'a>(&'a self, doc: &'a mut Value) -> &mut Value {
let mut result = doc;
let fields = self.fields();
Self::walk(&mut result, fields);
return result;
}
pub fn create_mut<'a>(&'a self, doc: &'a mut Value) -> &mut Value {
let mut result = doc;
let mut fields = self.fields();
if let Some(new_field) = fields.pop() {
Self::walk(&mut result, fields);
}
*return =
return result;
}
fn fields(&self) -> Vec<String> {
let JsonPointer(fields) = self;
return fields.clone();
}
fn walk(mut result: &mut Value, fields: Vec<String>) -> &mut Value {
for field in fields {
if let Ok(number) = field.parse::<usize>() {
result = result
.get_mut(number)
.expect("Field {field} wasn't found");
} else {
result = result
.get_mut(field)
.expect("Field {field} wasn't found");
}
}
return result;
}
}
impl From<String> for JsonPointer {
fn from(item: String) -> Self {
if !item.starts_with("/") {
panic!("{item} is not a valid JSON pointer. It should start with a '/'");
}
let result: Vec<String> = item.split('/')
.skip(1) .map(|x| x.to_string())
.collect();
return JsonPointer(result);
}
}
fn read_file(path: &PathBuf) -> String {
return fs::read_to_string(path)
.expect(&format!("Failed to open file {}", path.display()));
}
fn parse(path: &PathBuf) -> Document {
let content = read_file(path);
let result: Document = serde_yaml::from_str(&content).unwrap();
return result;
}
fn normalize(root: &PathBuf, input: &str) -> PathBuf {
let mut result = root.clone();
if result.is_file() {
result.pop();
};
result.push(input);
return result
.canonicalize()
.expect(&format!("Failed to normalize {}", result.display()));
}
fn run(path: PathBuf, result: &mut Vec<Variant>, variant: Variant) {
let document = parse(&path);
let file = File {
path: path.clone(),
document: document.clone(),
};
let mut branch = variant.branch;
branch.push(file);
let mut new_variant = Variant {
branch: branch,
..variant
};
match document {
Document::Karbonfile { resources: resources, .. } => {
for r in resources.keys() {
run(normalize(&path, r), result, new_variant.clone());
};
},
Document::Other(_) => {
new_variant.render();
result.push(new_variant);
},
};
}
fn main() {
let cli = Cli::parse();
let root = current_dir().unwrap();
match cli.command {
Command::Build { path } => {
let mut result = Vec::new();
for p in path {
let normalized = normalize(&root, &p);
run(normalized, &mut result, Variant::new());
};
println!("{:#?}", result);
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_jsonpointer() {
assert_eq!(
JsonPointer::from("/a/b/c".to_string()),
JsonPointer(vec!["a".to_string(), "b".to_string(), "c".to_string()])
);
}
}