use serde::Serialize;
use crate::error::ProjectError;
use crate::project::Project;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuildTarget {
JsonManifest,
RustModule,
Both,
}
#[derive(Debug, Clone, Default)]
pub struct BuildOutput {
pub json: Option<String>,
pub rust: Option<String>,
}
#[derive(Serialize)]
struct JsonBundle<'a> {
schema_version: u32,
name: &'a str,
version: &'a str,
language: &'a str,
engine: &'a crate::manifest::EngineSettings,
templates: Vec<&'a crate::template::TemplateFile>,
partials: Vec<&'a crate::partial::PartialFile>,
}
pub fn build_bundle(project: &Project, target: BuildTarget) -> Result<BuildOutput, ProjectError> {
let mut out = BuildOutput::default();
if matches!(target, BuildTarget::JsonManifest | BuildTarget::Both) {
out.json = Some(build_json(project)?);
}
if matches!(target, BuildTarget::RustModule | BuildTarget::Both) {
out.rust = Some(build_rust(project)?);
}
Ok(out)
}
fn build_json(project: &Project) -> Result<String, ProjectError> {
let bundle = JsonBundle {
schema_version: 1,
name: &project.manifest.name,
version: &project.manifest.version,
language: &project.manifest.language,
engine: &project.manifest.engine,
templates: project.templates.values().collect(),
partials: project.partials.values().collect(),
};
serde_json::to_string_pretty(&bundle).map_err(|e| ProjectError::JsonParse {
file: "(bundle)".to_string(),
cause: e.to_string(),
})
}
fn build_rust(project: &Project) -> Result<String, ProjectError> {
use std::fmt::Write;
let mut s = String::new();
writeln!(s, "// Generated by `prosaic build` — do not edit by hand.").unwrap();
writeln!(s, "// schema_version = 1").unwrap();
writeln!(s, "use prosaic_core::{{Engine, Salience}};").unwrap();
writeln!(s).unwrap();
writeln!(
s,
"pub fn register(engine: &mut Engine) -> Result<(), prosaic_core::ProsaicError> {{"
)
.unwrap();
for partial in project.partials.values() {
writeln!(
s,
" engine.register_partial({:?}, {:?})?;",
partial.name, partial.body
)
.unwrap();
}
for template in project.templates.values() {
for variant in &template.variants {
let salience = match variant.salience.as_str() {
"low" => "Salience::Low",
"high" => "Salience::High",
_ => "Salience::Medium",
};
let lang = variant.language.as_deref().unwrap_or("");
let style = variant.style.as_deref().unwrap_or("");
if lang.is_empty() && style.is_empty() {
writeln!(
s,
" engine.register_template_at({:?}, {:?}, {})?;",
template.key, variant.body, salience
)
.unwrap();
} else {
writeln!(
s,
" engine.register_template_with_language_and_style_at({:?}, {:?}, {}, {}, {})?;",
template.key,
variant.body,
salience,
option_str_literal(lang),
option_str_literal(style)
)
.unwrap();
}
}
}
writeln!(s, " Ok(())").unwrap();
writeln!(s, "}}").unwrap();
Ok(s)
}
fn option_str_literal(value: &str) -> String {
if value.is_empty() {
"None".to_string()
} else {
format!("Some({value:?})")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::project::Project;
use std::path::Path;
fn project() -> Project {
Project::load_from_dir(
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/multi-variant"),
)
.unwrap()
}
#[test]
fn json_bundle_contains_template() {
let bundle = build_bundle(&project(), BuildTarget::JsonManifest).unwrap();
let json = bundle.json.unwrap();
assert!(json.contains("\"schema_version\""));
assert!(json.contains("\"code.modified\""));
assert!(json.contains("\"impact_tail\""));
}
#[test]
fn rust_bundle_emits_register_calls() {
let bundle = build_bundle(&project(), BuildTarget::RustModule).unwrap();
let rust = bundle.rust.unwrap();
assert!(rust.contains("register_partial(\"impact_tail\""));
assert!(rust.contains("register_template_at(\"code.modified\""));
assert!(rust.contains("Salience::Low"));
assert!(rust.contains("Salience::Medium"));
assert!(rust.contains("Salience::High"));
}
#[test]
fn rust_bundle_emits_style_tagged_register_call() {
let mut p = project();
let t = p.templates.get_mut("code.modified").unwrap();
t.variants[0].style = Some("executive".to_string());
let bundle = build_bundle(&p, BuildTarget::RustModule).unwrap();
let rust = bundle.rust.unwrap();
assert!(rust.contains("register_template_with_language_and_style_at(\"code.modified\""));
assert!(rust.contains("Some(\"executive\")"));
}
#[test]
fn both_target_produces_both() {
let bundle = build_bundle(&project(), BuildTarget::Both).unwrap();
assert!(bundle.json.is_some());
assert!(bundle.rust.is_some());
}
}