systemprompt-generator 0.2.1

Static site generation, theme rendering, and asset bundling for systemprompt.io AI governance dashboards. Handlebars and Markdown pipeline for the MCP governance platform.
Documentation
use super::xml::{RssChannel, RssItem, build_rss_xml};
use anyhow::{Context, Result, anyhow};
use std::path::Path;
use std::sync::Arc;
use systemprompt_database::DbPool;
use systemprompt_models::{AppPaths, Config};
use systemprompt_provider_contracts::{RssFeedContext, RssFeedProvider};
use tokio::fs;

use super::default_provider::DefaultRssFeedProvider;

#[derive(Debug, Clone)]
pub struct GeneratedFeed {
    pub filename: String,
    pub xml: String,
    pub item_count: usize,
}

pub async fn generate_feed(db_pool: DbPool) -> Result<()> {
    let provider = DefaultRssFeedProvider::new(Arc::clone(&db_pool)).await?;
    let providers: Vec<Arc<dyn RssFeedProvider>> = vec![Arc::new(provider)];
    let feeds = generate_feed_with_providers(&providers, db_pool).await?;

    let web_dir = AppPaths::get()
        .map_err(|e| anyhow!("{}", e))?
        .web()
        .dist()
        .to_path_buf();

    for feed in feeds {
        let feed_path = web_dir.join(&feed.filename);
        ensure_parent_exists(&feed_path).await?;
        fs::write(&feed_path, &feed.xml).await?;
        tracing::info!(
            path = %feed_path.display(),
            items = feed.item_count,
            "Generated RSS feed"
        );
    }

    Ok(())
}

pub async fn generate_feed_with_providers(
    providers: &[Arc<dyn RssFeedProvider>],
    _db_pool: DbPool,
) -> Result<Vec<GeneratedFeed>> {
    let global_config = Config::get()?;
    let base_url = &global_config.api_external_url;

    let mut feeds = Vec::new();

    for provider in providers {
        for spec in provider.feed_specs() {
            let ctx = RssFeedContext {
                base_url,
                source_name: spec.source_id.as_str(),
            };

            let metadata = provider
                .feed_metadata(&ctx)
                .await
                .context("Failed to fetch feed metadata")?;

            let items = provider
                .fetch_items(&ctx, spec.max_items)
                .await
                .context("Failed to fetch feed items")?;

            let rss_items: Vec<RssItem> = items
                .into_iter()
                .map(|item| RssItem {
                    title: item.title,
                    link: item.link,
                    description: item.description,
                    pub_date: item.pub_date,
                    guid: item.guid,
                    author: item.author,
                })
                .collect();

            let channel = RssChannel {
                title: metadata.title,
                link: metadata.link,
                description: metadata.description,
                items: rss_items.clone(),
            };

            let xml = build_rss_xml(&channel);

            feeds.push(GeneratedFeed {
                filename: spec.output_filename,
                xml,
                item_count: rss_items.len(),
            });
        }
    }

    if feeds.is_empty() {
        return Err(anyhow!(
            "No RSS feeds generated. Ensure at least one RssFeedProvider is registered."
        ));
    }

    Ok(feeds)
}

async fn ensure_parent_exists(path: &Path) -> Result<()> {
    if let Some(parent) = path.parent() {
        if !parent.exists() {
            fs::create_dir_all(parent).await?;
        }
    }
    Ok(())
}