Skip to main content

progit_plugin_sdk/traits/
core.rs

1// SPDX-License-Identifier: LSL-1.0
2// Copyright (c) 2025 Markus Maiwald
3
4//! Core plugin trait definitions
5//!
6//! These traits define the fundamental contract between ProGit and plugins.
7
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// Plugin metadata
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct PluginMetadata {
14    pub name: String,
15    pub version: String,
16    pub author: String,
17    pub description: String,
18    pub hooks: Vec<PluginHook>,
19}
20
21/// Available plugin hooks
22#[derive(Debug, Clone, Serialize, Deserialize, Eq)]
23pub enum PluginHook {
24    // === Issue Lifecycle ===
25    /// Called when an issue is created
26    OnIssueCreated,
27    /// Called when an issue is updated
28    OnIssueUpdated,
29    /// Called when an issue is deleted
30    OnIssueDeleted,
31    /// Called when an issue status changes
32    OnStatusChanged,
33
34    // === Sync Operations ===
35    /// Called before sync push
36    OnSyncPush,
37    /// Called after sync pull
38    OnSyncPull,
39
40    // === Git Operations ===
41    /// Called when a merge request is created
42    OnMergeRequestCreated,
43
44    // === Custom Commands ===
45    /// Custom command hook
46    OnCommand(String),
47
48    // === Scheduled Operations ===
49    /// Called on a named schedule: "hourly", "daily", "sprint_end"
50    OnSchedule(String),
51
52    // === Bulk Operations ===
53    /// Called before/after bulk operations
54    OnBulkOperation(BulkOp),
55
56    // === Integration Hooks ===
57    /// External system sync requested
58    OnExternalSync,
59    /// Incoming webhook from external system
60    OnWebhookReceived,
61
62    // === Sprint/Time Hooks ===
63    /// Called when a sprint starts
64    OnSprintStart(u32),
65    /// Called when a sprint ends
66    OnSprintEnd(u32),
67    /// Called 24h before an issue's due date
68    OnDueDateApproaching,
69    /// Called when an issue's due date has passed
70    OnDueDatePassed,
71
72    // === Analytics Hooks ===
73    /// Report generation requested
74    OnReportRequested,
75    /// Metric computation requested
76    OnMetricQuery,
77}
78
79// NOTE: OnCommand variants compare equal regardless of the command string
80// This allows "hooks" manifest to match OnCommand("hooks") dispatch
81impl PartialEq for PluginHook {
82    fn eq(&self, other: &Self) -> bool {
83        match (self, other) {
84            (PluginHook::OnCommand(_), PluginHook::OnCommand(_)) => true,
85            _ => core::mem::discriminant(self) == core::mem::discriminant(other),
86        }
87    }
88}
89
90/// Bulk operation types
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
92pub enum BulkOp {
93    Import,
94    Export,
95    Archive,
96    Delete,
97}
98
99/// Issue representation for plugins
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct Issue {
102    pub id: String,
103    pub title: String,
104    pub description: String,
105    pub status: String,
106    pub tags: Vec<String>,
107    pub assignee: Option<String>,
108    pub effort: Option<u8>,
109    pub blocked: bool,
110    pub created: String,
111    pub updated: String,
112    pub due: Option<String>,
113    pub metadata: HashMap<String, serde_json::Value>,
114}
115
116/// Plugin execution context
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct PluginContext {
119    /// Current repository path
120    pub repo_path: String,
121    /// Current user
122    pub user: Option<String>,
123    /// Environment variables accessible to plugin
124    pub env: HashMap<String, String>,
125    /// Plugin configuration
126    pub config: HashMap<String, serde_json::Value>,
127}
128
129/// Plugin execution result
130pub type PluginResult<T> = Result<T, PluginError>;
131
132/// Plugin error types
133#[derive(Debug, thiserror::Error)]
134pub enum PluginError {
135    #[error("Plugin initialization failed: {0}")]
136    InitError(String),
137
138    #[error("Plugin execution failed: {0}")]
139    ExecutionError(String),
140
141    #[error("Invalid plugin configuration: {0}")]
142    ConfigError(String),
143
144    #[error("Hook not supported: {0:?}")]
145    UnsupportedHook(PluginHook),
146
147    #[error("Serialization error: {0}")]
148    SerializationError(#[from] serde_json::Error),
149
150    #[error("IO error: {0}")]
151    IoError(#[from] std::io::Error),
152
153    #[error("Storage error: {0}")]
154    StorageError(String),
155
156    #[error("Sync error: {0}")]
157    SyncError(String),
158
159    #[error("External API error: {0}")]
160    ExternalApiError(String),
161}
162
163/// Core plugin trait
164///
165/// Note: Plugins are single-threaded by design for simplicity and safety.
166/// The ProGit TUI loads and executes plugins on the main thread only.
167pub trait Plugin {
168    /// Get plugin metadata
169    fn metadata(&self) -> &PluginMetadata;
170
171    /// Initialize the plugin with context
172    fn init(&mut self, context: &PluginContext) -> PluginResult<()>;
173
174    /// Execute a hook
175    fn execute_hook(
176        &mut self,
177        hook: &PluginHook,
178        data: &serde_json::Value,
179    ) -> PluginResult<serde_json::Value>;
180
181    /// Check if plugin supports a specific hook
182    fn supports_hook(&self, hook: &PluginHook) -> bool {
183        self.metadata().hooks.contains(hook)
184    }
185
186    /// Handle a plugin event (new event system)
187    ///
188    /// This method supports the event-based plugin API where plugins receive
189    /// structured events and return optional responses. This is newer than
190    /// the hook-based system and is used for query-style interactions like
191    /// CI/CD status queries.
192    ///
193    /// Returns `Ok(None)` if the plugin doesn't handle this event type.
194    /// Returns `Ok(Some(response))` with the response data.
195    fn on_event(&mut self, event: &serde_json::Value) -> PluginResult<Option<serde_json::Value>> {
196        // Default implementation: plugin doesn't support events
197        let _ = event; // Suppress unused warning
198        Ok(None)
199    }
200
201    /// Render-time hook: provide syntax-highlighted spans for a chunk of text.
202    ///
203    /// Called from the host's frame-render path (e.g. the diff renderer).
204    /// The host caches results aggressively, but on a cache miss the
205    /// plugin must respond fast — keep the implementation linear in
206    /// `content` length and avoid I/O.
207    ///
208    /// Returning `Ok(None)` means "not a highlight provider for this
209    /// language" — the host tries the next plugin, or falls through to
210    /// plain text. Plugins that *are* highlight providers but cannot
211    /// handle a specific language should also return `None`, not an error.
212    fn highlight(
213        &mut self,
214        request: &crate::render::HighlightRequest,
215    ) -> PluginResult<Option<crate::render::HighlightResponse>> {
216        let _ = request;
217        Ok(None)
218    }
219}
220
221/// Convenience trait for issue lifecycle hooks
222pub trait IssuePlugin: Plugin {
223    fn on_issue_created(&mut self, issue: &Issue) -> PluginResult<()> {
224        let data = serde_json::to_value(issue)?;
225        self.execute_hook(&PluginHook::OnIssueCreated, &data)?;
226        Ok(())
227    }
228
229    fn on_issue_updated(&mut self, issue: &Issue) -> PluginResult<()> {
230        let data = serde_json::to_value(issue)?;
231        self.execute_hook(&PluginHook::OnIssueUpdated, &data)?;
232        Ok(())
233    }
234
235    fn on_issue_deleted(&mut self, issue_id: &str) -> PluginResult<()> {
236        let data = serde_json::json!({ "id": issue_id });
237        self.execute_hook(&PluginHook::OnIssueDeleted, &data)?;
238        Ok(())
239    }
240}
241
242/// Convenience trait for sync hooks
243pub trait SyncPlugin: Plugin {
244    fn on_sync_push(&mut self, issues: &[Issue]) -> PluginResult<()> {
245        let data = serde_json::to_value(issues)?;
246        self.execute_hook(&PluginHook::OnSyncPush, &data)?;
247        Ok(())
248    }
249
250    fn on_sync_pull(&mut self, issues: &[Issue]) -> PluginResult<()> {
251        let data = serde_json::to_value(issues)?;
252        self.execute_hook(&PluginHook::OnSyncPull, &data)?;
253        Ok(())
254    }
255}