progit-plugin-sdk 0.3.0

Plugin SDK for ProGit — sandboxed LuaJIT runtime with capability-based security. LSL-1.0 (file-level copyleft, proprietary plugins allowed via the commercial bridge).
Documentation
// SPDX-License-Identifier: LSL-1.0
// Copyright (c) 2025 Markus Maiwald

//! Canonical plugin event model.
//!
//! [ARCH] One event enum, one source of truth. Both the host and plugins
//! consume `PluginEvent` from this crate. Earlier the host re-defined a
//! parallel enum — that drift is gone as of v0.2.
//!
//! Two parallel surfaces coexist on purpose:
//!
//! - [`crate::traits::PluginHook`] is the *closed* hook enum used by the
//!   convenience traits (`IssuePlugin`, `SyncPlugin`). Maps 1:1 to a Lua
//!   function name.
//! - [`PluginEvent`] is the *open* structured event used for richer
//!   payloads and query-style interactions (e.g. `PipelineStatusQuery`).
//!   Plugins handle it via `plugin.on_event(event)`.

use serde::{Deserialize, Serialize};

/// Plugin lifecycle and operational events.
///
/// Serialised as `{"type": "...", "data": {...}}` so Lua tables stay
/// ergonomic (`event.type == "IssueCreated"`) and Rust code stays exhaustive.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
pub enum PluginEvent {
    /// ProGit just started.
    Startup,
    /// An issue was created.
    IssueCreated { issue_id: String },
    /// An issue was updated.
    IssueUpdated { issue_id: String },
    /// An issue's status changed.
    IssueStatusChanged {
        issue_id: String,
        old_status: String,
        new_status: String,
    },
    /// A commit was created.
    CommitCreated { commit_hash: String },
    /// A virtual branch was created.
    BranchCreated { branch_id: String },
    /// A virtual branch was updated.
    BranchUpdated { branch_id: String },
    /// An agent action ran on a branch.
    AgentAction { action: String, branch_id: String },
    /// CI pipeline status query for a merge request.
    PipelineStatusQuery {
        mr_id: String,
        project_id: String,
        source_branch: String,
        target_branch: String,
        /// `gitlab` | `github` | `forgejo`
        forge_type: String,
        api_url: String,
    },
    /// Plugin-to-plugin custom event.
    Custom {
        name: String,
        payload: serde_json::Value,
    },
}

/// CI/CD pipeline status — return type for `PipelineStatusQuery` events.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineStatus {
    pub status: PipelineState,
    pub pipeline_id: Option<String>,
    pub jobs: Vec<PipelineJob>,
    pub updated_at: Option<String>,
    pub web_url: Option<String>,
}

/// Pipeline lifecycle state.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum PipelineState {
    Passed,
    Failed,
    Running,
    Pending,
    Canceled,
    Skipped,
    Unknown,
}

/// One job in a pipeline.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineJob {
    pub name: String,
    pub status: PipelineState,
    /// Free-form duration string from the forge (e.g. `"2m 14s"`).
    pub duration: Option<String>,
}

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

    #[test]
    fn event_round_trips_through_json() {
        let evt = PluginEvent::IssueStatusChanged {
            issue_id: "abc".into(),
            old_status: "todo".into(),
            new_status: "done".into(),
        };
        let s = serde_json::to_string(&evt).unwrap();
        assert!(s.contains(r#""type":"IssueStatusChanged""#));
        let back: PluginEvent = serde_json::from_str(&s).unwrap();
        match back {
            PluginEvent::IssueStatusChanged { new_status, .. } => assert_eq!(new_status, "done"),
            _ => panic!("variant lost in round-trip"),
        }
    }

    #[test]
    fn pipeline_state_lowercases() {
        let s = serde_json::to_string(&PipelineState::Passed).unwrap();
        assert_eq!(s, "\"passed\"");
    }
}