Skip to main content

pmcp_code_mode/
templates.rs

1//! Code Mode instruction and policy templates.
2//!
3//! This module provides a simple template engine for generating Code Mode
4//! `code-mode://instructions` and `code-mode://policies` resources from
5//! compiled-in templates. Templates use `{{key}}` substitution markers.
6//!
7//! ## Architecture
8//!
9//! Each server type crate embeds its own `.md` template files via `include_str!()`.
10//! The shared template engine here provides:
11//!
12//! - **`render()`** — Simple `{{key}}` → value substitution
13//! - **`TemplateContext`** — Builder for template variables
14//! - **URI constants** — Standard resource URIs for auto-generated resources
15//!
16//! ## Override Mechanism
17//!
18//! If an admin defines a resource with the same URI in the TOML config,
19//! the admin's version takes precedence. The server startup code should
20//! check for existing resources before registering auto-generated ones.
21
22use std::collections::HashMap;
23
24/// Standard URI for auto-generated instructions resource.
25pub const INSTRUCTIONS_URI: &str = "code-mode://instructions";
26
27/// Standard URI for auto-generated policies resource.
28pub const POLICIES_URI: &str = "code-mode://policies";
29
30/// Template context holding key-value pairs for substitution.
31#[derive(Debug, Clone, Default)]
32pub struct TemplateContext {
33    vars: HashMap<String, String>,
34}
35
36impl TemplateContext {
37    /// Create a new empty context.
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    /// Set a variable.
43    pub fn set(mut self, key: &str, value: impl Into<String>) -> Self {
44        self.vars.insert(key.to_string(), value.into());
45        self
46    }
47
48    /// Set a boolean variable (renders as "true"/"false").
49    pub fn set_bool(self, key: &str, value: bool) -> Self {
50        self.set(key, if value { "true" } else { "false" })
51    }
52
53    /// Set a numeric variable.
54    pub fn set_num(self, key: &str, value: impl std::fmt::Display) -> Self {
55        self.set(key, value.to_string())
56    }
57
58    /// Render a template string, replacing `{{key}}` with values.
59    ///
60    /// Unknown keys are left as-is (not replaced). This allows templates
61    /// to contain literal `{{` in code examples by using keys that don't
62    /// match any variable.
63    pub fn render(&self, template: &str) -> String {
64        let mut result = template.to_string();
65        for (key, value) in &self.vars {
66            let placeholder = format!("{{{{{}}}}}", key);
67            result = result.replace(&placeholder, value);
68        }
69        result
70    }
71}
72
73/// Render a template with the given context.
74///
75/// Convenience function equivalent to `ctx.render(template)`.
76pub fn render(template: &str, ctx: &TemplateContext) -> String {
77    ctx.render(template)
78}
79
80/// Conditionally include a section based on a boolean.
81///
82/// Returns the section content if `condition` is true, otherwise empty string.
83/// Useful for building template content with optional sections.
84pub fn conditional(condition: bool, content: &str) -> String {
85    if condition {
86        content.to_string()
87    } else {
88        String::new()
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_basic_render() {
98        let ctx = TemplateContext::new()
99            .set("name", "IMDB")
100            .set_num("timeout", 300);
101
102        let result = ctx.render("Server: {{name}}, Timeout: {{timeout}}s");
103        assert_eq!(result, "Server: IMDB, Timeout: 300s");
104    }
105
106    #[test]
107    fn test_unknown_keys_preserved() {
108        let ctx = TemplateContext::new().set("name", "test");
109        let result = ctx.render("{{name}} and {{unknown}}");
110        assert_eq!(result, "test and {{unknown}}");
111    }
112
113    #[test]
114    fn test_conditional() {
115        assert_eq!(conditional(true, "hello"), "hello");
116        assert_eq!(conditional(false, "hello"), "");
117    }
118
119    #[test]
120    fn test_code_examples_not_broken() {
121        // Ensure that code examples with JS template literals aren't affected
122        let ctx = TemplateContext::new().set("server_name", "Test");
123        let template = r#"{{server_name}} API
124
125```javascript
126const path = `/users/${userId}`;
127```"#;
128        let result = ctx.render(template);
129        assert!(result.contains("Test API"));
130        assert!(result.contains("${userId}"));
131    }
132}