use crate::{LogLevel, info, verb}; use regex::Regex; use serde_json::{Map, Value, json}; use std::path::{Path, PathBuf}; use std::{env, fmt, fs}; use walkdir::WalkDir; use yaml_serde; #[derive(Debug)] pub struct Specification { pub partitional: PathBuf, pub regional: PathBuf, pub common: PathBuf, pub zonal: PathBuf, pub device: PathBuf, pub compiled: Value, } impl Specification { pub fn build( partitional: PathBuf, regional: PathBuf, common: PathBuf, zonal: PathBuf, device: PathBuf, ) -> Result> { let json_values: Vec = [&partitional, ®ional, &common, &zonal, &device] .iter() .filter_map(|path| fs::read_to_string(path).ok()) .map(|content| yaml_serde::from_str::(&content)) .collect::, _>>()?; let merged_map: Map = json_values .into_iter() .filter_map(|v| v.as_object().cloned()) .flat_map(|obj| obj.into_values().filter_map(|v| v.as_object().cloned())) .flatten() .collect(); let compiled = json!({"data": merged_map}); Ok(Self { partitional, regional, common, zonal, device, compiled, }) } pub fn get_layer(&self) -> String { self.device .parent() .unwrap() .file_name() .unwrap() .to_string_lossy() .chars() .filter(|c| !c.is_numeric()) .collect::() } } impl fmt::Display for Specification { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "Specification:\nPartitional: {}\nRegional: {}\nCommon: {}\nZonal: {}\nDevice: {}\nCompiled:\n{}", 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)); info!( dbg, "Matched {} devices against '{}'", &spec_list.len(), &pattern ); for spec in &spec_list { verb!(dbg, " | {}", spec.display()); } let mut specifications: Vec = 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, ®ional); 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) -> Vec { 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 { 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 }