systemprompt-models 0.1.21

Shared data models and types for systemprompt.io OS
Documentation
use std::fmt;

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use super::hooks::HookEventsConfig;

const fn default_true() -> bool {
    true
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum ComponentSource {
    #[default]
    Instance,
    Explicit,
}

impl fmt::Display for ComponentSource {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Instance => write!(f, "instance"),
            Self::Explicit => write!(f, "explicit"),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum ComponentFilter {
    Enabled,
}

impl fmt::Display for ComponentFilter {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Enabled => write!(f, "enabled"),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginConfigFile {
    pub plugin: PluginConfig,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct PluginVariableDef {
    pub name: String,
    #[serde(default)]
    pub description: String,
    #[serde(default = "default_true")]
    pub required: bool,
    #[serde(default)]
    pub secret: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub example: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginConfig {
    pub id: String,
    pub name: String,
    pub description: String,
    pub version: String,
    #[serde(default = "default_true")]
    pub enabled: bool,
    pub author: PluginAuthor,
    pub keywords: Vec<String>,
    pub license: String,
    pub category: String,

    pub skills: PluginComponentRef,
    pub agents: PluginComponentRef,
    #[serde(default)]
    pub mcp_servers: Vec<String>,
    #[serde(default)]
    pub hooks: HookEventsConfig,
    #[serde(default)]
    pub scripts: Vec<PluginScript>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PluginComponentRef {
    #[serde(default)]
    pub source: ComponentSource,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub filter: Option<ComponentFilter>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub include: Vec<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub exclude: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginScript {
    pub name: String,
    pub source: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginAuthor {
    pub name: String,
    pub email: String,
}

impl PluginConfig {
    pub fn validate(&self, key: &str) -> anyhow::Result<()> {
        if self.id.len() < 3 || self.id.len() > 50 {
            anyhow::bail!("Plugin '{}': id must be between 3 and 50 characters", key);
        }

        if !self
            .id
            .chars()
            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
        {
            anyhow::bail!(
                "Plugin '{}': id must be lowercase alphanumeric with hyphens only (kebab-case)",
                key
            );
        }

        if self.version.is_empty() {
            anyhow::bail!("Plugin '{}': version must not be empty", key);
        }

        Self::validate_component_ref(&self.skills, key, "skills")?;
        Self::validate_component_ref(&self.agents, key, "agents")?;
        self.hooks.validate()?;

        Ok(())
    }

    fn validate_component_ref(
        component: &PluginComponentRef,
        key: &str,
        field: &str,
    ) -> anyhow::Result<()> {
        if component.source == ComponentSource::Explicit && component.include.is_empty() {
            anyhow::bail!(
                "Plugin '{}': {}.source is 'explicit' but {}.include is empty",
                key,
                field,
                field
            );
        }

        Ok(())
    }
}