use minijinja::{Environment, Value, syntax::SyntaxConfig};
use serde::Serialize;
use super::Edits;
use crate::{
CliResult,
output::{CliError, Report},
project::Project,
};
#[derive(Serialize)]
pub struct TemplateInfo {
pub path: String,
pub overridden: bool,
}
pub(crate) const OVERRIDES: &str = ".ocre/templates";
pub(crate) const BUILTIN: [(&str, &str); 13] = [
("controller/api.rs", include_str!("../../templates/generate/controller/api.rs")),
("controller/html.rs", include_str!("../../templates/generate/controller/html.rs")),
("controller/view.html", include_str!("../../templates/generate/controller/view.html")),
("resource/api.rs", include_str!("../../templates/generate/resource/api.rs")),
("resource/html.rs", include_str!("../../templates/generate/resource/html.rs")),
("resource/index.html", include_str!("../../templates/generate/resource/index.html")),
("resource/show.html", include_str!("../../templates/generate/resource/show.html")),
("scaffold/_form.html", include_str!("../../templates/generate/scaffold/_form.html")),
("scaffold/_row.html", include_str!("../../templates/generate/scaffold/_row.html")),
("scaffold/edit.html", include_str!("../../templates/generate/scaffold/edit.html")),
("scaffold/index.html", include_str!("../../templates/generate/scaffold/index.html")),
("scaffold/new.html", include_str!("../../templates/generate/scaffold/new.html")),
("scaffold/show.html", include_str!("../../templates/generate/scaffold/show.html")),
];
pub(crate) fn render(edits: &Edits, path: &str, context: &Value) -> Result<String, CliError> {
let override_path = format!("{OVERRIDES}/{path}");
match edits.read(&override_path)? {
Some(source) => render_source(&override_path, &source, context),
None => {
let (_, source) = BUILTIN.iter().find(|(p, _)| *p == path).expect("generators use built-in paths");
render_source(&format!("built-in template {path}"), source, context)
}
}
}
pub(crate) fn render_source(origin: &str, source: &str, context: &Value) -> Result<String, CliError> {
let failed = |err: minijinja::Error| {
let mut message = format!("{origin} failed to render: {err}");
if let Some(detail) = err.detail() {
message = format!("{message} ({detail})");
}
let hint = if origin.starts_with(OVERRIDES) {
format!("fix {origin}, or delete it to use the built-in template again")
} else {
format!("fix {origin}")
};
CliError::new(message).hint(hint)
};
let mut env = Environment::new();
env.set_syntax(
SyntaxConfig::builder()
.block_delimiters("<%", "%>")
.variable_delimiters("<%=", "%>")
.comment_delimiters("<%#", "%>")
.build()
.expect("valid delimiters"),
);
env.set_trim_blocks(true);
env.set_lstrip_blocks(true);
env.set_keep_trailing_newline(true);
env.set_undefined_behavior(minijinja::UndefinedBehavior::Strict);
env.render_str(source, context).map_err(failed)
}
pub fn override_templates(project: &Project, paths: &[String]) -> CliResult {
let mut edits = Edits::new(project);
if paths.is_empty() {
let templates = BUILTIN
.iter()
.map(|(path, _)| TemplateInfo {
path: (*path).to_owned(),
overridden: edits.exists(&format!("{OVERRIDES}/{path}")),
})
.collect();
let mut report = Report::new("generate override");
report.templates = Some(templates);
report.next =
vec!["ocre g override <path> (e.g. controller/html.rs, or controller for all its files)".to_owned()];
return Ok(report);
}
for wanted in paths {
let wanted = wanted.trim_end_matches('/');
let matching: Vec<&(&str, &str)> = BUILTIN
.iter()
.filter(|(path, _)| *path == wanted || path.strip_prefix(wanted).is_some_and(|rest| rest.starts_with('/')))
.collect();
if matching.is_empty() {
let all: Vec<&str> = BUILTIN.iter().map(|(path, _)| *path).collect();
return Err(CliError::new(format!("no generator template `{wanted}`"))
.hint(format!("templates: {}; or a generator, e.g. `controller`", all.join(", "))));
}
for (path, source) in matching {
edits.create(&format!("{OVERRIDES}/{path}"), (*source).to_owned())?;
}
}
let mut report = edits.apply("generate override")?;
report.next = vec![format!(
"edit the files in {OVERRIDES}/ (<%= value %>, <% for x in xs %>...<% endfor %>); generators use them until deleted"
)];
Ok(report)
}