systemprompt-generator 0.2.2

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 std::path::PathBuf;

#[derive(Debug, thiserror::Error)]
pub enum PublishError {
    #[error("Missing field '{field}' for content '{slug}'")]
    MissingField {
        field: String,
        slug: String,
        source_path: Option<PathBuf>,
        suggestion: Option<String>,
    },

    #[error("No template for content type '{content_type}'")]
    TemplateNotFound {
        content_type: String,
        slug: String,
        available_templates: Vec<String>,
    },

    #[error("Page data provider '{provider_id}' failed: {cause}")]
    ProviderFailed {
        provider_id: String,
        cause: String,
        suggestion: Option<String>,
    },

    #[error("Template render failed for '{template_name}'")]
    RenderFailed {
        template_name: String,
        slug: Option<String>,
        cause: String,
    },

    #[error("Content fetch failed for source '{source_name}'")]
    FetchFailed { source_name: String, cause: String },

    #[error("Configuration error: {message}")]
    Config {
        message: String,
        path: Option<String>,
    },

    #[error("Page prerenderer '{page_type}' failed: {cause}")]
    PagePrerendererFailed { page_type: String, cause: String },
}

impl PublishError {
    pub fn missing_field(field: impl Into<String>, slug: impl Into<String>) -> Self {
        let field_str = field.into();
        Self::MissingField {
            suggestion: suggest_fix_for_field(&field_str),
            field: field_str,
            slug: slug.into(),
            source_path: None,
        }
    }

    pub fn missing_field_with_path(
        field: impl Into<String>,
        slug: impl Into<String>,
        path: PathBuf,
    ) -> Self {
        let field_str = field.into();
        Self::MissingField {
            suggestion: suggest_fix_for_field(&field_str),
            field: field_str,
            slug: slug.into(),
            source_path: Some(path),
        }
    }

    pub fn template_not_found(
        content_type: impl Into<String>,
        slug: impl Into<String>,
        available: Vec<String>,
    ) -> Self {
        Self::TemplateNotFound {
            content_type: content_type.into(),
            slug: slug.into(),
            available_templates: available,
        }
    }

    pub fn provider_failed(provider_id: impl Into<String>, cause: impl Into<String>) -> Self {
        Self::ProviderFailed {
            provider_id: provider_id.into(),
            cause: cause.into(),
            suggestion: None,
        }
    }

    pub fn render_failed(
        template_name: impl Into<String>,
        slug: Option<String>,
        cause: impl Into<String>,
    ) -> Self {
        Self::RenderFailed {
            template_name: template_name.into(),
            slug,
            cause: cause.into(),
        }
    }

    pub fn fetch_failed(source_name: impl Into<String>, cause: impl Into<String>) -> Self {
        Self::FetchFailed {
            source_name: source_name.into(),
            cause: cause.into(),
        }
    }

    pub fn config(message: impl Into<String>) -> Self {
        Self::Config {
            message: message.into(),
            path: None,
        }
    }

    pub fn page_prerenderer_failed(page_type: impl Into<String>, cause: impl Into<String>) -> Self {
        Self::PagePrerendererFailed {
            page_type: page_type.into(),
            cause: cause.into(),
        }
    }

    pub fn location(&self) -> Option<String> {
        match self {
            Self::MissingField { source_path, .. } => {
                source_path.as_ref().map(|p| p.display().to_string())
            },
            Self::Config { path, .. } => path.clone(),
            _ => None,
        }
    }

    pub fn suggestion_string(&self) -> Option<String> {
        match self {
            Self::MissingField { suggestion, .. } | Self::ProviderFailed { suggestion, .. } => {
                suggestion.clone()
            },
            Self::TemplateNotFound {
                available_templates,
                content_type,
                ..
            } => {
                if available_templates.is_empty() {
                    Some("Add templates to the templates directory".to_string())
                } else {
                    Some(format!(
                        "Change content type from '{}' to one of: {}",
                        content_type,
                        available_templates.join(", ")
                    ))
                }
            },
            _ => None,
        }
    }

    pub fn cause_string(&self) -> Option<String> {
        match self {
            Self::ProviderFailed { cause, .. }
            | Self::RenderFailed { cause, .. }
            | Self::FetchFailed { cause, .. }
            | Self::PagePrerendererFailed { cause, .. } => Some(cause.clone()),
            _ => None,
        }
    }
}

fn suggest_fix_for_field(field: &str) -> Option<String> {
    match field {
        "image" | "cover_image" => {
            Some("Add 'image: \"/files/images/placeholder.svg\"' to frontmatter".to_string())
        },
        "published_at" | "date" | "created_at" | "published_at/date/created_at" => {
            Some("Add 'date: YYYY-MM-DD' to frontmatter".to_string())
        },
        "author" => Some(
            "Add 'author: Your Name' to frontmatter or set metadata.default_author in config"
                .to_string(),
        ),
        "title" => Some("Add 'title: Your Title' to frontmatter".to_string()),
        "slug" => Some("Add 'slug: your-slug' to frontmatter".to_string()),
        "content_type" => {
            Some("Ensure content has a valid 'kind' field in frontmatter".to_string())
        },
        field if field.starts_with("organization.") => Some(format!(
            "Add '{}' under metadata.structured_data.organization in content.yaml",
            field.strip_prefix("organization.").unwrap_or(field)
        )),
        field if field.starts_with("article.") => Some(format!(
            "Add '{}' under metadata.structured_data.article in content.yaml",
            field.strip_prefix("article.").unwrap_or(field)
        )),
        field if field.starts_with("branding.") => Some(format!(
            "Add '{}' under branding in web.yaml",
            field.strip_prefix("branding.").unwrap_or(field)
        )),
        _ => None,
    }
}