refactor: modularize codebase and improve type safety

This commit is contained in:
2026-02-18 03:39:29 -07:00
parent 57a3287f38
commit 8877c61aed
10 changed files with 358 additions and 327 deletions

2
Cargo.lock generated
View File

@@ -717,7 +717,7 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]] [[package]]
name = "skyforge" name = "skyforge"
version = "0.1.0" version = "0.1.112"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "skyforge" name = "skyforge"
version = "0.1.0" version = "0.1.112"
edition = "2024" edition = "2024"
[dependencies] [dependencies]

View File

@@ -47,10 +47,10 @@ Options:
-V, --version Print version -V, --version Print version
Environment: Environment:
SF_SPEC_PATH Path to the directory containing templates. Defaults to "./spec". SF_SPEC_PATH Directory containing templates
SF_TMPL_PATH Path to the directory containing specifications. Defaults to "./tmpl". SF_TMPL_PATH Directory containing specifications
SF_OUT_PATH Path to the directory for command output. Defaults to "./out". SF_OUT_PATH Directory for command output
SF_LOG_PATH Path to the directory for log output. Defaults to "./log". SF_LOG_PATH Directory for log output
``` ```
### Standard ### Standard

View File

@@ -1,2 +1,2 @@
device: device:
hostname: xyz2-ex-core-r202 hostname: xyz2-ex-core-r102

View File

@@ -1,81 +1,7 @@
use crate::log::LogLevel; use crate::log::LogLevel;
use clap::{Arg, ArgAction, ArgGroup, Command}; use clap::{Arg, ArgAction, ArgGroup, Command};
use regex::Regex; use regex::Regex;
use std::fmt; use std::path::PathBuf;
#[derive(Debug)]
pub struct Args {
pub devices: Regex,
pub loglevel: LogLevel,
pub env: EnvVars,
}
impl fmt::Display for Args {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"devices: {}, loglevel: {}, env: {}",
self.devices, self.loglevel, self.env
)
}
}
#[derive(Debug)]
pub struct EnvVars {
pub spec_path: String,
pub tmpl_path: String,
pub out_path: String,
pub log_path: String,
}
impl fmt::Display for EnvVars {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"spec_path: {}, tmpl_path: {}, out_path: {}, log_path: {}",
self.spec_path, self.tmpl_path, self.out_path, self.log_path
)
}
}
/// loads `command line arguments` and `environment variables` using custom `clap`
pub fn parse_args() -> Args {
let matches: clap::ArgMatches = build().get_matches();
let raw_devices = matches.get_one::<String>("devices").unwrap();
Args {
devices: Regex::new(&raw_devices).expect("Invalid regex pattern provided for `devices`"),
loglevel: if matches.get_flag("debug") {
LogLevel::Debug
} else if matches.get_flag("verbose") {
LogLevel::Verbose
} else {
LogLevel::Info
},
env: parse_env(),
}
}
/// loads environment variables
pub fn parse_env() -> EnvVars {
EnvVars {
spec_path: match std::env::var("SF_SPEC_PATH") {
Ok(path) => path.trim_end_matches('/').to_string(),
Err(_) => String::from("./spec"),
},
tmpl_path: match std::env::var("SF_TMPL_PATH") {
Ok(path) => path.trim_end_matches('/').to_string(),
Err(_) => String::from("./tmpl"),
},
out_path: match std::env::var("SF_OUT_PATH") {
Ok(path) => path.trim_end_matches('/').to_string(),
Err(_) => String::from("./out"),
},
log_path: match std::env::var("SF_LOG_PATH") {
Ok(path) => path.trim_end_matches('/').to_string(),
Err(_) => String::from("./log"),
},
}
}
const ABOUT_MSG: &str = r#"Skyforge Config Generation Engine"#; const ABOUT_MSG: &str = r#"Skyforge Config Generation Engine"#;
const ENV_MSG: &str = r#"Environment: const ENV_MSG: &str = r#"Environment:
@@ -85,38 +11,90 @@ const ENV_MSG: &str = r#"Environment:
SF_LOG_PATH Directory for log output SF_LOG_PATH Directory for log output
"#; "#;
/// builds a custom command line argument parser #[derive(Debug)]
fn build() -> Command { pub struct Args {
Command::new("app") pub devices: Regex,
.about(ABOUT_MSG) pub loglevel: LogLevel,
.version(env!("CARGO_PKG_VERSION")) pub env: EnvVars,
.author("rskntroot") }
.arg(
Arg::new("devices") impl Args {
.long("devices") /// loads command line arguments and environment variables
.short('d') pub fn parse() -> Args {
.help("A regular expression pattern") let matches: clap::ArgMatches = Self::parser().get_matches();
.required(true), let raw_devices = matches.get_one::<String>("devices").unwrap();
) Args {
.arg( devices: Regex::new(&raw_devices)
Arg::new("debug") .expect("Invalid regex pattern provided for `devices`"),
.long("debug") loglevel: if matches.get_flag("debug") {
.help("Print debug information") LogLevel::Debug
.action(ArgAction::SetTrue) } else if matches.get_flag("verbose") {
.required(false), LogLevel::Verbose
) } else {
.arg( LogLevel::Info
Arg::new("verbose") },
.short('v') env: EnvVars::parse(),
.long("verbose") }
.help("Print verbose information") }
.action(ArgAction::SetTrue) fn parser() -> Command {
.required(false), Command::new("skyforge")
) .about(ABOUT_MSG)
.group( .version(env!("CARGO_PKG_VERSION"))
ArgGroup::new("loglevel") .author("rskntroot")
.args(&["debug", "verbose"]) .arg(
.required(false), Arg::new("devices")
) .long("devices")
.after_help(ENV_MSG) .short('d')
.help("A regular expression pattern")
.required(true),
)
.arg(
Arg::new("debug")
.long("debug")
.help("Print debug information")
.action(ArgAction::SetTrue)
.required(false),
)
.arg(
Arg::new("verbose")
.short('v')
.long("verbose")
.help("Print verbose information")
.action(ArgAction::SetTrue)
.required(false),
)
.group(
ArgGroup::new("loglevel")
.args(&["debug", "verbose"])
.required(false),
)
.after_help(ENV_MSG)
}
}
#[derive(Debug)]
pub struct EnvVars {
pub spec_path: PathBuf,
pub tmpl_path: PathBuf,
pub out_path: PathBuf,
pub log_path: PathBuf,
}
impl EnvVars {
pub fn parse() -> Self {
Self {
spec_path: std::env::var("SF_SPEC_PATH")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("./spec")),
tmpl_path: std::env::var("SF_TMPL_PATH")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("./tmpl")),
out_path: std::env::var("SF_OUT_PATH")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("./out")),
log_path: std::env::var("SF_LOG_PATH")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("./log")),
}
}
} }

