diff --git a/Cargo.lock b/Cargo.lock index 75356da..8946aa5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1266,7 +1266,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "weaveconfig" -version = "0.2.0" +version = "0.3.0" dependencies = [ "anyhow", "biome_formatter", diff --git a/Cargo.toml b/Cargo.toml index fd3e4f8..5a98f61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "weaveconfig" -version = "0.2.0" +version = "0.3.0" edition = "2021" description = "A unified configuration tool for monorepos" readme = "README.md" diff --git a/src/apply_resolved.rs b/src/apply_resolved.rs index 1165be6..e7786a3 100644 --- a/src/apply_resolved.rs +++ b/src/apply_resolved.rs @@ -1,15 +1,20 @@ use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, path::{Path, PathBuf}, }; use anyhow::Context; use futures::{stream::FuturesUnordered, StreamExt}; +use serde_json::{Map, Value}; use crate::{ - get_environment_value::get_environment_value, map_path::map_path, - resolve_spaces::ResolvedSpace, template_file::template_file, - ts_binding::generate_binding::generate_binding, write_json_file::write_json_file, + get_environment_value::get_environment_value, + map_path::map_path, + resolve_spaces::ResolvedSpace, + space_graph::{CopyTree, ToCopy}, + template_file::template_file, + ts_binding::generate_binding::generate_binding, + write_json_file::write_json_file, }; async fn gen_folder(real_path: &PathBuf) -> Result { @@ -62,65 +67,164 @@ async fn write_gitignore(gen_folder: &PathBuf) -> Result<(), anyhow::Error> { Ok(()) } +// Function to write files and directories to be copied async fn write_to_copy(space: &ResolvedSpace, real_path: &Path) -> Result<(), anyhow::Error> { - // Prepare variables once to avoid cloning multiple times - let variables = space.variables.clone().unwrap_or_default(); + // Copy the tree structure with files and directories + copy_tree( + &space.files_to_copy, + real_path, + None, + &space.variables, + &space.environments, + ) + .await + .with_context(|| format!("Failed to copy tree structure for: {}", real_path.display()))?; - for to_copy in &space.files_to_copy { - // Determine the relative destination path - let dest_relative = to_copy - .path - .strip_prefix(&space.path) - .with_context(|| format!("Failed to strip prefix for {}", to_copy.path.display()))?; - - // Read the file content asynchronously - let content = tokio::fs::read_to_string(&to_copy.path) - .await - .with_context(|| format!("Failed to read file: {}", to_copy.path.display()))?; - - // Compute the full destination path - let mapped_dist = real_path.join(dest_relative); - - // Create parent directories if they don't exist - if let Some(parent) = mapped_dist.parent() { - tokio::fs::create_dir_all(parent) - .await - .with_context(|| format!("Failed to create directories: {}", parent.display()))?; - } - - if to_copy.for_each_env { - // Iterate over each environment and write templated files - for env in &space.environments { - let env_variables = get_environment_value(&variables, env) - .with_context(|| format!("Failed to get variables for environment: {}", env))?; + Ok(()) +} - let templated_content = - template_file(&content, &env_variables).with_context(|| { - format!( - "Failed to template file for environment {}: {}", - env, - to_copy.path.display() +// Recursive function to copy a tree of files and directories +async fn copy_tree( + copytree: &CopyTree, + copy_into: &Path, + env: Option<&str>, + variables: &Option>, + environments: &HashSet, +) -> Result<(), anyhow::Error> { + for to_copy in ©tree.to_copy { + let prefix = "_forenv"; + // Check if the file/directory name needs environment-specific substitution + if needs_substitution( + &to_copy + .last_segment() + .with_context(|| format!("Failed to get last segment for {:?}", to_copy))?, + prefix, + ) { + match env { + // If environment is specified, copy with that environment + Some(env) => { + copy_tocopy_with_env(to_copy, copy_into, Some(env), variables, environments) + .await + .with_context(|| { + format!("Failed to copy {:?} with environment: {}", to_copy, env) + })?; + } + // If no environment is specified, copy for all environments + None => { + for env in environments { + // Get environment-specific variables + let variables = match variables { + Some(variables) => { + Some(get_environment_value(variables, env).with_context(|| { + format!( + "Failed to get environment value for '{}' in {:?}", + env, variables + ) + })?) + } + None => None, + }; + copy_tocopy_with_env( + to_copy, + copy_into, + Some(env), + &variables, + environments, ) - })?; - - let dest = mapped_dist.with_file_name(format!("{}.{}", env, to_copy.dest_filename)); - - tokio::fs::write(&dest, templated_content) - .await - .with_context(|| format!("Failed to write file: {}", dest.display()))?; + .await + .with_context(|| { + format!("Failed to copy {:?} for environment: {}", to_copy, env) + })?; + } + } } } else { - // Template the content once and write to the destination - let templated_content = template_file(&content, &variables) - .with_context(|| format!("Failed to template file: {}", to_copy.path.display()))?; + // If no environment substitution is needed, copy without environment + copy_tocopy_with_env(to_copy, copy_into, None, variables, environments) + .await + .with_context(|| { + format!( + "Failed to copy {:?} without environment substitution", + to_copy + ) + })?; + } + } - let dest = mapped_dist.with_file_name(&to_copy.dest_filename); + Ok(()) +} - tokio::fs::write(&dest, templated_content) +// Function to copy a single file or directory with environment-specific handling +async fn copy_tocopy_with_env( + to_copy: &ToCopy, + copy_into: &Path, + env: Option<&str>, + variables: &Option>, + environments: &HashSet, +) -> Result<(), anyhow::Error> { + let last_segment = to_copy + .last_segment() + .with_context(|| "Failed to get last segment")?; + // Substitute environment in the file/directory name if needed + let substituted_name = match env { + Some(env) => substitute_path_segment(last_segment, "_forenv", env), + None => last_segment.to_string(), + }; + let destination = copy_into.join(substituted_name); + + match to_copy { + ToCopy::File(file) => { + // Read file content + let content = tokio::fs::read_to_string(&file) .await - .with_context(|| format!("Failed to write file: {}", dest.display()))?; + .with_context(|| format!("Failed to read file: {:?}", file))?; + // Apply variable substitution if variables are provided + let content = if let Some(variables) = variables { + template_file(&content, variables) + .with_context(|| "Failed to apply variable substitution")? + } else { + content + }; + // Write the processed content to the destination + tokio::fs::write(&destination, content) + .await + .with_context(|| format!("Failed to write to destination: {:?}", destination))?; + } + ToCopy::Directory { subtree, .. } => { + // Create the directory if it doesn't exist + if !destination.exists() { + tokio::fs::create_dir(&destination) + .await + .with_context(|| format!("Failed to create directory: {:?}", destination))?; + } + // Recursively copy the subdirectory + Box::pin(copy_tree( + subtree, + &destination, + env, + variables, + environments, + )) + .await + .with_context(|| { + format!("Failed to recursively copy subdirectory: {:?}", destination) + })?; } } Ok(()) } + +// Function to substitute environment in a path segment +fn substitute_path_segment(segment: &str, from: &str, to: &str) -> String { + if needs_substitution(segment, from) { + segment.replacen(from, to, 1) + } else { + segment.to_string() + } +} + +// Function to check if a segment needs environment substitution +fn needs_substitution(segment: &str, from: &str) -> bool { + segment.starts_with(from) +} diff --git a/src/file_graph.rs b/src/file_graph.rs index e1f4ccc..103fa06 100644 --- a/src/file_graph.rs +++ b/src/file_graph.rs @@ -11,7 +11,7 @@ pub struct Directory { pub path: PathBuf, pub parent_directory: Option, pub space: Option, - pub rest_to_copy: Vec, + pub rest_to_copy: Vec } #[derive(Debug, Clone, PartialEq)] @@ -20,12 +20,7 @@ pub struct SpaceNode { pub variables: Option>, } -#[derive(Debug, Clone, PartialEq)] -pub struct FileToCopy { - pub path: PathBuf, - pub dest_filename: String, - pub for_each_env: bool, -} +const FORENV_PREFIX: &str = "_forenv"; /// Creates a graph of the weaveconfig configuration. /// The root of the graph is typically the `weaveconfig` directory within the project root. @@ -41,7 +36,7 @@ pub async fn traverse_directory( path, parent_directory: None, space: None, - rest_to_copy: Vec::new(), + rest_to_copy: Vec::new() }; locate_directories(&mut root_directory).await?; @@ -76,7 +71,7 @@ async fn locate_directories(directory: &mut Directory) -> Result<(), anyhow::Err path: entry_path.clone(), parent_directory: Some(parent_path.clone()), space: None, - rest_to_copy: Vec::new(), + rest_to_copy: Vec::new() }; if let Err(e) = locate_directories(&mut sub_directory).await { @@ -143,7 +138,7 @@ async fn locate_directories(directory: &mut Directory) -> Result<(), anyhow::Err enum FileType { Space(SpaceSchema), Variables(serde_json::Map), - Rest(FileToCopy), + Rest(PathBuf), } async fn process_file(file_path: PathBuf) -> Result { @@ -195,13 +190,8 @@ async fn process_file(file_path: PathBuf) -> Result { map.insert(prefix, serde_json::Value::Object(variables)); Ok(FileType::Variables(map)) } - segments if segments.first() == Some(&"_forenv") => { - let dest_filename = segments[1..].join("."); - Ok(FileType::Rest(FileToCopy { - path: file_path, - dest_filename, - for_each_env: true, - })) + segments if segments.first() == Some(&FORENV_PREFIX) => { + Ok(FileType::Rest(file_path)) } _ => Err(anyhow!( "Invalid file name format: '{}'. Expected '_space.json', '_env.json', '__env.json' or '_forenv.'.", @@ -209,11 +199,7 @@ async fn process_file(file_path: PathBuf) -> Result { )), } } else { - Ok(FileType::Rest(FileToCopy { - path: file_path.clone(), - dest_filename: file_name.to_string(), - for_each_env: false, - })) + Ok(FileType::Rest(file_path)) } } diff --git a/src/get_environment_value.rs b/src/get_environment_value.rs index a2da108..5acf05a 100644 --- a/src/get_environment_value.rs +++ b/src/get_environment_value.rs @@ -2,6 +2,8 @@ use serde_json::{Map, Value}; use crate::template_file::value_type; +/// Get the environment variables for a given environment +/// This involves merging the key of the environment into the root overwriting any existing keys pub fn get_environment_value( variables: &Map, environment: &str, diff --git a/src/resolve_spaces.rs b/src/resolve_spaces.rs index e3ae46b..08c02ce 100644 --- a/src/resolve_spaces.rs +++ b/src/resolve_spaces.rs @@ -1,7 +1,6 @@ use crate::{ - file_graph::FileToCopy, merging::merge_map_consume, - space_graph::{GenerateSpace, SpaceGraph}, + space_graph::{CopyTree, GenerateSpace, SpaceGraph}, }; use anyhow::{Context, Result}; use serde_json::{Map, Value}; @@ -15,7 +14,7 @@ pub struct ResolvedSpace { pub variables: Option>, pub environments: HashSet, pub path: PathBuf, - pub files_to_copy: Vec, + pub files_to_copy: CopyTree, pub generate: GenerateSpace, } diff --git a/src/space_graph.rs b/src/space_graph.rs index 73da5fe..4a53e7c 100644 --- a/src/space_graph.rs +++ b/src/space_graph.rs @@ -1,9 +1,8 @@ use std::{collections::HashMap, path::PathBuf}; -use crate::{ - file_graph::{Directory, FileToCopy}, - schemas::GenerateSchema, -}; +use anyhow::Context; + +use crate::{file_graph::Directory, schemas::GenerateSchema}; use std::collections::HashSet; #[derive(Debug, Clone, PartialEq)] @@ -14,11 +13,36 @@ pub struct Space { pub mapping: Option>>, pub environments: HashSet, pub variables: Option>, - pub files_to_copy: Vec, + pub files_to_copy: CopyTree, pub parent_space: Option, pub generate: GenerateSpace, } +#[derive(Debug, Clone, PartialEq)] +pub struct CopyTree { + pub to_copy: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ToCopy { + File(PathBuf), + Directory { path: PathBuf, subtree: CopyTree }, +} + +impl ToCopy { + pub fn last_segment(&self) -> Result<&str, anyhow::Error> { + let path = match self { + ToCopy::File(path) => path, + ToCopy::Directory { path, .. } => path, + }; + let file_name = path.file_name().context("File has no name")?; + let file_name = file_name + .to_str() + .context("File name is not valid unicode")?; + Ok(file_name) + } +} + #[derive(Debug, Clone, PartialEq)] pub struct GenerateSpace { pub generate: bool, @@ -46,9 +70,7 @@ fn add_to_spaces_graph( .map(|s| s.schema.name.to_string()) .or_else(|| closest_parent_space.clone()); if let Some(space) = dir.space.take() { - let mut files_to_copy = vec![]; - resolve_files_to_copy(&dir, &mut files_to_copy); - let mut space = Space { + let space = Space { name: space.schema.name, path: dir.path.clone(), dependencies: space.schema.dependencies.unwrap_or_default(), @@ -61,7 +83,7 @@ fn add_to_spaces_graph( }), environments: space.schema.environments.unwrap_or_default(), variables: space.variables, - files_to_copy: vec![], + files_to_copy: resolve_files_to_copy(&dir), parent_space: closest_parent_space, generate: { match space.schema.generate { @@ -80,7 +102,6 @@ fn add_to_spaces_graph( } }, }; - resolve_files_to_copy(&dir, &mut space.files_to_copy); space_graph.insert(space.name.clone(), space); } @@ -89,14 +110,20 @@ fn add_to_spaces_graph( } } -fn resolve_files_to_copy(dir: &Directory, files: &mut Vec) { +fn resolve_files_to_copy(dir: &Directory) -> CopyTree { + let mut files = vec![]; for file in &dir.rest_to_copy { - files.push(file.clone()); + files.push(ToCopy::File(file.clone())); } for entry in &dir.directories { if entry.space.is_none() { - resolve_files_to_copy(&entry, files) + files.push(ToCopy::Directory { + path: entry.path.clone(), + subtree: resolve_files_to_copy(entry), + }); } } + + CopyTree { to_copy: files } }