systemprompt-generator 0.8.0

Static site generation, theme rendering, and asset bundling for systemprompt.io AI governance dashboards. Handlebars and Markdown pipeline for the MCP governance platform.
Documentation
//! Helpers shared by the prerender pipeline: deep-merging JSON objects from
//! template-data providers, and rendering registered template components.

use std::collections::HashSet;

use systemprompt_template_provider::{ComponentContext, RenderedComponent};
use systemprompt_templates::TemplateRegistry;

pub fn merge_json_data(base: &mut serde_json::Value, extension: &serde_json::Value) {
    match (base, extension) {
        (serde_json::Value::Object(base_obj), serde_json::Value::Object(ext_obj)) => {
            for (key, ext_value) in ext_obj {
                match base_obj.get_mut(key) {
                    Some(base_value) => merge_json_data(base_value, ext_value),
                    None => {
                        base_obj.insert(key.clone(), ext_value.clone());
                    },
                }
            }
        },
        (base, extension) => {
            *base = extension.clone();
        },
    }
}

pub async fn render_components(
    template_registry: &TemplateRegistry,
    target_type: &str,
    component_ctx: &ComponentContext<'_>,
    data: &mut serde_json::Value,
) {
    let mut rendered_variables: HashSet<String> = HashSet::new();

    for component in template_registry.components_for(target_type) {
        let variable_name = component.variable_name();

        if rendered_variables.contains(variable_name) {
            tracing::debug!(
                component_id = %component.component_id(),
                variable_name = %variable_name,
                priority = component.priority(),
                "Skipping component, variable already rendered by higher-priority component"
            );
            continue;
        }

        let result: Result<RenderedComponent, String> =
            if let Some(partial) = component.partial_template() {
                template_registry
                    .render_partial(&partial.name, data)
                    .map(|html| RenderedComponent::new(component.variable_name(), html))
                    .map_err(|e| e.to_string())
            } else {
                component
                    .render(component_ctx)
                    .await
                    .map_err(|e| e.to_string())
            };

        match result {
            Ok(rendered) => {
                if let Some(obj) = data.as_object_mut() {
                    rendered_variables.insert(rendered.variable_name.clone());
                    obj.insert(
                        rendered.variable_name,
                        serde_json::Value::String(rendered.html),
                    );
                }
            },
            Err(e) => {
                tracing::warn!(
                    component_id = %component.component_id(),
                    target_type = %target_type,
                    error = %e,
                    "Component render failed"
                );
            },
        }
    }
}