10
src/lib.rs Normal file
View File

@@ -0,0 +1,10 @@
pub mod cli;
pub mod log;
pub mod render;
pub mod spec;
pub mod tmpl;
pub use cli::Args;
pub use log::LogLevel;
pub use render::DeviceConfigBundle;
pub use spec::Specification;

View File

@@ -1,27 +1,11 @@
mod cli; use skyforge::{Args, DeviceConfigBundle, Specification};
mod log;
mod render;
mod spec;
mod tmpl;
use log::LogLevel; fn main() -> Result<(), Box<dyn std::error::Error>> {
use render::RenderedConfig; let args = Args::parse();
fn main() { for spec in Specification::compile(&args) {
let args: cli::Args = cli::parse_args(); DeviceConfigBundle::from_spec(spec)?.output_artifacts();
let dbg: LogLevel = args.loglevel;
dbug!(dbg, "{:#?}", &args);
let specifications: Vec<spec::Specification> =
spec::compile(&args.devices, &args.env.spec_path, dbg);
for spec in specifications {
RenderedConfig::from_spec(&spec, dbg)
.unwrap_or_else(|e| {
eprintln!("{}", e);
std::process::exit(1);
})
.output(&args.env.out_path, dbg);
} }
Ok(())
} }

View File

@@ -1,13 +1,19 @@
use crate::log::LogLevel;
use crate::spec::Specification; use crate::spec::Specification;
use crate::tmpl; use crate::tmpl;
use crate::{LogLevel, info, verb}; use crate::{dbug, info, verb};
use serde_json::Value;
use std::io::Write; use std::io::Write;
pub struct RenderedConfig { pub struct DeviceConfigBundle {
pub hostname: String, pub hostname: String,
pub configs: Vec<ConfigVariant>,
pub spec: Specification,
pub platform: String,
}
pub struct ConfigVariant {
pub name: String,
pub configs: Vec<Configuration>, pub configs: Vec<Configuration>,
pub spec: Value,
} }
pub struct Configuration { pub struct Configuration {
@@ -15,15 +21,12 @@ pub struct Configuration {
pub data: String, pub data: String,
} }
impl RenderedConfig { impl DeviceConfigBundle {
pub fn from_spec( pub fn from_spec(spec: Specification) -> Result<Self, Box<dyn std::error::Error>> {
spec: &Specification, let mut context = Self::get_context(&spec);
dbg: LogLevel,
) -> Result<Self, Box<dyn std::error::Error>> {
let context = Self::get_context(&spec);
let hostname = Self::get_hostname(&context, &spec); let hostname = Self::get_hostname(&context, &spec);
info!(dbg, "Rendering {}", hostname); info!(&spec.loglevel, "Rendering {}", hostname);
let base_dir = std::path::Path::new("./tmpl").join(spec.get_layer()); let base_dir = std::path::Path::new("./tmpl").join(spec.get_layer());
let structure_path = base_dir.join("structure.yaml"); let structure_path = base_dir.join("structure.yaml");
@@ -31,28 +34,38 @@ impl RenderedConfig {
.map_err(|e| format!("Failed to parse {}: {}", structure_path.display(), e))?; .map_err(|e| format!("Failed to parse {}: {}", structure_path.display(), e))?;
let mut renderer = tera::Tera::default(); let mut renderer = tera::Tera::default();
renderer.add_raw_templates(structure.load_template_data(&base_dir))?; renderer.add_raw_templates(structure.load_template_data(&base_dir, spec.loglevel))?;
dbug!(&spec.loglevel, "Processing templates for {}", hostname);
let mut configs = Vec::new(); let mut config_variants = Vec::new();
for template_name in &structure.files { for variant in &structure.variations {
verb!(dbg, " | {}", &template_name); context.insert(variant, &true);
if !renderer.get_template_names().any(|n| n == template_name) { let mut configs = Vec::new();
continue; for template_name in &structure.files {
} dbug!(&spec.loglevel, " | {}.{}", &template_name, variant);
if !renderer.get_template_names().any(|n| n == template_name) {
match renderer.render(template_name, &context) { continue;
Ok(rendered) => configs.push(Configuration { }
name: template_name.clone(), match renderer.render(template_name, &context) {
data: rendered, Ok(data) => configs.push(Configuration {
}), name: template_name.clone(),
Err(e) => Self::log_tera_error(template_name, &e), data,
}),
Err(e) => Self::log_tera_error(template_name, &e),
}
} }
config_variants.push(ConfigVariant {
name: String::from(variant),
configs,
});
context.remove(variant);
} }
Ok(Self { Ok(Self {
hostname, hostname,
configs, configs: config_variants,
spec: spec.compiled.clone(), spec,
platform: structure.platform,
}) })
} }
@@ -61,13 +74,7 @@ impl RenderedConfig {
.get("hostname") .get("hostname")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.map(|s| s.to_string()) .map(|s| s.to_string())
.unwrap_or_else(|| { .unwrap_or_else(|| spec.components.get_hostname())
spec.device
.file_stem()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string()
})
} }
fn get_context(spec: &Specification) -> tera::Context { fn get_context(spec: &Specification) -> tera::Context {
@@ -90,33 +97,43 @@ impl RenderedConfig {
eprintln!("[tera] {}: {}", name, chain); eprintln!("[tera] {}: {}", name, chain);
} }
pub fn output(self: Self, outdir: &str, dbg: LogLevel) { pub fn output_artifacts(self: Self) {
info!(dbg, "Writing Output:"); info!(&self.spec.loglevel, "Writing Output:");
let out_path = std::path::Path::new(outdir).join(self.hostname); let device_outpath = std::path::Path::new(&self.spec.outpath).join(self.hostname);
if out_path.exists() { if device_outpath.exists() {
std::fs::remove_dir_all(&out_path).ok(); std::fs::remove_dir_all(&device_outpath).ok();
} }
std::fs::create_dir_all(&out_path).ok(); std::fs::create_dir_all(&device_outpath).ok();
let merged_config_path = out_path.join("all.conf"); for variant_configs in &self.configs {
let mut all_file: std::fs::File = std::fs::OpenOptions::new() let out_path = device_outpath.join(&variant_configs.name);
.create(true) std::fs::create_dir_all(&out_path).ok();
.append(true)
.open(&merged_config_path)
.expect("unable to open");
for config in &self.configs { let merged_config_path =
let template_path = out_path.join(format!("{}.tera", &config.name)); device_outpath.join(format!("all.{}.{}", variant_configs.name, &self.platform));
verb!(dbg, " | {}", &template_path.display()); let mut all_file: std::fs::File = std::fs::OpenOptions::new()
std::fs::write(&template_path, &config.data).ok(); .create(true)
all_file.write_all(&config.data.as_bytes()).ok(); .append(true)
.open(&merged_config_path)
.expect("unable to open");
for config in &variant_configs.configs {
let config_outpath = out_path.join(format!("{}.{}", &config.name, &self.platform));
std::fs::create_dir_all(&config_outpath).ok();
verb!(&self.spec.loglevel, " | {}", &config_outpath.display());
std::fs::write(&config_outpath, &config.data).ok();
all_file.write_all(&config.data.as_bytes()).ok();
}
info!(&self.spec.loglevel, " | {} ", &merged_config_path.display());
} }
let spec: String = yaml_serde::to_string(&self.spec).unwrap(); let compiled_spec_path = device_outpath.join("context.yaml");
let compiled_spec_path = out_path.join("compiled-spec.yaml"); verb!(&self.spec.loglevel, " | {}", &compiled_spec_path.display());
verb!(dbg, " | {}", &compiled_spec_path.display()); std::fs::write(
std::fs::write(&compiled_spec_path, spec).ok(); &compiled_spec_path,
info!(dbg, " | {} ", &merged_config_path.display()); yaml_serde::to_string(&self.spec.compiled).unwrap(),
)
.ok();
} }
} }

View File

@@ -1,19 +1,17 @@
use crate::cli::Args;
use crate::{LogLevel, info, verb}; use crate::{LogLevel, info, verb};
use regex::Regex; use regex::Regex;
use serde_json::{Map, Value, json}; use serde_json::{Map, Value, json};
use std::path::{Path, PathBuf}; use std::path::PathBuf;
use std::{env, fmt, fs};
use walkdir::WalkDir; use walkdir::WalkDir;
use yaml_serde; use yaml_serde;
#[derive(Debug)] #[derive(Debug)]
pub struct Specification { pub struct Specification {
pub partitional: PathBuf,
pub regional: PathBuf,
pub common: PathBuf,
pub zonal: PathBuf,
pub device: PathBuf,
pub compiled: Value, pub compiled: Value,
pub components: Components,
pub loglevel: LogLevel,
pub outpath: PathBuf,
} }
impl Specification { impl Specification {
@@ -23,10 +21,12 @@ impl Specification {
common: PathBuf, common: PathBuf,
zonal: PathBuf, zonal: PathBuf,
device: PathBuf, device: PathBuf,
loglevel: LogLevel,
outpath: PathBuf,
) -> Result<Self, Box<dyn std::error::Error>> { ) -> Result<Self, Box<dyn std::error::Error>> {
let json_values: Vec<Value> = [&partitional, &regional, &common, &zonal, &device] let json_values: Vec<Value> = [&partitional, &regional, &common, &zonal, &device]
.iter() .iter()
.filter_map(|path| fs::read_to_string(path).ok()) .filter_map(|path| std::fs::read_to_string(path).ok())
.map(|content| yaml_serde::from_str::<Value>(&content)) .map(|content| yaml_serde::from_str::<Value>(&content))
.collect::<Result<Vec<Value>, _>>()?; .collect::<Result<Vec<Value>, _>>()?;
@@ -40,17 +40,22 @@ impl Specification {
let compiled = json!({"data": merged_map}); let compiled = json!({"data": merged_map});
Ok(Self { Ok(Self {
partitional,
regional,
common,
zonal,
device,
compiled, compiled,
components: Components {
partitional,
regional,
common,
zonal,
device,
},
loglevel,
outpath,
}) })
} }
pub fn get_layer(&self) -> String { pub fn get_layer(&self) -> String {
self.device self.components
.device
.parent() .parent()
.unwrap() .unwrap()
.file_name() .file_name()
@@ -60,126 +65,157 @@ impl Specification {
.filter(|c| !c.is_numeric()) .filter(|c| !c.is_numeric())
.collect::<String>() .collect::<String>()
} }
/// compiles specifications from regex and paths provided by Args
pub fn compile(args: &Args) -> Vec<Specification> {
let spec_list: Vec<PathBuf> = Self::filter_specs(
&args.devices,
Self::get_specs(&args.env.spec_path, args.loglevel),
);
info!(
&args.loglevel,
"Matched {} devices against '{}'",
&spec_list.len(),
&args.devices
);
spec_list
.into_iter()
.map(|spec| {
verb!(&args.loglevel, " | {}", spec.display());
Self::build(
Self::get_partitional(&Self::get_common(&spec), &Self::get_regional(&spec)),
Self::get_regional(&spec),
Self::get_common(&spec),
Self::get_zonal(&spec),
spec,
args.loglevel,
std::path::PathBuf::from(&args.env.out_path),
)
.unwrap_or_else(|e| panic!("failed to build Specification: {}", e))
})
.collect()
}
fn get_partitional(common: &PathBuf, regional: &PathBuf) -> PathBuf {
let re = Regex::new(r"partition: ([^\n\r$]+)").expect("failed");
[common, regional]
.iter()
.find_map(|path| {
std::fs::read_to_string(path).ok().and_then(|contents| {
re.captures(&contents).and_then(|captures| {
captures.get(1).map(|matched| {
PathBuf::from(format!("./spec/common/{}.yaml", matched.as_str().trim()))
})
})
})
})
.unwrap_or_else(|| PathBuf::from("./spec/common/default.yaml"))
}
fn get_regional(path: &PathBuf) -> PathBuf {
let dir_name = path
.parent()
.unwrap()
.file_name()
.unwrap()
.to_string_lossy()
.into_owned();
path.parent()
.unwrap()
.parent()
.unwrap()
.join("common")
.join(format!("{}.yaml", &dir_name[0..2]))
}
fn get_common(spec: &PathBuf) -> PathBuf {
spec.with_file_name("common.yaml")
}
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"));
PathBuf::from(format!("{}.common.yaml", zone.to_str().unwrap()))
}
fn filter_specs(pattern: &Regex, spec_list: Vec<PathBuf>) -> Vec<PathBuf> {
spec_list
.into_iter()
.filter(|spec| {
spec.file_name()
.and_then(|name| name.to_str())
.map_or(false, |name| pattern.is_match(name))
})
.collect()
}
fn get_specs(spec_path: &PathBuf, dbg: LogLevel) -> Vec<PathBuf> {
let mut result = Vec::new();
let root = spec_path.as_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(root.join(path));
}
}
}
info!(
dbg,
"Skyforge found {} renderable devices in {}",
result.len(),
std::env::current_dir().unwrap().display()
);
result
}
} }
impl fmt::Display for Specification { impl std::fmt::Display for Specification {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!( write!(
f, f,
"Specification:\nPartitional: {}\nRegional: {}\nCommon: {}\nZonal: {}\nDevice: {}\nCompiled:\n{}", "Compiled Specification: {}\nLogLevel: {}\nComponents: {}",
self.compiled, self.loglevel, self.components,
)
}
}
#[derive(Debug)]
pub struct Components {
partitional: PathBuf,
regional: PathBuf,
common: PathBuf,
zonal: PathBuf,
device: PathBuf,
}
impl Components {
pub fn get_hostname(self: &Self) -> String {
self.device
.file_stem()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string()
}
}
impl std::fmt::Display for Components {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
" | Partitional: {}\n | Regional: {}\n | Common: {}\n | Zonal: {}\n | Device: {}\n",
self.partitional.display(), self.partitional.display(),
self.regional.display(), self.regional.display(),
self.common.display(), self.common.display(),
self.zonal.display(), self.zonal.display(),
self.device.display(), self.device.display(),
self.compiled
) )
} }
} }
pub fn compile(pattern: &Regex, spec_path: &String, dbg: LogLevel) -> Vec<Specification> {
let spec_list: Vec<PathBuf> = filter_specs(pattern, get_specs(spec_path, dbg));
info!(
dbg,
"Matched {} devices against '{}'",
&spec_list.len(),
&pattern
);
for spec in &spec_list {
verb!(dbg, " | {}", spec.display());
}
let mut specifications: Vec<Specification> = Vec::new();
for spec in spec_list {
let zonal = get_zonal(&spec);
let common = get_common(&spec);
let regional = get_regional(&spec);
let partitional = get_partitional(&common, &regional);
specifications.push(
match Specification::build(partitional, regional, common, zonal, spec) {
Ok(compiled_spec) => compiled_spec,
Err(e) => panic!("failed to build Specification: {}", e),
},
)
}
specifications
}
fn get_partitional(common: &PathBuf, regional: &PathBuf) -> PathBuf {
let re = Regex::new(r"partition: ([^\n\r$]+)").expect("failed");
[common, regional]
.iter()
.find_map(|path| {
fs::read_to_string(path).ok().and_then(|contents| {
re.captures(&contents).and_then(|captures| {
captures.get(1).map(|matched| {
PathBuf::from(format!("./spec/common/{}.yaml", matched.as_str().trim()))
})
})
})
})
.unwrap_or_else(|| PathBuf::from("./spec/common/default.yaml"))
}
fn get_regional(path: &PathBuf) -> PathBuf {
let dir_name = path
.parent()
.unwrap()
.file_name()
.unwrap()
.to_string_lossy()
.into_owned();
path.parent()
.unwrap()
.parent()
.unwrap()
.join("common")
.join(format!("{}.yaml", &dir_name[0..2]))
}
fn get_common(spec: &PathBuf) -> PathBuf {
spec.with_file_name("common.yaml")
}
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"));
PathBuf::from(format!("{}.common.yaml", zone.to_str().unwrap()))
}
fn filter_specs(pattern: &Regex, spec_list: Vec<PathBuf>) -> Vec<PathBuf> {
spec_list
.into_iter()
.filter(|spec| {
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<PathBuf> {
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(root.join(path));
}
}
}
info!(
dbg,
"Skyforge found {} renderable devices in {}",
result.len(),
env::current_dir().unwrap().display()
);
result
}

