use std::collections::{ HashMap, HashSet };
use std::path::{ Path, PathBuf };
use crate::
{
ParameterDescriptor,
Parameters,
Value,
Values,
WriteMode,
};
#[derive(Debug, Clone)]
#[cfg_attr(any(feature = "json", feature = "yaml"), derive(serde::Serialize, serde::Deserialize))]
pub struct TemplateArchive
{
pub name: String,
#[cfg_attr(any(feature = "json", feature = "yaml"), serde(default = "default_version"))]
pub version: String,
#[cfg_attr(any(feature = "json", feature = "yaml"), serde(skip_serializing_if = "Option::is_none"))]
pub description: Option< String >,
#[cfg_attr(any(feature = "json", feature = "yaml"), serde(default))]
pub files: Vec< TemplateFile >,
#[cfg_attr(any(feature = "json", feature = "yaml"), serde(default))]
pub parameters: Parameters,
#[cfg_attr(any(feature = "json", feature = "yaml"), serde(skip_serializing_if = "Option::is_none"))]
pub values: Option< Values< Value > >,
#[cfg_attr(any(feature = "json", feature = "yaml"), serde(skip_serializing_if = "Option::is_none"))]
pub metadata: Option< ArchiveMetadata >,
}
fn default_version() -> String
{
"0.1.0".to_string()
}
#[derive(Debug, Clone)]
#[cfg_attr(any(feature = "json", feature = "yaml"), derive(serde::Serialize, serde::Deserialize))]
pub struct ArchiveMetadata
{
#[cfg_attr(any(feature = "json", feature = "yaml"), serde(skip_serializing_if = "Option::is_none"))]
pub author: Option< String >,
#[cfg_attr(any(feature = "json", feature = "yaml"), serde(skip_serializing_if = "Option::is_none"))]
pub license: Option< String >,
#[cfg_attr(any(feature = "json", feature = "yaml"), serde(default, skip_serializing_if = "Vec::is_empty"))]
pub tags: Vec< String >,
#[cfg_attr(any(feature = "json", feature = "yaml"), serde(skip_serializing_if = "Option::is_none"))]
pub created_at: Option< String >,
#[cfg_attr(any(feature = "json", feature = "yaml"), serde(skip_serializing_if = "Option::is_none"))]
pub modified_at: Option< String >,
}
#[derive(Debug, Clone)]
#[cfg_attr(any(feature = "json", feature = "yaml"), derive(serde::Serialize, serde::Deserialize))]
pub struct TemplateFile
{
pub path: PathBuf,
pub content: FileContent,
pub write_mode: WriteMode,
#[cfg_attr(any(feature = "json", feature = "yaml"), serde(skip_serializing_if = "Option::is_none"))]
pub metadata: Option< FileMetadata >,
#[cfg_attr(any(feature = "json", feature = "yaml"), serde(skip_serializing_if = "Option::is_none"))]
#[cfg(feature = "external_content")]
pub content_source: Option< crate::ContentSource >,
}
#[derive(Debug, Clone)]
#[cfg_attr(any(feature = "json", feature = "yaml"), derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(any(feature = "json", feature = "yaml"), serde(tag = "type", content = "data"))]
pub enum FileContent
{
Text( String ),
#[ cfg_attr( all( any( feature = "json", feature = "yaml" ), feature = "binary" ), serde( with = "base64_encoding" ) ) ]
Binary( Vec< u8 > ),
}
#[ cfg( all( any( feature = "json", feature = "yaml" ), feature = "binary" ) ) ]
mod base64_encoding
{
use serde::{ Deserialize, Deserializer, Serializer };
pub fn serialize< S >( bytes: &Vec< u8 >, serializer: S ) -> Result< S::Ok, S::Error >
where
S: Serializer,
{
use base64::Engine;
let base64_string = base64::engine::general_purpose::STANDARD.encode( bytes );
serializer.serialize_str( &base64_string )
}
pub fn deserialize< 'de, D >( deserializer: D ) -> Result< Vec< u8 >, D::Error >
where
D: Deserializer< 'de >,
{
use base64::Engine;
let base64_string = String::deserialize( deserializer )?;
base64::engine::general_purpose::STANDARD
.decode( base64_string )
.map_err( serde::de::Error::custom )
}
}
#[derive(Debug, Clone)]
#[cfg_attr(any(feature = "json", feature = "yaml"), derive(serde::Serialize, serde::Deserialize))]
pub struct FileMetadata
{
#[cfg_attr(any(feature = "json", feature = "yaml"), serde(skip_serializing_if = "Option::is_none"))]
pub permissions: Option< u32 >,
#[cfg_attr(any(feature = "json", feature = "yaml"), serde(default))]
pub is_template: bool,
#[cfg_attr(any(feature = "json", feature = "yaml"), serde(skip_serializing_if = "Option::is_none"))]
pub comment: Option< String >,
}
impl TemplateArchive
{
pub fn new( name: impl Into< String > ) -> Self
{
Self
{
name: name.into(),
version: default_version(),
description: None,
files: Vec::new(),
parameters: Parameters::default(),
values: None,
metadata: None,
}
}
pub fn set_version( &mut self, version: impl Into< String > ) -> &mut Self
{
self.version = version.into();
self
}
pub fn set_description( &mut self, desc: impl Into< String > ) -> &mut Self
{
self.description = Some( desc.into() );
self
}
pub fn set_metadata( &mut self, metadata: ArchiveMetadata ) -> &mut Self
{
self.metadata = Some( metadata );
self
}
pub fn add_file(
&mut self,
path: PathBuf,
content: FileContent,
write_mode: WriteMode
) -> &mut Self
{
self.files.push( TemplateFile
{
path,
content,
write_mode,
metadata: None,
#[cfg(feature = "external_content")]
content_source: None,
});
self
}
pub fn add_text_file(
&mut self,
path: PathBuf,
content: impl Into< String >,
write_mode: WriteMode
) -> &mut Self
{
self.add_file( path, FileContent::Text( content.into() ), write_mode )
}
pub fn add_binary_file( &mut self, path: PathBuf, content: Vec< u8 > ) -> &mut Self
{
self.add_file( path, FileContent::Binary( content ), WriteMode::Rewrite )
}
#[cfg(feature = "external_content")]
pub fn add_file_from< S >(
&mut self,
path: PathBuf,
source: S,
write_mode: WriteMode
) -> &mut Self
where
S: crate::IntoContentSource,
{
let content_source = source.into_content_source();
self.files.push( TemplateFile
{
path,
content: FileContent::Text( String::new() ), write_mode,
metadata: None,
content_source: Some( content_source ),
});
self
}
pub fn remove_file( &mut self, path: &Path ) -> Option< TemplateFile >
{
self.files
.iter()
.position( | f | f.path == path )
.map( | idx | self.files.remove( idx ) )
}
#[must_use]
pub fn get_file( &self, path: &Path ) -> Option< &TemplateFile >
{
self.files.iter().find( | f | f.path == path )
}
pub fn get_file_mut( &mut self, path: &Path ) -> Option< &mut TemplateFile >
{
self.files.iter_mut().find( | f | f.path == path )
}
#[must_use]
pub fn has_file( &self, path: &Path ) -> bool
{
self.files.iter().any( | f | f.path == path )
}
#[must_use]
pub fn list_files( &self ) -> Vec< &Path >
{
self.files.iter().map( | f | f.path.as_path() ).collect()
}
#[must_use]
pub fn list_directories( &self ) -> Vec< PathBuf >
{
let mut dirs = HashSet::new();
for file in &self.files
{
let mut current = file.path.as_path();
while let Some( parent ) = current.parent()
{
if parent.as_os_str().is_empty()
{
break;
}
dirs.insert( parent.to_path_buf() );
current = parent;
}
}
let mut result: Vec< _ > = dirs.into_iter().collect();
result.sort();
result
}
pub fn add_parameter( &mut self, param: ParameterDescriptor ) -> &mut Self
{
self.parameters.descriptors.push( param );
self
}
pub fn remove_parameter( &mut self, name: &str ) -> Option< ParameterDescriptor >
{
self.parameters
.descriptors
.iter()
.position( | p | p.parameter == name )
.map( | idx | self.parameters.descriptors.remove( idx ) )
}
#[must_use]
pub fn get_parameter( &self, name: &str ) -> Option< &ParameterDescriptor >
{
self.parameters.descriptors.iter().find( | p | p.parameter == name )
}
#[must_use]
pub fn list_parameters( &self ) -> Vec< &str >
{
self.parameters.descriptors.iter().map( | p | p.parameter.as_str() ).collect()
}
#[must_use]
pub fn list_mandatory_parameters( &self ) -> Vec< &str >
{
self.parameters.list_mandatory()
}
#[cfg(feature = "parameter_discovery")]
#[must_use]
pub fn discover_parameters( &self ) -> HashSet< String >
{
let mut params = HashSet::new();
let pattern = regex::Regex::new( r"\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}" ).unwrap();
for file in &self.files
{
if let FileContent::Text( content ) = &file.content
{
for cap in pattern.captures_iter( content )
{
if let Some( param_name ) = cap.get( 1 )
{
params.insert( param_name.as_str().to_string() );
}
}
}
}
params
}
#[cfg(feature = "parameter_discovery")]
#[must_use]
pub fn get_undefined_parameters( &self ) -> Vec< String >
{
let discovered = self.discover_parameters();
let defined: HashSet< _ > = self.list_parameters().into_iter().collect();
discovered
.into_iter()
.filter( | p | !defined.contains( p.as_str() ) )
.collect()
}
#[cfg(feature = "parameter_discovery")]
pub fn get_unused_parameters( &self ) -> Vec< String >
{
let discovered = self.discover_parameters();
let defined = self.list_parameters();
defined
.into_iter()
.filter( | p | !discovered.contains( *p ) )
.map( String::from )
.collect()
}
#[cfg(feature = "parameter_discovery")]
#[must_use]
pub fn analyze_parameter_usage( &self ) -> HashMap< String, Vec< PathBuf > >
{
let mut usage: HashMap< String, Vec< PathBuf > > = HashMap::new();
let pattern = regex::Regex::new( r"\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}" ).unwrap();
for file in &self.files
{
if let FileContent::Text( content ) = &file.content
{
for cap in pattern.captures_iter( content )
{
if let Some( param_name ) = cap.get( 1 )
{
usage
.entry( param_name.as_str().to_string() )
.or_default()
.push( file.path.clone() );
}
}
}
}
usage
}
pub fn set_value( &mut self, name: impl Into< String >, value: Value ) -> &mut Self
{
let name_string = name.into();
if self.values.is_none()
{
self.values = Some( Values::new() );
}
self.values.as_mut().unwrap().insert( &name_string, value );
self
}
#[must_use]
pub fn get_value( &self, name: &str ) -> Option< &Value >
{
self.values.as_ref().and_then( | v | v.get( name ) )
}
pub fn set_values( &mut self, values: HashMap< String, Value > ) -> &mut Self
{
for ( name, value ) in values
{
self.set_value( name, value );
}
self
}
pub fn values_mut( &mut self ) -> &mut Values< Value >
{
if self.values.is_none()
{
self.values = Some( Values::new() );
}
self.values.as_mut().unwrap()
}
pub fn clear_values( &mut self )
{
self.values = None;
}
#[must_use]
pub fn file_count( &self ) -> usize
{
self.files.len()
}
#[must_use]
pub fn text_file_count( &self ) -> usize
{
self.files.iter().filter( | f | matches!( f.content, FileContent::Text( _ ) ) ).count()
}
#[must_use]
pub fn binary_file_count( &self ) -> usize
{
self.files.iter().filter( | f | matches!( f.content, FileContent::Binary( _ ) ) ).count()
}
#[must_use]
pub fn total_size( &self ) -> usize
{
self.files.iter().map( | f |
{
match &f.content
{
FileContent::Text( s ) => s.len(),
FileContent::Binary( b ) => b.len(),
}
}).sum()
}
#[must_use]
pub fn max_directory_depth( &self ) -> usize
{
self.files
.iter()
.map( | f | f.path.components().count().saturating_sub( 1 ) )
.max()
.unwrap_or( 0 )
}
}
mod io;
#[derive(Debug, Clone, Default)]
pub struct MaterializationReport
{
pub files_created: Vec< PathBuf >,
pub files_updated: Vec< PathBuf >,
pub files_skipped: Vec< PathBuf >,
pub directories_created: Vec< PathBuf >,
pub total_bytes_written: usize,
pub errors: Vec< ( PathBuf, String ) >,
}