roder-api 0.1.0

Agentic software development tools and SDKs for Roder.
Documentation
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;

pub type WorkflowImportId = String;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "camelCase")]
pub enum WorkflowSourceType {
    Guidance,
    Skill,
    McpServer,
    SlashCommand,
    Hook,
    Plugin,
    Unknown,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum WorkflowImportState {
    Detected,
    Previewed,
    Enabled,
    Ignored,
    Disabled,
    Removed,
    Stale,
    Failed,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum WorkflowImportRisk {
    Passive,
    ReadsWorkspace,
    StartsProcess,
    RunsHook,
    Unknown,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowSource {
    pub source_type: WorkflowSourceType,
    pub path: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    pub hash: String,
    #[serde(with = "time::serde::rfc3339")]
    pub detected_at: OffsetDateTime,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowConflict {
    pub field: String,
    pub existing: String,
    pub incoming: String,
    pub detail: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowImportItem {
    pub id: WorkflowImportId,
    pub title: String,
    pub summary: String,
    pub source: WorkflowSource,
    pub state: WorkflowImportState,
    pub risk: WorkflowImportRisk,
    #[serde(default)]
    pub command_capable: bool,
    #[serde(default)]
    pub approval_required: bool,
    #[serde(default)]
    pub preview: serde_json::Value,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub conflicts: Vec<WorkflowConflict>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    #[serde(with = "time::serde::rfc3339::option")]
    pub enabled_at: Option<OffsetDateTime>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum WorkflowImportDecisionKind {
    Preview,
    Enable,
    Ignore,
    Disable,
    Refresh,
    Remove,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowImportDecision {
    pub item_id: WorkflowImportId,
    pub decision: WorkflowImportDecisionKind,
    pub source_hash: String,
    #[serde(default)]
    pub approved_side_effects: bool,
    #[serde(with = "time::serde::rfc3339")]
    pub decided_at: OffsetDateTime,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowImportScan {
    pub workspace: String,
    pub items: Vec<WorkflowImportItem>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub errors: Vec<WorkflowImportError>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowImportError {
    pub path: String,
    pub message: String,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn workflow_import_records_are_camel_case_and_source_attributed() {
        let item = WorkflowImportItem {
            id: "skill-demo".to_string(),
            title: "Demo Skill".to_string(),
            summary: "Imported skill".to_string(),
            source: WorkflowSource {
                source_type: WorkflowSourceType::Skill,
                path: ".agents/skills/demo/SKILL.md".to_string(),
                name: Some("demo".to_string()),
                hash: "abc123".to_string(),
                detected_at: OffsetDateTime::UNIX_EPOCH,
            },
            state: WorkflowImportState::Detected,
            risk: WorkflowImportRisk::Passive,
            command_capable: false,
            approval_required: false,
            preview: serde_json::json!({ "description": "safe" }),
            conflicts: Vec::new(),
            enabled_at: None,
        };

        let value = serde_json::to_value(&item).unwrap();

        assert_eq!(value["source"]["sourceType"], "skill");
        assert_eq!(value["source"]["detectedAt"], "1970-01-01T00:00:00Z");
        assert_eq!(value["commandCapable"], false);
        assert_eq!(value["source"]["path"], ".agents/skills/demo/SKILL.md");
    }

    #[test]
    fn command_capable_imports_can_require_approval() {
        let decision = WorkflowImportDecision {
            item_id: "mcp-server-local".to_string(),
            decision: WorkflowImportDecisionKind::Enable,
            source_hash: "hash".to_string(),
            approved_side_effects: true,
            decided_at: OffsetDateTime::UNIX_EPOCH,
        };

        let value = serde_json::to_value(decision).unwrap();

        assert_eq!(value["approvedSideEffects"], true);
        assert_eq!(value["decision"], "enable");
    }
}