From ae5bf37df1902ddea681cad7eec7ddffed02480e Mon Sep 17 00:00:00 2001 From: lost Date: Tue, 17 Feb 2026 03:59:16 -0700 Subject: [PATCH] use pathbuf in spec; move output to render --- src/main.rs | 43 +++--------------- src/render.rs | 119 +++++++++++++++++++++++++++++++++++--------------- src/spec.rs | 113 ++++++++++++++++++++++------------------------- 3 files changed, 141 insertions(+), 134 deletions(-) diff --git a/src/main.rs b/src/main.rs index cedb88b..d2799d7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,10 +6,6 @@ mod tmpl; use log::LogLevel; use render::RenderedConfig; -use std::fs::{self, File, OpenOptions, create_dir_all, write}; -use std::io::Write; -use std::path::Path; -use yaml_serde; fn main() { let args: cli::Args = cli::parse_args(); @@ -21,38 +17,11 @@ fn main() { spec::compile(&args.devices, &args.env.spec_path, dbg); for spec in specifications { - let result = RenderedConfig::from_spec(&spec, dbg).unwrap_or_else(|e| { - eprintln!("{}", e); - std::process::exit(1); - }); - output_rendered_configs(result, &args.env.out_path, dbg) + RenderedConfig::from_spec(&spec, dbg) + .unwrap_or_else(|e| { + eprintln!("{}", e); + std::process::exit(1); + }) + .output(&args.env.out_path, dbg); } } - -fn output_rendered_configs(rendered_config: RenderedConfig, outdir: &str, dbg: LogLevel) { - info!(dbg, "Writing Output"); - let out_path = Path::new(outdir).join(rendered_config.hostname); - if out_path.exists() { - fs::remove_dir_all(&out_path).ok(); - } - create_dir_all(&out_path).ok(); - - let merged_config_path = out_path.join("all.conf"); - let mut all_file: File = OpenOptions::new() - .create(true) - .append(true) - .open(&merged_config_path) - .expect("unable to open"); - for config in rendered_config.configs { - let template_path = out_path.join(config.name + ".tera"); - verb!(dbg, " | {}", &template_path.display()); - write(&template_path, &config.data).ok(); - all_file.write_all(&config.data.as_bytes()).ok(); - } - - let spec: String = yaml_serde::to_string(&rendered_config.spec).unwrap(); - let compiled_spec_path = out_path.join("compiled-spec.yaml"); - verb!(dbg, " | {}", &compiled_spec_path.display()); - write(&compiled_spec_path, spec).ok(); - info!(dbg, " | {} ", &merged_config_path.display()); -} diff --git a/src/render.rs b/src/render.rs index b7e2484..88971b4 100644 --- a/src/render.rs +++ b/src/render.rs @@ -2,8 +2,7 @@ use crate::spec::Specification; use crate::tmpl; use crate::{LogLevel, info, verb}; use serde_json::Value; -use std::fs; -use tera::{Context, Tera}; +use std::io::Write; pub struct RenderedConfig { pub hostname: String, @@ -21,56 +20,30 @@ impl RenderedConfig { spec: &Specification, dbg: LogLevel, ) -> Result> { - let mut context = Context::new(); + let context = get_context_from_specification(&spec); - // Flatten "data" map into Tera context - if let Some(data_map) = spec.compiled.get("data").and_then(|v| v.as_object()) { - for (key, val) in data_map { - context.insert(key, val); - } - } - - let hostname = context - .get("hostname") - .and_then(|v| v.as_str()) - .unwrap_or("default_hostname") - .to_string(); + let hostname = get_hostname(&context, &spec); + info!(dbg, "Rendering {}", hostname); let base_dir = std::path::Path::new("./tmpl").join(spec.get_layer()); - let structure_path = base_dir.join("structure.yaml"); let structure = tmpl::Structure::from_file(&structure_path) .map_err(|e| format!("Failed to parse {}: {}", structure_path.display(), e))?; - let mut tera = Tera::default(); + let mut renderer = tera::Tera::default(); let mut configs = Vec::new(); - info!(dbg, "Rendering {}", hostname); + let template_data = get_template_data_from_structure(&structure, &base_dir); - let template_data: Vec<(String, String)> = structure - .files - .iter() - .filter_map(|name| { - let path = base_dir.join(format!("{}.tera", name)); - match fs::read_to_string(&path) { - Ok(content) => Some((name.clone(), content)), - Err(e) => { - eprintln!("Skipping {}: {}", path.display(), e); - None - } - } - }) - .collect(); - - tera.add_raw_templates(template_data)?; + renderer.add_raw_templates(template_data)?; for template_name in &structure.files { verb!(dbg, " | {}", &template_name); - if !tera.get_template_names().any(|n| n == template_name) { + if !renderer.get_template_names().any(|n| n == template_name) { continue; } - match tera.render(template_name, &context) { + match renderer.render(template_name, &context) { Ok(rendered) => configs.push(Configuration { name: template_name.clone(), data: rendered, @@ -96,4 +69,78 @@ impl RenderedConfig { } eprintln!("[tera] {}: {}", name, chain); } + + pub fn output(self: Self, outdir: &str, dbg: LogLevel) { + info!(dbg, "Writing Output:"); + + let out_path = std::path::Path::new(outdir).join(self.hostname); + if out_path.exists() { + std::fs::remove_dir_all(&out_path).ok(); + } + std::fs::create_dir_all(&out_path).ok(); + + let merged_config_path = out_path.join("all.conf"); + let mut all_file: std::fs::File = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&merged_config_path) + .expect("unable to open"); + + for config in &self.configs { + let template_path = out_path.join(format!("{}.tera", &config.name)); + verb!(dbg, " | {}", &template_path.display()); + std::fs::write(&template_path, &config.data).ok(); + all_file.write_all(&config.data.as_bytes()).ok(); + } + + let spec: String = yaml_serde::to_string(&self.spec).unwrap(); + let compiled_spec_path = out_path.join("compiled-spec.yaml"); + verb!(dbg, " | {}", &compiled_spec_path.display()); + std::fs::write(&compiled_spec_path, spec).ok(); + info!(dbg, " | {} ", &merged_config_path.display()); + } +} + +fn get_template_data_from_structure( + structure: &tmpl::Structure, + base_dir: &std::path::Path, +) -> Vec<(String, String)> { + structure + .files + .iter() + .filter_map(|name| { + let path = base_dir.join(format!("{}.tera", name)); + match std::fs::read_to_string(&path) { + Ok(content) => Some((name.clone(), content)), + Err(e) => { + eprintln!("Skipping {}: {}", path.display(), e); + None + } + } + }) + .collect() +} + +fn get_context_from_specification(spec: &Specification) -> tera::Context { + let mut context = tera::Context::new(); + if let Some(data_map) = spec.compiled.get("data").and_then(|v| v.as_object()) { + for (key, val) in data_map { + context.insert(key, val); + } + } + context +} + +fn get_hostname(context: &tera::Context, spec: &Specification) -> String { + context + .get("hostname") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + .unwrap_or_else(|| { + spec.device + .file_stem() + .and_then(|n| n.to_str()) + .unwrap_or("unknown") + .to_string() + }) } diff --git a/src/spec.rs b/src/spec.rs index e3bdb35..8feff88 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -2,28 +2,27 @@ use crate::{LogLevel, info, verb}; use regex::Regex; use serde_json::{Map, Value, json}; use std::path::{Path, PathBuf}; -use std::str::FromStr; use std::{env, fmt, fs}; use walkdir::WalkDir; use yaml_serde; #[derive(Debug)] pub struct Specification { - pub partitional: String, - pub regional: String, - pub common: String, - pub zonal: String, - pub device: String, + pub partitional: PathBuf, + pub regional: PathBuf, + pub common: PathBuf, + pub zonal: PathBuf, + pub device: PathBuf, pub compiled: Value, } impl Specification { pub fn build( - partitional: String, - regional: String, - common: String, - zonal: String, - device: String, + partitional: PathBuf, + regional: PathBuf, + common: PathBuf, + zonal: PathBuf, + device: PathBuf, ) -> Result> { let json_values: Vec = [&partitional, ®ional, &common, &zonal, &device] .iter() @@ -31,7 +30,6 @@ impl Specification { .map(|content| yaml_serde::from_str::(&content)) .collect::, _>>()?; - // Merge all objects under a single key let merged_map: Map = json_values .into_iter() .filter_map(|v| v.as_object().cloned()) @@ -39,21 +37,20 @@ impl Specification { .flatten() .collect(); - // Create the final merged object let compiled = json!({"data": merged_map}); Ok(Self { - partitional: String::from(partitional), - regional: String::from(regional), - common: String::from(common), - zonal: String::from(zonal), - device: String::from(device), + partitional, + regional, + common, + zonal, + device, compiled, }) } pub fn get_layer(&self) -> String { - PathBuf::from(&self.device) + self.device .parent() .unwrap() .file_name() @@ -70,13 +67,18 @@ impl fmt::Display for Specification { write!( f, "Specification:\nPartitional: {}\nRegional: {}\nCommon: {}\nZonal: {}\nDevice: {}\nCompiled:\n{}", - self.partitional, self.regional, self.common, self.zonal, self.device, self.compiled + self.partitional.display(), + self.regional.display(), + self.common.display(), + self.zonal.display(), + self.device.display(), + self.compiled ) } } pub fn compile(pattern: &Regex, spec_path: &String, dbg: LogLevel) -> Vec { - let spec_list: Vec = filter_specs(pattern, get_specs(spec_path, dbg)); + let spec_list: Vec = filter_specs(pattern, get_specs(spec_path, dbg)); info!( dbg, "Matched {} devices against '{}'", @@ -84,14 +86,14 @@ pub fn compile(pattern: &Regex, spec_path: &String, dbg: LogLevel) -> Vec = Vec::new(); for spec in spec_list { - let zonal: String = get_zonal(&spec); - let common: String = get_common(&spec); - let regional: String = get_regional(&spec); - let partitional: String = get_partional(&common, ®ional); + let zonal = get_zonal(&spec); + let common = get_common(&spec); + let regional = get_regional(&spec); + let partitional = get_partitional(&common, ®ional); specifications.push( match Specification::build(partitional, regional, common, zonal, spec) { Ok(compiled_spec) => compiled_spec, @@ -102,25 +104,25 @@ pub fn compile(pattern: &Regex, spec_path: &String, dbg: LogLevel) -> Vec String { +fn get_partitional(common: &PathBuf, regional: &PathBuf) -> PathBuf { let re = Regex::new(r"partition: ([^\n\r$]+)").expect("failed"); [common, regional] .iter() - .find_map(|&path| { + .find_map(|path| { fs::read_to_string(path).ok().and_then(|contents| { re.captures(&contents).and_then(|captures| { - captures - .get(1) - .map(|matched| format!("./spec/common/{}.yaml", matched.as_str().trim())) + captures.get(1).map(|matched| { + PathBuf::from(format!("./spec/common/{}.yaml", matched.as_str().trim())) + }) }) }) }) - .unwrap_or_else(|| format!("./spec/common/default.yaml")) + .unwrap_or_else(|| PathBuf::from("./spec/common/default.yaml")) } -fn get_regional(path: &str) -> String { - let dir_name = PathBuf::from(path) +fn get_regional(path: &PathBuf) -> PathBuf { + let dir_name = path .parent() .unwrap() .file_name() @@ -128,59 +130,48 @@ fn get_regional(path: &str) -> String { .to_string_lossy() .into_owned(); - let new_path = PathBuf::from(path) - .parent() + path.parent() .unwrap() .parent() .unwrap() .join("common") - .join(format!("{}.yaml", &dir_name[0..2])); - - String::from(new_path.to_str().unwrap()) + .join(format!("{}.yaml", &dir_name[0..2])) } -fn get_common(spec: &str) -> String { - let path: PathBuf = PathBuf::from_str(&spec).expect("Path was invalid"); - String::from(path.with_file_name("common.yaml").to_str().unwrap()) +fn get_common(spec: &PathBuf) -> PathBuf { + spec.with_file_name("common.yaml") } -fn get_zonal(spec: &str) -> String { - let path: PathBuf = PathBuf::from_str(&spec).expect("Path was invalid"); +fn get_zonal(spec: &PathBuf) -> PathBuf { + let zone = spec + .file_name() + .and_then(|f| f.to_str()) + .and_then(|f| f.split_once('-')) + .map(|(zone, _)| spec.with_file_name(zone)) + .unwrap_or_else(|| spec.with_file_name("common.yaml")); - String::from(format!( - "{}.common.yaml", - path.file_name() - .and_then(|filename| filename.to_str()) - .and_then(|filename| filename.split_once('-')) - .map(|(zone, _)| path.with_file_name(zone)) - .unwrap_or_else(|| path.with_file_name("common.yaml")) - .to_str() - .unwrap() - .to_string(), - )) + PathBuf::from(format!("{}.common.yaml", zone.to_str().unwrap())) } -fn filter_specs(pattern: &Regex, spec_list: Vec) -> Vec { +fn filter_specs(pattern: &Regex, spec_list: Vec) -> Vec { spec_list .into_iter() .filter(|spec| { - Path::new(spec) - .file_name() + spec.file_name() .and_then(|name| name.to_str()) .map_or(false, |name| pattern.is_match(name)) }) .collect() } -fn get_specs(spec_path: &String, dbg: LogLevel) -> Vec { +fn get_specs(spec_path: &String, dbg: LogLevel) -> Vec { let mut result = Vec::new(); let root = Path::new(&spec_path); for entry in WalkDir::new(root).into_iter().filter_map(|e| e.ok()) { if let Ok(path) = entry.path().strip_prefix(root) { let path_str = path.to_string_lossy().to_string(); - if !path_str.contains("common") && path_str.ends_with("yaml") { - result.push(format!("{}/{}", spec_path, path_str)); + result.push(root.join(path)); } } }