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

//! Integration plugin traits
//!
//! Traits for plugins that sync with external issue trackers
//! (Jira, Linear, GitHub Issues, Notion, etc.)

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

use super::core::{Issue, Plugin, PluginResult};

/// Result of syncing an issue to an external system
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncResult {
    /// Local ProGit issue ID
    pub local_id: String,
    /// ID in the external system
    pub external_id: String,
    /// URL to view in external system (optional)
    pub external_url: Option<String>,
    /// Status of this sync operation
    pub status: SyncStatus,
}

/// Status of a sync operation
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum SyncStatus {
    /// Issue was created in external system
    Created,
    /// Issue was updated in external system
    Updated,
    /// Issue was unchanged (no sync needed)
    Unchanged,
    /// Sync failed with error message
    Failed(String),
    /// Issue was skipped (e.g., filtered out)
    Skipped,
}

/// How to resolve conflicts between local and remote issues
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ConflictResolution {
    /// Use the local version
    TakeLocal,
    /// Use the remote version
    TakeRemote,
    /// Use a custom merged result
    Merge(Issue),
    /// Skip this issue (don't sync)
    Skip,
}

/// Information about an integration plugin
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IntegrationInfo {
    /// Display name: "Jira", "Linear", "GitHub Issues", etc.
    pub name: String,
    /// ASCII/Unicode icon for UI display
    pub icon: Option<String>,
    /// Whether this integration supports bidirectional sync
    pub supports_bidirectional: bool,
    /// Whether this integration can receive webhooks
    pub supports_webhooks: bool,
    /// Authentication method required
    pub auth_type: AuthType,
    /// External system base URL (if configured)
    pub base_url: Option<String>,
}

/// Authentication methods supported by integrations
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum AuthType {
    /// API token (e.g., Jira, Linear)
    ApiToken,
    /// OAuth 2.0 flow
    OAuth2,
    /// Basic HTTP auth (username/password)
    BasicAuth,
    /// No authentication required
    None,
}

/// Link to an issue in an external system
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExternalLink {
    /// Provider name: "jira", "linear", "github", etc.
    pub provider: String,
    /// ID in the external system
    pub external_id: String,
    /// URL to view in external system
    pub external_url: Option<String>,
    /// When this link was last synced
    pub last_synced: String,
    /// Current sync status
    pub sync_status: String,
}

/// Configuration for field mapping between ProGit and external systems
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct FieldMappings {
    /// Status mappings: local -> external
    pub status_outbound: HashMap<String, String>,
    /// Status mappings: external -> local
    pub status_inbound: HashMap<String, String>,
    /// Tag/label mappings
    pub tags: HashMap<String, String>,
    /// Custom field mappings (external field ID -> local field)
    pub custom_fields: HashMap<String, String>,
}

/// Trait for plugins that sync with external issue trackers
pub trait IntegrationPlugin: Plugin {
    /// Push issues to external system
    ///
    /// Called periodically or on-demand to sync issues TO the external system.
    /// Returns list of sync results indicating what happened for each issue.
    fn push_issues(&mut self, issues: &[Issue]) -> PluginResult<Vec<SyncResult>>;

    /// Pull issues from external system
    ///
    /// Called to fetch issues FROM the external system.
    /// Returns issues that should be merged into local storage.
    fn pull_issues(&mut self) -> PluginResult<Vec<Issue>>;

    /// Resolve conflict between local and remote versions
    ///
    /// Called when local and remote versions of an issue have diverged.
    /// Default implementation uses timestamp-based resolution (newer wins).
    fn resolve_conflict(
        &mut self,
        local: &Issue,
        remote: &Issue,
    ) -> PluginResult<ConflictResolution> {
        // Default: remote wins if newer
        Ok(if remote.updated > local.updated {
            ConflictResolution::TakeRemote
        } else {
            ConflictResolution::TakeLocal
        })
    }

    /// Map local status to external status
    ///
    /// Converts ProGit status (e.g., "in-progress") to external system
    /// status (e.g., "In Progress" for Jira).
    fn map_status_outbound(&self, status: &str) -> String;

    /// Map external status to local status
    ///
    /// Converts external system status to ProGit status.
    fn map_status_inbound(&self, external_status: &str) -> String;

    /// Get integration metadata
    ///
    /// Returns information about this integration for UI display and
    /// capability detection.
    fn integration_info(&self) -> IntegrationInfo;

    /// Handle incoming webhook payload
    ///
    /// Process a webhook from the external system. Returns issues that
    /// were affected by the webhook.
    fn handle_webhook(&mut self, _payload: &serde_json::Value) -> PluginResult<Vec<Issue>> {
        // Default: no webhook support
        Ok(vec![])
    }

    /// Get field mappings configuration
    ///
    /// Returns the current field mapping configuration for this integration.
    fn field_mappings(&self) -> FieldMappings {
        FieldMappings::default()
    }

    /// Validate connection to external system
    ///
    /// Tests that the integration can connect to the external system
    /// with the current configuration.
    fn validate_connection(&mut self) -> PluginResult<bool> {
        // Default: assume connected
        Ok(true)
    }
}