Skip to main content

ralph/template/loader/
load.rs

1//! Purpose: Load built-in and custom templates, then optionally apply template
2//! context substitution.
3//!
4//! Responsibilities:
5//! - Resolve template lookup precedence between custom and built-in templates.
6//! - Parse template JSON into tasks.
7//! - Validate template variables and apply context-aware substitution.
8//!
9//! Scope:
10//! - Loading and substitution only; listing/query helpers live elsewhere.
11//!
12//! Usage:
13//! - Called by CLI task-creation surfaces that need template resolution.
14//!
15//! Invariants/Assumptions:
16//! - Custom templates under `.ralph/templates/{name}.json` override built-ins.
17//! - Strict mode fails on unknown template variables.
18//! - Non-strict mode preserves unknown placeholders and returns warnings.
19
20use std::path::Path;
21
22use anyhow::{Result, bail};
23
24use crate::contracts::Task;
25use crate::template::builtin::get_builtin_template;
26use crate::template::variables::{
27    TemplateContext, detect_context_with_warnings, substitute_variables_in_task,
28    validate_task_template,
29};
30
31use super::types::{LoadedTemplate, TemplateError, TemplateSource};
32
33/// Load a template by name.
34///
35/// Checks `.ralph/templates/{name}.json` first, then falls back to built-in templates.
36pub fn load_template(name: &str, project_root: &Path) -> Result<(Task, TemplateSource)> {
37    let custom_path = project_root
38        .join(".ralph/templates")
39        .join(format!("{}.json", name));
40    if custom_path.exists() {
41        let content = std::fs::read_to_string(&custom_path)
42            .map_err(|e| TemplateError::ReadError(e.to_string()))?;
43        let task: Task = serde_json::from_str(&content)
44            .map_err(|e| TemplateError::InvalidJson(e.to_string()))?;
45
46        let validation = validate_task_template(&task);
47        if validation.has_unknown_variables() {
48            let unknowns = validation.unknown_variable_names();
49            log::warn!(
50                "Template '{}' contains unknown variables: {}",
51                name,
52                unknowns.join(", ")
53            );
54        }
55
56        return Ok((task, TemplateSource::Custom(custom_path)));
57    }
58
59    if let Some(template_json) = get_builtin_template(name) {
60        let task: Task = serde_json::from_str(template_json)
61            .map_err(|e| TemplateError::InvalidJson(e.to_string()))?;
62        return Ok((task, TemplateSource::Builtin(name.to_string())));
63    }
64
65    Err(TemplateError::NotFound(name.to_string()).into())
66}
67
68/// Load a template by name with variable substitution.
69///
70/// Checks `.ralph/templates/{name}.json` first, then falls back to built-in templates.
71/// Substitutes template variables (`{{target}}`, `{{module}}`, `{{file}}`, `{{branch}}`) with
72/// context-aware values.
73///
74/// If `strict` is true and unknown variables are present, returns an error.
75pub fn load_template_with_context(
76    name: &str,
77    project_root: &Path,
78    target: Option<&str>,
79    strict: bool,
80) -> Result<LoadedTemplate> {
81    let (mut task, source) = load_template(name, project_root)?;
82    let validation = validate_task_template(&task);
83
84    if strict && validation.has_unknown_variables() {
85        let unknowns = validation.unknown_variable_names();
86        bail!(TemplateError::ValidationError(format!(
87            "Template '{}' contains unknown variables: {}",
88            name,
89            unknowns.join(", ")
90        )));
91    }
92
93    let (context, mut warnings) =
94        detect_context_with_warnings(target, project_root, validation.uses_branch);
95    warnings.extend(validation.warnings);
96    substitute_variables_in_task(&mut task, &context);
97
98    Ok(LoadedTemplate {
99        task,
100        source,
101        warnings,
102    })
103}
104
105/// Load a template by name with variable substitution (legacy, non-strict).
106///
107/// This is a convenience function for backward compatibility.
108/// Use `load_template_with_context` for full control.
109pub fn load_template_with_context_legacy(
110    name: &str,
111    project_root: &Path,
112    target: Option<&str>,
113) -> Result<(Task, TemplateSource)> {
114    let loaded = load_template_with_context(name, project_root, target, false)?;
115    Ok((loaded.task, loaded.source))
116}
117
118/// Get the template context for inspection.
119pub fn get_template_context(target: Option<&str>, project_root: &Path) -> TemplateContext {
120    let (context, _) = detect_context_with_warnings(target, project_root, true);
121    context
122}