Skip to main content

coda_pm/
manager.rs

1//! Prompt manager implementation.
2//!
3//! Manages a collection of minijinja templates that can be loaded from
4//! directories, registered manually, and rendered with context data.
5
6use std::collections::HashMap;
7use std::path::Path;
8
9use minijinja::Environment;
10use serde::Serialize;
11
12use crate::loader::load_templates_from_dir;
13use crate::{PromptError, PromptTemplate};
14
15/// Manages prompt templates using minijinja for rendering.
16///
17/// Templates can be added individually via [`add_template`](Self::add_template)
18/// or loaded in bulk from a directory via [`load_from_dir`](Self::load_from_dir).
19#[derive(Clone)]
20pub struct PromptManager {
21    env: Environment<'static>,
22    templates: HashMap<String, PromptTemplate>,
23}
24
25impl PromptManager {
26    /// Creates a new empty prompt manager with no templates loaded.
27    ///
28    /// Use [`add_template`](Self::add_template) or [`load_from_dir`](Self::load_from_dir)
29    /// to populate with templates.
30    pub fn new() -> Self {
31        Self {
32            env: Environment::new(),
33            templates: HashMap::new(),
34        }
35    }
36
37    /// Creates a new prompt manager pre-loaded with all built-in templates.
38    ///
39    /// Built-in templates are embedded at compile time via [`include_str!`] and
40    /// are always available, even when installed via `cargo install`.
41    ///
42    /// # Errors
43    ///
44    /// Returns `PromptError::InvalidTemplate` if any built-in template has
45    /// invalid minijinja syntax.
46    ///
47    /// # Examples
48    ///
49    /// ```
50    /// use coda_pm::PromptManager;
51    ///
52    /// let pm = PromptManager::with_builtin_templates().unwrap();
53    /// assert!(pm.get_template("init/system").is_some());
54    /// ```
55    pub fn with_builtin_templates() -> Result<Self, PromptError> {
56        let mut pm = Self::new();
57        for template in crate::builtin::builtin_templates() {
58            pm.add_template(template)?;
59        }
60        Ok(pm)
61    }
62
63    /// Registers a single template with the manager.
64    ///
65    /// # Errors
66    ///
67    /// Returns `PromptError::InvalidTemplate` if the template content has
68    /// invalid minijinja syntax.
69    pub fn add_template(&mut self, template: PromptTemplate) -> Result<(), PromptError> {
70        self.env
71            .add_template_owned(template.name.clone(), template.content.clone())
72            .map_err(|e: minijinja::Error| PromptError::InvalidTemplate(e.to_string()))?;
73        self.templates.insert(template.name.clone(), template);
74        Ok(())
75    }
76
77    /// Loads all `.j2` templates from a directory recursively.
78    ///
79    /// Template names are derived from relative paths (e.g., `init/system`
80    /// from `init/system.j2`). Existing templates with the same name are
81    /// overwritten.
82    ///
83    /// # Errors
84    ///
85    /// Returns `PromptError::IoError` if the directory cannot be read, or
86    /// `PromptError::InvalidTemplate` if a template has invalid syntax.
87    pub fn load_from_dir(&mut self, dir: &Path) -> Result<(), PromptError> {
88        let templates = load_templates_from_dir(dir)?;
89        for template in templates {
90            self.add_template(template)?;
91        }
92        Ok(())
93    }
94
95    /// Renders a named template with the given context data.
96    ///
97    /// # Errors
98    ///
99    /// Returns `PromptError::TemplateNotFound` if no template with the given
100    /// name exists, or `PromptError::RenderError` if rendering fails.
101    pub fn render<T: Serialize>(&self, name: &str, ctx: T) -> Result<String, PromptError> {
102        let tmpl = self
103            .env
104            .get_template(name)
105            .map_err(|_| PromptError::TemplateNotFound(name.to_string()))?;
106
107        tmpl.render(ctx)
108            .map_err(|e| PromptError::RenderError(e.to_string()))
109    }
110
111    /// Returns a reference to the template with the given name, if it exists.
112    pub fn get_template(&self, name: &str) -> Option<&PromptTemplate> {
113        self.templates.get(name)
114    }
115
116    /// Returns the number of templates currently loaded.
117    ///
118    /// # Examples
119    ///
120    /// ```
121    /// use coda_pm::{PromptManager, PromptTemplate};
122    ///
123    /// let mut pm = PromptManager::new();
124    /// assert_eq!(pm.template_count(), 0);
125    /// pm.add_template(PromptTemplate::new("test", "Hello")).unwrap();
126    /// assert_eq!(pm.template_count(), 1);
127    /// ```
128    pub fn template_count(&self) -> usize {
129        self.templates.len()
130    }
131}
132
133impl Default for PromptManager {
134    fn default() -> Self {
135        Self::new()
136    }
137}
138
139impl std::fmt::Debug for PromptManager {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        f.debug_struct("PromptManager")
142            .field("template_count", &self.templates.len())
143            .field("template_names", &self.templates.keys().collect::<Vec<_>>())
144            .finish()
145    }
146}