systemprompt-templates 0.10.2

Template registry, loading, and rendering for systemprompt.io config-as-code AI governance deployments. Handlebars-powered template engine for the MCP governance pipeline.
Documentation
mod lifecycle;
mod queries;
mod stats;

use std::collections::HashMap;

use handlebars::{
    Context, Handlebars, Helper, HelperResult, Output, RenderContext, RenderError,
    RenderErrorReason,
};
use systemprompt_template_provider::{
    DynComponentRenderer, DynPageDataProvider, DynPagePrerenderer, DynTemplateDataExtender,
    DynTemplateLoader, DynTemplateProvider, TemplateDefinition,
};
use tracing::debug;

pub use stats::RegistryStats;

pub struct TemplateRegistry {
    pub(super) providers: Vec<DynTemplateProvider>,
    pub(super) loaders: Vec<DynTemplateLoader>,
    pub(super) extenders: Vec<DynTemplateDataExtender>,
    pub(super) components: Vec<DynComponentRenderer>,
    pub(super) page_providers: Vec<DynPageDataProvider>,
    pub(super) page_prerenderers: Vec<DynPagePrerenderer>,
    pub(super) resolved_templates: HashMap<String, TemplateDefinition>,
    pub(super) handlebars: Handlebars<'static>,
    pub(super) template_sources: HashMap<String, String>,
}

impl Default for TemplateRegistry {
    fn default() -> Self {
        Self::new()
    }
}

impl TemplateRegistry {
    #[must_use]
    pub fn new() -> Self {
        let mut handlebars = Handlebars::new();
        handlebars.register_helper("json", Box::new(json_helper));
        Self {
            providers: Vec::new(),
            loaders: Vec::new(),
            extenders: Vec::new(),
            components: Vec::new(),
            page_providers: Vec::new(),
            page_prerenderers: Vec::new(),
            resolved_templates: HashMap::new(),
            handlebars,
            template_sources: HashMap::new(),
        }
    }

    pub fn register_provider(&mut self, provider: DynTemplateProvider) {
        debug!(
            provider_id = %provider.provider_id(),
            priority = provider.priority(),
            "Registering template provider"
        );
        self.providers.push(provider);
        self.providers.sort_by_key(|p| p.priority());
    }

    pub fn register_loader(&mut self, loader: DynTemplateLoader) {
        self.loaders.push(loader);
    }

    pub fn register_extender(&mut self, extender: DynTemplateDataExtender) {
        debug!(
            extender_id = %extender.extender_id(),
            priority = extender.priority(),
            "Registering template data extender"
        );
        self.extenders.push(extender);
        self.extenders.sort_by_key(|e| e.priority());
    }

    pub fn register_component(&mut self, component: DynComponentRenderer) {
        debug!(
            component_id = %component.component_id(),
            variable_name = %component.variable_name(),
            priority = component.priority(),
            "Registering component renderer"
        );
        self.components.push(component);
        self.components.sort_by_key(|c| c.priority());
    }

    pub fn register_page_provider(&mut self, provider: DynPageDataProvider) {
        debug!(
            provider_id = %provider.provider_id(),
            pages = ?provider.applies_to_pages(),
            "Registering page data provider"
        );
        self.page_providers.push(provider);
        self.page_providers.sort_by_key(|p| p.priority());
    }

    pub fn register_page_prerenderer(&mut self, prerenderer: DynPagePrerenderer) {
        debug!(
            page_type = %prerenderer.page_type(),
            priority = prerenderer.priority(),
            "Registering page prerenderer"
        );
        self.page_prerenderers.push(prerenderer);
        self.page_prerenderers.sort_by_key(|p| p.priority());
    }
}

fn json_helper(
    h: &Helper<'_>,
    _: &Handlebars<'_>,
    _: &Context,
    _: &mut RenderContext<'_, '_>,
    out: &mut dyn Output,
) -> HelperResult {
    let param = h
        .param(0)
        .ok_or_else(|| RenderError::from(RenderErrorReason::ParamNotFoundForIndex("json", 0)))?;
    let serialized = serde_json::to_string(param.value()).map_err(|e| {
        RenderError::from(RenderErrorReason::NestedError(Box::new(
            std::io::Error::other(e.to_string()),
        )))
    })?;
    out.write(&serialized)?;
    Ok(())
}

impl std::fmt::Debug for TemplateRegistry {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("TemplateRegistry")
            .field("providers", &self.providers.len())
            .field(
                "templates",
                &self.resolved_templates.keys().collect::<Vec<_>>(),
            )
            .field("loaders", &self.loaders.len())
            .field("extenders", &self.extenders.len())
            .field("components", &self.components.len())
            .field("page_providers", &self.page_providers.len())
            .field("page_prerenderers", &self.page_prerenderers.len())
            .finish_non_exhaustive()
    }
}