systemprompt-content 0.2.2

Markdown content management, sources, and event tracking for systemprompt.io AI governance dashboards. Governed publishing pipeline for the MCP governance platform.
Documentation
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use sqlx::FromRow;
use systemprompt_identifiers::{CategoryId, ContentId, SourceId, TagId};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum ContentKind {
    #[default]
    Article,
    Guide,
    Tutorial,
}

impl ContentKind {
    pub const fn as_str(&self) -> &'static str {
        match self {
            Self::Article => "article",
            Self::Guide => "guide",
            Self::Tutorial => "tutorial",
        }
    }
}

impl std::fmt::Display for ContentKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Content {
    pub id: ContentId,
    pub slug: String,
    pub title: String,
    pub description: String,
    pub body: String,
    pub author: String,
    pub published_at: DateTime<Utc>,
    pub keywords: String,
    pub kind: String,
    pub image: Option<String>,
    pub category_id: Option<CategoryId>,
    pub source_id: SourceId,
    pub version_hash: String,
    pub public: bool,
    #[serde(default)]
    pub links: JsonValue,
    pub updated_at: DateTime<Utc>,
}

impl Content {
    pub fn links_metadata(&self) -> Result<Vec<ContentLinkMetadata>, serde_json::Error> {
        serde_json::from_value(self.links.clone())
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContentSummary {
    pub id: ContentId,
    pub slug: String,
    pub title: String,
    pub description: String,
    pub published_at: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContentMetadata {
    pub title: String,
    #[serde(default)]
    pub description: String,
    #[serde(default)]
    pub author: String,
    pub published_at: String,
    pub slug: String,
    #[serde(default)]
    pub keywords: String,
    pub kind: String,
    #[serde(default)]
    pub image: Option<String>,
    #[serde(default)]
    pub category: Option<String>,
    #[serde(default)]
    pub tags: Vec<String>,
    #[serde(default)]
    pub links: Vec<ContentLinkMetadata>,
    #[serde(default)]
    pub public: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContentLinkMetadata {
    pub title: String,
    pub url: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Tag {
    pub id: TagId,
    pub name: String,
    pub slug: String,
    pub created_at: Option<DateTime<Utc>>,
    pub updated_at: Option<DateTime<Utc>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IngestionReport {
    pub files_found: usize,
    pub files_processed: usize,
    pub errors: Vec<String>,
    #[serde(default)]
    pub warnings: Vec<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub would_create: Vec<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub would_update: Vec<String>,
    #[serde(default)]
    pub unchanged_count: usize,
    #[serde(default)]
    pub skipped_count: usize,
}

impl IngestionReport {
    pub const fn new() -> Self {
        Self {
            files_found: 0,
            files_processed: 0,
            errors: Vec::new(),
            warnings: Vec::new(),
            would_create: Vec::new(),
            would_update: Vec::new(),
            unchanged_count: 0,
            skipped_count: 0,
        }
    }

    pub fn is_success(&self) -> bool {
        self.errors.is_empty()
    }
}

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

#[derive(Debug, Clone, Copy, Default)]
pub struct IngestionOptions {
    pub override_existing: bool,
    pub recursive: bool,
    pub dry_run: bool,
}

impl IngestionOptions {
    pub const fn with_override(mut self, override_existing: bool) -> Self {
        self.override_existing = override_existing;
        self
    }

    pub const fn with_recursive(mut self, recursive: bool) -> Self {
        self.recursive = recursive;
        self
    }

    pub const fn with_dry_run(mut self, dry_run: bool) -> Self {
        self.dry_run = dry_run;
        self
    }
}

#[derive(Debug, Clone)]
pub struct IngestionSource<'a> {
    pub source_id: &'a SourceId,
    pub source_name: &'a str,
    pub category_id: &'a CategoryId,
}

impl<'a> IngestionSource<'a> {
    pub const fn new(
        source_id: &'a SourceId,
        source_name: &'a str,
        category_id: &'a CategoryId,
    ) -> Self {
        Self {
            source_id,
            source_name,
            category_id,
        }
    }
}