View File

@@ -1,12 +1,11 @@
use crate::{LogLevel, crit, info};
use serde::Deserialize; use serde::Deserialize;
use yaml_serde; use yaml_serde;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct Structure { pub struct Structure {
pub files: Vec<String>, pub files: Vec<String>,
#[allow(dead_code)] //todo
pub platform: String, pub platform: String,
#[allow(dead_code)] //todo
pub variations: Vec<String>, pub variations: Vec<String>,
} }
@@ -17,15 +16,22 @@ impl Structure {
Ok(yaml_serde::from_str(&data)?) Ok(yaml_serde::from_str(&data)?)
} }
pub fn load_template_data(&self, base_dir: &std::path::Path) -> Vec<(String, String)> { pub fn load_template_data(
&self,
tmpl_dir: &std::path::Path,
dbg: LogLevel,
) -> Vec<(String, String)> {
self.files self.files
.iter() .iter()
.filter_map(|name| { .filter_map(|name| {
let path = base_dir.join(format!("{}.tera", name)); let template = tmpl_dir.join(format!("{}.tera", name));
match std::fs::read_to_string(&path) { match std::fs::read_to_string(&template) {
Ok(content) => Some((name.clone(), content)), Ok(content) => {
info!(dbg, " | {}", template.display());
Some((name.clone(), content))
}
Err(e) => { Err(e) => {
eprintln!("Skipping {}: {}", path.display(), e); crit!(dbg, "Skipping {}: {}", template.display(), e);
None None
} }
} }