use genfile_core::
{
TemplateArchive,
ContentSource,
ContentResolver,
ContentStorage,
DefaultContentResolver,
FileContent,
WriteMode,
Value,
HandlebarsRenderer,
Error,
FileRef,
UrlRef,
};
use std::path::{ Path, PathBuf };
use std::collections::HashMap;
struct TemplateServiceResolver
{
templates: HashMap< String, String >,
}
impl TemplateServiceResolver
{
fn new() -> Self
{
let mut templates = HashMap::new();
templates.insert(
"https://templates.example.com/header.hbs".to_string(),
"=== {{title}} ===\n".to_string(),
);
templates.insert(
"https://templates.example.com/footer.hbs".to_string(),
"---\n{{copyright}}\n".to_string(),
);
templates.insert(
"/local/templates/body.hbs".to_string(),
"Content: {{body}}\n".to_string(),
);
Self { templates }
}
}
impl ContentResolver for TemplateServiceResolver
{
fn resolve( &self, source: &ContentSource ) -> Result< FileContent, Error >
{
match source
{
ContentSource::Inline { content } => Ok( content.clone() ),
ContentSource::File { path } =>
{
let key = path.display().to_string();
match self.templates.get( &key )
{
Some( content ) => Ok( FileContent::Text( content.clone() ) ),
None => Err( Error::Render( format!( "Template not found: {key}" ) ) ),
}
}
ContentSource::Url { url } =>
{
match self.templates.get( url )
{
Some( content ) => Ok( FileContent::Text( content.clone() ) ),
None => Err( Error::Render( format!( "URL not found: {url}" ) ) ),
}
}
}
}
}
struct CloudStorage
{
pub objects: HashMap< String, Vec< u8 > >,
}
impl CloudStorage
{
fn new() -> Self
{
Self
{
objects: HashMap::new(),
}
}
fn get_object( &self, key: &str ) -> Option< &Vec< u8 > >
{
self.objects.get( key )
}
fn object_count( &self ) -> usize
{
self.objects.len()
}
}
impl ContentStorage for CloudStorage
{
fn store( &mut self, path: &Path, content: &FileContent ) -> Result< (), Error >
{
let key = path.display().to_string();
let bytes = match content
{
FileContent::Text( text ) => text.as_bytes().to_vec(),
FileContent::Binary( bytes ) => bytes.clone(),
};
self.objects.insert( key, bytes );
Ok( () )
}
}
#[ test ]
fn complete_workflow_example()
{
println!( "\n=== Complete Template Archive Workflow ===" );
println!( "\n1. Creating archive with mixed inline and external sources..." );
let mut archive = TemplateArchive::new( "document-template" );
archive.set_version( "1.0.0" );
archive.set_description( "Complete document template example" );
archive.add_text_file(
PathBuf::from( "index.txt" ),
"{{header}}{{body}}{{footer}}",
WriteMode::Rewrite
);
archive.add_file_from(
PathBuf::from( "header.txt" ),
UrlRef::new( "https://templates.example.com/header.hbs" ),
WriteMode::Rewrite
);
archive.add_file_from(
PathBuf::from( "body.txt" ),
FileRef::new( PathBuf::from( "/local/templates/body.hbs" ) ),
WriteMode::Rewrite
);
archive.add_file_from(
PathBuf::from( "footer.txt" ),
UrlRef::new( "https://templates.example.com/footer.hbs" ),
WriteMode::Rewrite
);
let file_count = archive.file_count();
assert_eq!( file_count, 4 );
println!( " Created archive with {file_count} files" );
let external_count = archive.list_files().iter()
.filter( | p | archive.get_file( p ).unwrap().content_source.is_some() )
.count();
println!( " {external_count} files have external sources" );
println!( "\n2. Internalizing external sources..." );
let resolver = TemplateServiceResolver::new();
archive.internalize( &resolver ).unwrap();
let external_after = archive.list_files().iter()
.filter( | p | archive.get_file( p ).unwrap().content_source.is_some() )
.count();
println!( " After internalization: {external_after} external sources" );
assert_eq!( external_after, 0 );
println!( "\n3. Setting parameter values..." );
archive.set_value( "title", Value::String( "My Document".into() ) );
archive.set_value( "body", Value::String( "Important content here".into() ) );
archive.set_value( "copyright", Value::String( "2024 Example Corp".into() ) );
archive.set_value( "header", Value::String( "=== My Document ===\n".into() ) );
archive.set_value( "footer", Value::String( "---\n2024 Example Corp\n".into() ) );
let values_count = archive.values_mut().len();
println!( " Set {values_count} parameter values" );
println!( "\n4. Materializing to cloud storage..." );
let renderer = HandlebarsRenderer::new();
let mut cloud = CloudStorage::new();
archive.materialize_with_storage(
Path::new( "s3://my-bucket/output" ),
&renderer,
&mut cloud,
&resolver
).unwrap();
let object_count = cloud.object_count();
println!( " Stored {object_count} objects in cloud" );
assert_eq!( object_count, 4 );
let header_obj = cloud.get_object( "s3://my-bucket/output/header.txt" ).unwrap();
let header_text = String::from_utf8_lossy( header_obj );
assert_eq!( header_text, "=== My Document ===\n" );
let body_obj = cloud.get_object( "s3://my-bucket/output/body.txt" ).unwrap();
let body_text = String::from_utf8_lossy( body_obj );
assert_eq!( body_text, "Content: Important content here\n" );
println!( "\n=== Workflow Complete ===" );
println!( "Successfully demonstrated:" );
println!( " ✓ Mixed inline and external content sources" );
println!( " ✓ Internalization of external references" );
println!( " ✓ Template rendering with parameters" );
println!( " ✓ Materialization to custom storage backend" );
}
#[ test ]
fn workflow_serialize_deserialize()
{
println!( "\n=== Serialization Workflow ===" );
let mut archive = TemplateArchive::new( "config-template" );
archive.add_text_file(
PathBuf::from( "app.conf" ),
"port={{port}}\nhost={{host}}",
WriteMode::Rewrite
);
archive.add_file_from(
PathBuf::from( "database.conf" ),
UrlRef::new( "https://configs.example.com/db.conf" ),
WriteMode::Rewrite
);
println!( "\n1. Serializing archive with external references..." );
let json_with_refs = archive.to_json_pretty().unwrap();
let json_with_refs_len = json_with_refs.len();
println!( " JSON size with refs: {json_with_refs_len} bytes" );
assert!( json_with_refs.contains( "content_source" ) );
println!( "\n2. Internalizing content..." );
let mut resolver = TemplateServiceResolver::new();
resolver.templates.insert(
"https://configs.example.com/db.conf".to_string(),
"connection_string={{db_url}}".to_string(),
);
archive.internalize( &resolver ).unwrap();
println!( "\n3. Serializing archive with inline content..." );
let json_inline = archive.to_json_pretty().unwrap();
let json_inline_len = json_inline.len();
println!( " JSON size inline: {json_inline_len} bytes" );
assert!( !json_inline.contains( "content_source" ) );
println!( "\n4. Deserializing archive..." );
let restored = TemplateArchive::from_json( &json_inline ).unwrap();
assert_eq!( restored.name, "config-template" );
assert_eq!( restored.file_count(), 2 );
let db_file = restored.get_file( Path::new( "database.conf" ) ).unwrap();
assert!( db_file.content_source.is_none() );
match &db_file.content
{
FileContent::Text( s ) => assert_eq!( s, "connection_string={{db_url}}" ),
FileContent::Binary( _ ) => panic!( "Expected text" ),
}
println!( "\n=== Serialization Workflow Complete ===" );
}
#[ test ]
fn workflow_roundtrip_with_parameters()
{
println!( "\n=== Round-trip Workflow with Parameters ===" );
let mut archive = TemplateArchive::new( "app-template" );
archive.add_text_file(
PathBuf::from( "config.yaml" ),
"app_name: {{name}}\nversion: {{version}}\n",
WriteMode::Rewrite
);
archive.add_binary_file(
PathBuf::from( "logo.png" ),
vec![ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A ]
);
use genfile_core::ParameterDescriptor;
archive.add_parameter( ParameterDescriptor
{
parameter: "name".into(),
is_mandatory: true,
default_value: None,
description: Some( "Application name".into() ),
});
archive.add_parameter( ParameterDescriptor
{
parameter: "version".into(),
is_mandatory: true,
default_value: Some( "1.0.0".into() ),
description: None,
});
archive.set_value( "name", Value::String( "MyApp".into() ) );
archive.set_value( "version", Value::String( "2.0.0".into() ) );
println!( "\n1. Serializing archive with parameters and values..." );
let json = archive.to_json_pretty().unwrap();
println!( "\n2. Deserializing archive..." );
let restored = TemplateArchive::from_json( &json ).unwrap();
assert_eq!( restored.list_parameters().len(), 2 );
assert_eq!( restored.get_value( "name" ), Some( &Value::String( "MyApp".into() ) ) );
assert_eq!( restored.get_value( "version" ), Some( &Value::String( "2.0.0".into() ) ) );
println!( "\n3. Materializing restored archive..." );
let renderer = HandlebarsRenderer::new();
let mut storage = CloudStorage::new();
let resolver = DefaultContentResolver::new();
restored.materialize_with_storage(
Path::new( "/output" ),
&renderer,
&mut storage,
&resolver
).unwrap();
assert_eq!( storage.object_count(), 2 );
let config = storage.get_object( "/output/config.yaml" ).unwrap();
let config_text = String::from_utf8_lossy( config );
assert_eq!( config_text, "app_name: MyApp\nversion: 2.0.0\n" );
let logo = storage.get_object( "/output/logo.png" ).unwrap();
assert_eq!( logo, &vec![ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A ] );
println!( "\n=== Round-trip Complete ===" );
}