use core::str;
use ewe_templates::minijinja;
use foundations_ext::strings_ext::{IntoStr, IntoString};
use std::{io::Write, marker::PhantomData, path::PathBuf, sync};
use crate::{error::BoxedError, FileContent, FileSystemCommand};
pub struct Directorate<T: rust_embed::RustEmbed> {
pub _data: PhantomData<T>,
}
impl<T: rust_embed::Embed + Default> Default for Directorate<T> {
fn default() -> Self {
Self {
_data: PhantomData::default(),
}
}
}
pub struct StringIterator(rust_embed::Filenames);
impl Iterator for StringIterator {
type Item = std::borrow::Cow<'static, str>;
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
}
pub trait PackageDirectorate {
fn get_file(&self, target_file: &str) -> Option<rust_embed::EmbeddedFile>;
fn files(&self) -> StringIterator;
fn files_for(&self, directory: &str) -> Option<Vec<String>>;
fn jinja_for<'a>(&self, directory: &str) -> Option<minijinja::Environment<'a>>;
fn root_directories(&self) -> Vec<String>;
fn list_all(&self) -> Vec<String>;
}
impl<T: rust_embed::Embed + 'static> Into<Box<dyn PackageDirectorate>> for Directorate<T> {
fn into(self) -> Box<dyn PackageDirectorate> {
Box::new(self)
}
}
impl<T: rust_embed::Embed> PackageDirectorate for Directorate<T> {
fn get_file(&self, target_file: &str) -> Option<rust_embed::EmbeddedFile> {
T::get(target_file)
}
fn files(&self) -> StringIterator {
StringIterator(T::iter())
}
fn root_directories(&self) -> Vec<String> {
let mut dirs: Vec<String> = T::iter()
.filter(|t| t.contains("/"))
.map(|t| match t.split_once("/") {
None => None,
Some((directory, _)) => Some(String::from(directory)),
})
.filter(|t| t.is_some())
.map(|t| t.unwrap())
.collect();
dirs.sort();
dirs.dedup();
dirs
}
fn list_all(&self) -> Vec<String> {
T::iter().map(|t| String::from(t)).collect()
}
fn files_for(&self, directory: &str) -> Option<Vec<String>> {
let target_dir = if directory.ends_with("/") {
directory
} else {
&format!("{}/", directory)
};
let files: Vec<String> = T::iter()
.filter(|t| t.starts_with(target_dir))
.map(|t| String::from(t))
.collect();
if files.is_empty() {
return None;
}
Some(files)
}
fn jinja_for<'a>(&self, directory: &str) -> Option<minijinja::Environment<'a>> {
let target_dir = if directory.ends_with("/") {
directory
} else {
&format!("{}/", directory)
};
let mut jinja_env = minijinja::Environment::new();
let items: Vec<std::borrow::Cow<'_, str>> =
T::iter().filter(|t| t.starts_with(target_dir)).collect();
if items.is_empty() {
return None;
}
for relevant_path in items.into_iter() {
let relevant_file = T::get(&relevant_path).unwrap();
let relevant_file_data = relevant_file.data.into_str().expect("should be string");
jinja_env
.add_template_owned(
relevant_path
.into_string()
.expect("should turn into String"),
relevant_file_data
.into_string()
.expect("convert into String"),
)
.expect("should store template");
}
Some(jinja_env)
}
}
#[cfg(test)]
mod directorate_tests {
use super::*;
#[derive(rust_embed::Embed, Default)]
#[folder = "templates/test_directory/"]
struct Directory;
#[test]
fn validate_can_create_directorate_generator_safely() {
let generator = Directorate::<Directory>::default();
assert!(matches!(generator.get_file("README.md"), Some(_)));
}
#[test]
fn validate_can_read_top_directories() {
let generator = Directorate::<Directory>::default();
let directories: Vec<String> = generator.root_directories();
assert_eq!(directories, vec! {"docs", "schema"});
}
#[test]
fn validate_can_read_only_files_for_top_directory() {
let generator = Directorate::<Directory>::default();
let files: Option<Vec<String>> = generator.files_for("schema");
assert_eq!(
files.unwrap(),
vec! {"schema/partials/partial_1.sql", "schema/schema.sql"}
);
}
#[test]
fn validate_can_read_all_directories() {
let generator = Directorate::<Directory>::default();
let files: Vec<String> = generator.files().map(|t| String::from(t)).collect();
assert_eq!(
files,
vec! {"README.md", "docs/runner.sh", "elem.js", "schema/partials/partial_1.sql", "schema/schema.sql"}
);
}
}
#[derive(Clone, Debug)]
pub struct PackageConfig {
pub params: serde_json::Map<String, serde_json::Value>,
pub output_directory: PathBuf,
pub template_name: String,
pub package_name: String,
}
impl PackageConfig {
pub fn new<S>(
output_directory: PathBuf,
params: serde_json::Map<String, serde_json::Value>,
template_name: S,
package_name: S,
) -> Self
where
S: Into<String>,
{
Self {
params,
template_name: template_name.into(),
package_name: package_name.into(),
output_directory,
}
}
}
pub trait PackageConfigurator {
fn directory(&self) -> std::path::PathBuf;
fn config(&self) -> PackageConfig;
fn params(&self) -> serde_json::Map<String, serde_json::Value>;
fn finalize(&self) -> std::result::Result<(), BoxedError>;
}
impl PackageConfigurator for Box<dyn PackageConfigurator> {
fn directory(&self) -> std::path::PathBuf {
(**self).directory()
}
fn config(&self) -> PackageConfig {
(**self).config()
}
fn params(&self) -> serde_json::Map<String, serde_json::Value> {
(**self).params()
}
fn finalize(&self) -> std::result::Result<(), BoxedError> {
(**self).finalize()
}
}
impl PackageConfigurator for PackageConfig {
fn directory(&self) -> std::path::PathBuf {
self.output_directory.join(self.package_name.clone())
}
fn config(&self) -> PackageConfig {
self.clone()
}
fn params(&self) -> serde_json::Map<String, serde_json::Value> {
self.params.clone()
}
fn finalize(&self) -> std::result::Result<(), BoxedError> {
Ok(())
}
}
pub struct RustConfig {
workspace_cargo: Option<PathBuf>,
retain_lib_section: bool,
}
impl RustConfig {
pub fn new(workspace_cargo: Option<PathBuf>, retain_lib_section: bool) -> Self {
Self {
workspace_cargo,
retain_lib_section,
}
}
#[allow(dead_code)]
pub fn standard(workspace_cargo: Option<PathBuf>) -> Self {
Self::new(workspace_cargo, false)
}
}
pub struct RustProjectConfigurator {
pub package_config: PackageConfig,
pub rust_config: Option<RustConfig>,
pub manifest: Option<cargo_toml::Manifest>,
}
pub type RustProjectConfiguratorResult<T> = core::result::Result<T, RustProjectConfiguratorError>;
#[derive(Debug, derive_more::From)]
pub enum RustProjectConfiguratorError {
BadCargoManifest(std::path::PathBuf),
#[from(ignore)]
NoCargoFile(std::path::PathBuf),
BadRustWorkspace,
BadRustProject,
}
impl core::error::Error for RustProjectConfiguratorError {}
impl core::fmt::Display for RustProjectConfiguratorError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl RustProjectConfigurator {
pub fn new(
package_config: PackageConfig,
rust_config: Option<RustConfig>,
) -> RustProjectConfiguratorResult<Self> {
Self {
package_config,
rust_config,
manifest: None,
}
.init()
}
fn init(mut self) -> RustProjectConfiguratorResult<Self> {
if let Some(rust_config) = &self.rust_config {
if let Some(workspace_cargo) = &rust_config.workspace_cargo {
let manifest =
cargo_toml::Manifest::from_path(workspace_cargo.clone()).map_err(|err| {
ewe_trace::error!("Failed to get cargo_toml::Manifest due to: {:?}", err);
RustProjectConfiguratorError::BadRustWorkspace
})?;
self.manifest = Some(manifest);
}
}
Ok(self)
}
}
impl PackageConfigurator for RustProjectConfigurator {
fn config(&self) -> PackageConfig {
self.package_config.clone()
}
fn directory(&self) -> std::path::PathBuf {
self.package_config.directory()
}
fn params(&self) -> serde_json::Map<String, serde_json::Value> {
let mut params = self.package_config.params.clone();
let output_directory_name = String::from(
self.package_config
.output_directory
.file_name()
.clone()
.unwrap()
.to_str()
.unwrap(),
);
params
.entry("TEMPLATE_NAME")
.or_insert(self.package_config.template_name.clone().into());
params
.entry("PACKAGE_NAME")
.or_insert(self.package_config.package_name.clone().into());
params.entry("PACKAGE_DIRECTORY").or_insert(
self.package_config
.directory()
.into_string()
.unwrap()
.into(),
);
if let Some(manifest) = &self.manifest {
if let Some(package) = &manifest.package {
params.entry("IS_WORKSPACE").or_insert(false.into());
params
.entry("PACKAGE_DIRECTORY_NAME")
.or_insert(output_directory_name.clone().into());
params
.entry("ROOT_PACKAGE_DOCUMENTATION")
.or_insert(package.documentation().unwrap_or("").into());
params
.entry("ROOT_PACKAGE_NAME")
.or_insert(package.name().into());
params
.entry("ROOT_PACKAGE_LICENSE_FILE")
.or_insert(package.license().unwrap_or("").into());
params
.entry("ROOT_PACKAGE_EDITION")
.or_insert(format!("{:?}", package.edition()).into());
params
.entry("ROOT_PACKAGE_REPOSITORY")
.or_insert(package.repository().unwrap_or("").into());
params
.entry("ROOT_PACKAGE_VERSION")
.or_insert(package.version().into());
params
.entry("ROOT_PACKAGE_RUST_VERSION")
.or_insert(package.rust_version().unwrap_or("").into());
params
.entry("ROOT_PACKAGE_LICENSE")
.or_insert(package.license().unwrap_or("").into());
params
.entry("ROOT_PACKAGE_DESCRIPTIONS")
.or_insert(package.description().unwrap_or("").into());
params
.entry("ROOT_PACKAGE_AUTHORS")
.or_insert(package.authors().into());
params
.entry("ROOT_PACKAGE_KEYWORDS")
.or_insert(package.keywords().into());
}
if let Some(workspace) = &manifest.workspace {
if let Some(package) = &workspace.package {
params.entry("IS_WORKSPACE").or_insert(true.into());
println!("Package name: {}", output_directory_name);
params
.entry("ROOT_PACKAGE_NAME")
.or_insert(output_directory_name.clone().into());
params.entry("ROOT_PACKAGE_DOCUMENTATION").or_insert(
package
.documentation
.clone()
.unwrap_or(String::from(""))
.into(),
);
params.entry("ROOT_PACKAGE_LICENSE_FILE").or_insert(
package
.license_file
.clone()
.unwrap_or(PathBuf::new())
.into_string()
.unwrap()
.into(),
);
if let Some(edition) = package.edition {
params
.entry("ROOT_PACKAGE_EDITION")
.or_insert(format!("{:?}", edition).into());
}
params.entry("ROOT_PACKAGE_REPOSITORY").or_insert(
package
.repository
.clone()
.unwrap_or("".into_string().unwrap())
.into(),
);
params.entry("ROOT_PACKAGE_VERSION").or_insert(
package
.rust_version
.clone()
.unwrap_or("".into_string().unwrap())
.into(),
);
params.entry("ROOT_PACKAGE_RUST_VERSION").or_insert(
package
.rust_version
.clone()
.unwrap_or("".into_string().unwrap())
.into(),
);
params.entry("ROOT_PACKAGE_LICENSE").or_insert(
package
.license
.clone()
.unwrap_or("".into_string().unwrap())
.into(),
);
params.entry("ROOT_PACKAGE_DESCRIPTIONS").or_insert(
package
.description
.clone()
.unwrap_or("".into_string().unwrap())
.into(),
);
params
.entry("ROOT_PACKAGE_AUTHORS")
.or_insert(package.authors.clone().unwrap_or(vec![]).into());
params
.entry("ROOT_PACKAGE_KEYWORDS")
.or_insert(package.keywords.clone().unwrap_or(vec![]).into());
}
}
}
params
}
fn finalize(&self) -> std::result::Result<(), BoxedError> {
if let Some(manifest) = &self.manifest {
let mut updated_manifest = manifest.clone();
if let Some(mut workspace) = updated_manifest.workspace.clone() {
if !workspace
.members
.contains(&self.package_config.package_name)
{
workspace
.members
.push(self.package_config.package_name.clone());
}
updated_manifest.workspace = Some(workspace);
if let Some(rust_config) = &self.rust_config {
let serilized_manifest = toml::to_string(&updated_manifest)?;
if let Some(workspace_cargo) = &rust_config.workspace_cargo {
let mut cargo_file = std::fs::File::create(workspace_cargo.clone())?;
cargo_file.write_all(serilized_manifest.as_bytes())?;
}
}
}
}
let project_directory = self.directory();
let project_cargo_file = project_directory.join("Cargo.toml");
if !project_cargo_file.exists() {
return Err(Box::new(RustProjectConfiguratorError::NoCargoFile(
project_cargo_file,
)));
}
match cargo_toml::Manifest::from_path(project_cargo_file.clone()).map_err(|err| {
ewe_trace::error!("Failed to get cargo_toml::Manifest due to: {:?}", err);
RustProjectConfiguratorError::BadCargoManifest(project_cargo_file.clone())
}) {
Ok(manifest) => {
let mut cloned_manifest = manifest.clone();
let mut manifest_package = cloned_manifest.package().clone();
if manifest_package.name == self.package_config.package_name {
return Ok(());
}
manifest_package.name = self.package_config.package_name.clone();
cloned_manifest.package = Some(manifest_package);
if let Some(rust_config) = &self.rust_config {
if !rust_config.retain_lib_section {
cloned_manifest.lib = None;
}
}
let serilized_manifest = toml::to_string(&cloned_manifest)?;
let mut cargo_file = std::fs::File::create(project_cargo_file.clone())?;
match cargo_file.write_all(serilized_manifest.as_bytes()) {
Ok(_) => Ok(()),
Err(err) => Err(Box::new(err)),
}
}
Err(err) => Err(Box::new(err)),
}
}
}
pub struct PackageGenerator {
pub templates: Box<dyn PackageDirectorate>,
}
pub type PackageGenResult<T> = core::result::Result<T, PackageGenError>;
#[derive(Debug, derive_more::From)]
pub enum PackageGenError {
Failed(crate::error::BoxedError),
NoTemplateFound(String),
}
impl std::error::Error for PackageGenError {}
impl core::fmt::Display for PackageGenError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:?}")
}
}
impl PackageGenerator {
pub fn new(templates: Box<dyn PackageDirectorate>) -> Self {
Self { templates }
}
pub fn create<S: PackageConfigurator>(&self, configurator: S) -> PackageGenResult<()> {
let config = configurator.config();
let template_files_container = self.templates.files_for(config.template_name.as_str());
ewe_trace::debug!(
"Project Template: `{}` with files: `{:?}`",
config.template_name,
template_files_container,
);
if template_files_container.is_none() {
return Err(PackageGenError::NoTemplateFound(
config.template_name.clone(),
));
}
let template_files = template_files_container.unwrap();
ewe_trace::debug!(
"Project Template: `{}` with files: `{:?}` where all=`{:?}`",
config.template_name,
template_files,
self.templates.list_all(),
);
let jinja_environment = self.templates.jinja_for(config.template_name.as_str());
if jinja_environment.is_none() {
return Err(PackageGenError::NoTemplateFound(
config.template_name.clone(),
));
}
let file_templates = sync::Arc::new(jinja_environment.unwrap());
let mut packager = crate::Templater::from(config.output_directory.clone());
for template_file in template_files.iter() {
let template_file_path = PathBuf::from(template_file.as_str());
if template_file_path.is_dir() || template_file_path.ends_with("/") {
continue;
}
let rewritten_template_file_name = config
.output_directory
.join(config.package_name.as_str())
.join(
template_file_path
.as_path()
.strip_prefix(config.template_name.as_str())
.expect(
format!("expected valid starting as `{}`", config.template_name)
.as_str(),
),
);
let rewritten_template_dir = rewritten_template_file_name
.parent()
.expect("should have parent directory");
let template_file_name =
String::from(template_file_path.file_name().unwrap().to_str().unwrap());
if template_file_name.starts_with("_") {
continue;
}
ewe_trace::debug!(
"Rewriting template path `{:?}` to `{:?}` (dir: {:?}",
template_file,
rewritten_template_file_name,
rewritten_template_dir,
);
packager.add(FileSystemCommand::DirPath(
PathBuf::from(rewritten_template_dir),
vec![FileSystemCommand::File(
template_file_name,
FileContent::Jinja(template_file.clone(), file_templates.clone()),
)
.into()],
));
}
packager
.run(configurator.params())
.map_err(|err| PackageGenError::Failed(err.into()))?;
configurator
.finalize()
.map_err(|err| PackageGenError::Failed(err))
}
}
#[cfg(test)]
mod package_generator_tests {
use std::fs;
use std::path;
use foundations_ext::strings_ext::IntoString;
use foundations_ext::vec_ext::VecExt;
use tracing_test::traced_test;
use super::*;
#[derive(rust_embed::Embed, Default)]
#[folder = "templates/"]
struct TemplateDefinitions;
fn list_dir(target_path: &path::Path) -> Vec<String> {
fs::read_dir(target_path)
.expect("directory should exists")
.into_iter()
.map(|entry| entry.unwrap())
.flat_map(|entry| {
if entry.file_type().unwrap().is_dir() {
return list_dir(&entry.path());
}
vec![entry.path().into_string().unwrap()]
})
.collect()
}
fn shorten_path(target: Vec<String>, path: String) -> Vec<String> {
target
.iter()
.map(|value| value.replace(path.as_str(), "").replacen("/", "", 1))
.collect()
}
#[test]
#[traced_test]
fn package_generator_can_create_package_for_standard() {
let template_directories = Box::new(Directorate::<TemplateDefinitions>::default());
let packager = PackageGenerator::new(template_directories);
let current_dir = std::env::current_dir().expect("should have gotten directory");
let output_directory = current_dir.join("output_directory");
let project_directory = output_directory.join("standard_project");
let project_cargo_file = project_directory.join("Cargo.toml");
let mut params: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
params
.entry(String::from("PROJECT_DIRECTORY"))
.or_insert(serde_json::Value::from(
project_directory
.into_string()
.expect("should convert into string"),
));
let rust_config = RustConfig::new(Some(project_cargo_file), false);
let package_config = PackageConfig::new(
project_directory.clone(),
params,
"CustomRustProject",
"retro_project",
);
let rust_configurator = RustProjectConfigurator::new(package_config, Some(rust_config))
.expect("should generate rust configurator");
let result = packager.create(rust_configurator);
ewe_trace::debug!("Result: {:?}", result);
assert!(matches!(result, Ok(())));
assert_eq!(
shorten_path(
list_dir(&project_directory),
project_directory.into_string().unwrap()
),
vec![
"Cargo.toml",
".gitignore",
"retro_project/src/lib.rs",
"retro_project/src/page.rs",
"retro_project/Cargo.toml",
]
.to_vec_string()
);
}
#[test]
#[traced_test]
fn package_generator_can_create_package_for_workspace() {
let template_directories = Box::new(Directorate::<TemplateDefinitions>::default());
let packager = PackageGenerator::new(template_directories);
let current_dir = std::env::current_dir().expect("should have gotten directory");
let output_directory = current_dir.join("output_directory");
let project_directory = output_directory.join("workspace_project");
let project_cargo_file = project_directory.join("Cargo.toml");
let mut params: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
params
.entry(String::from("PROJECT_DIRECTORY"))
.or_insert(serde_json::Value::from(
project_directory
.into_string()
.expect("should convert into string"),
));
let rust_config = RustConfig::new(Some(project_cargo_file), false);
let package_config = PackageConfig::new(
project_directory.clone(),
params,
"CustomRustProject",
"retro_project",
);
let rust_configurator = RustProjectConfigurator::new(package_config, Some(rust_config))
.expect("should generate rust configurator");
let result = packager.create(rust_configurator);
ewe_trace::debug!("Result: {:?}", result);
assert!(matches!(result, Ok(())));
assert_eq!(
shorten_path(
list_dir(&project_directory),
project_directory.into_string().unwrap()
),
vec![
"Cargo.toml",
".gitignore",
"retro_project/src/lib.rs",
"retro_project/src/page.rs",
"retro_project/Cargo.toml",
]
.to_vec_string()
);
}
#[test]
#[traced_test]
fn package_generator_can_create_non_rust_package_from_template() {
let template_directories = Box::new(Directorate::<TemplateDefinitions>::default());
let packager = PackageGenerator::new(template_directories);
let current_dir = std::env::current_dir().expect("should have gotten directory");
let output_directory = current_dir.join("output_directory");
let project_directory = output_directory.join("static_pages");
let mut params: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
params
.entry(String::from("PROJECT_DIRECTORY"))
.or_insert(serde_json::Value::from(
project_directory
.into_string()
.expect("should convert into string"),
));
let package_config = PackageConfig::new(
project_directory.clone(),
params,
"SimpleHTMLPage",
"retro_project",
);
assert!(matches!(packager.create(package_config), Ok(())));
assert_eq!(
shorten_path(
list_dir(&project_directory),
project_directory.clone().into_string().unwrap()
),
vec!["retro_project/index.html"].to_vec_string()
);
}
}