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}