Skip to main content

everruns_core/
connector.rs

1// Connector Plugin System
2//
3// Decision: Parallel to IntegrationPlugin, allows integration crates to register
4// connectors via inventory::submit! without core knowing about them.
5// Decision: Form schema is backend-driven — connectors define their own UI fields
6// and instructions, frontend renders generically.
7// Decision: Validation is async — connectors can call external APIs to verify credentials.
8
9use async_trait::async_trait;
10use serde::Serialize;
11use std::collections::HashMap;
12use std::sync::Arc;
13
14// ============================================================================
15// Plugin Registration
16// ============================================================================
17
18/// Plugin registration point for connector crates.
19///
20/// Integration crates use `inventory::submit!` to register their connectors.
21/// The server discovers them at runtime to serve form schemas
22/// and handle credential submission.
23///
24/// # Example
25///
26/// ```ignore
27/// inventory::submit! {
28///     ConnectorPlugin {
29///         experimental_only: true,
30///         factory: || Box::new(DaytonaConnector),
31///     }
32/// }
33/// ```
34pub struct ConnectorPlugin {
35    /// If true, only registered when experimental features are enabled.
36    pub experimental_only: bool,
37    /// Factory function that creates the provider instance.
38    pub factory: fn() -> Box<dyn Connector>,
39}
40
41inventory::collect!(ConnectorPlugin);
42
43// ============================================================================
44// Connector Trait
45// ============================================================================
46
47/// How the user provides credentials for this connection.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
49#[serde(rename_all = "snake_case")]
50pub enum ConnectorType {
51    /// OAuth flow (redirect-based, e.g. GitHub)
52    OAuth,
53    /// Direct API key entry (form-based, e.g. Daytona)
54    ApiKey,
55}
56
57/// A connection provider that can validate credentials and describe its UI form.
58#[async_trait]
59pub trait Connector: Send + Sync {
60    /// Unique provider identifier (e.g. "daytona"). Must match `user_connections.provider`.
61    fn provider_id(&self) -> &str;
62
63    /// Human-readable name (e.g. "Daytona").
64    fn display_name(&self) -> &str;
65
66    /// Short description for the connections UI.
67    fn description(&self) -> &str;
68
69    /// Lucide icon name (e.g. "cloud", "github").
70    fn icon(&self) -> &str;
71
72    /// Whether this provider uses OAuth or direct API key entry.
73    fn connection_type(&self) -> ConnectorType;
74
75    /// Form schema for API key providers. OAuth providers return None.
76    fn form_schema(&self) -> Option<ConnectorFormSchema>;
77
78    /// Validate a credential before saving. Called for API key providers.
79    /// Returns Ok with optional metadata on success, Err with user-facing message on failure.
80    async fn validate(&self, credential: &str) -> Result<ConnectorValidation, String>;
81
82    /// Validate with all form fields. Default delegates to `validate()` using the `api_key` field.
83    /// Override this when the provider needs extra fields (e.g. org slug for personal tokens).
84    async fn validate_fields(
85        &self,
86        fields: &HashMap<String, String>,
87    ) -> Result<ConnectorValidation, String> {
88        let api_key = fields.get("api_key").map(|s| s.as_str()).unwrap_or("");
89        self.validate(api_key).await
90    }
91}
92
93// ============================================================================
94// Connector Registry
95// ============================================================================
96
97/// Registry of connection providers available to a platform.
98///
99/// This is the explicit counterpart to `ConnectorPlugin`. Inventory-based
100/// discovery remains useful for the default OSS platform, but embedders need a
101/// concrete registry they can edit before handing the platform to the server.
102#[derive(Clone, Default)]
103pub struct ConnectorRegistry {
104    providers: HashMap<String, Arc<dyn Connector>>,
105}
106
107impl ConnectorRegistry {
108    /// Create an empty provider registry.
109    pub fn new() -> Self {
110        Self {
111            providers: HashMap::new(),
112        }
113    }
114
115    /// Register a connection provider.
116    ///
117    /// If a provider with the same `provider_id()` already exists, it is replaced.
118    pub fn register(&mut self, provider: impl Connector + 'static) {
119        self.providers
120            .insert(provider.provider_id().to_string(), Arc::new(provider));
121    }
122
123    /// Register a boxed connection provider.
124    pub fn register_boxed(&mut self, provider: Box<dyn Connector>) {
125        self.providers
126            .insert(provider.provider_id().to_string(), Arc::from(provider));
127    }
128
129    /// Register an `Arc`-wrapped connection provider.
130    pub fn register_arc(&mut self, provider: Arc<dyn Connector>) {
131        self.providers
132            .insert(provider.provider_id().to_string(), provider);
133    }
134
135    /// Remove a provider by ID.
136    pub fn unregister(&mut self, provider_id: &str) -> Option<Arc<dyn Connector>> {
137        self.providers.remove(provider_id)
138    }
139
140    /// Get a provider by ID.
141    pub fn get(&self, provider_id: &str) -> Option<&Arc<dyn Connector>> {
142        self.providers.get(provider_id)
143    }
144
145    /// Check whether a provider is registered.
146    pub fn has(&self, provider_id: &str) -> bool {
147        self.providers.contains_key(provider_id)
148    }
149
150    /// List all registered providers.
151    pub fn list(&self) -> Vec<&Arc<dyn Connector>> {
152        self.providers.values().collect()
153    }
154
155    /// Number of registered providers.
156    pub fn len(&self) -> usize {
157        self.providers.len()
158    }
159
160    /// Whether the registry is empty.
161    pub fn is_empty(&self) -> bool {
162        self.providers.is_empty()
163    }
164
165    /// Create a builder for fluent registration.
166    pub fn builder() -> ConnectorRegistryBuilder {
167        ConnectorRegistryBuilder::new()
168    }
169}
170
171impl std::fmt::Debug for ConnectorRegistry {
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        let ids: Vec<_> = self.providers.keys().collect();
174        f.debug_struct("ConnectorRegistry")
175            .field("providers", &ids)
176            .finish()
177    }
178}
179
180/// Builder for creating a `ConnectorRegistry`.
181pub struct ConnectorRegistryBuilder {
182    registry: ConnectorRegistry,
183}
184
185impl ConnectorRegistryBuilder {
186    /// Create a new empty builder.
187    pub fn new() -> Self {
188        Self {
189            registry: ConnectorRegistry::new(),
190        }
191    }
192
193    /// Add a connection provider to the registry.
194    pub fn provider(mut self, provider: impl Connector + 'static) -> Self {
195        self.registry.register(provider);
196        self
197    }
198
199    /// Build the registry.
200    pub fn build(self) -> ConnectorRegistry {
201        self.registry
202    }
203}
204
205impl Default for ConnectorRegistryBuilder {
206    fn default() -> Self {
207        Self::new()
208    }
209}
210
211// ============================================================================
212// Form Schema Types
213// ============================================================================
214
215// Form schema types are shared with provider drivers; see
216// `crate::credential_schema` and specs/providers.md "Credentials".
217pub use crate::credential_schema::{FieldType, FormField};
218
219/// Credential form schema for connectors.
220pub type ConnectorFormSchema = crate::credential_schema::CredentialFormSchema;
221
222/// Result of credential validation.
223#[derive(Debug, Clone)]
224pub struct ConnectorValidation {
225    /// Display name from the provider (e.g. organization name, username).
226    pub provider_username: Option<String>,
227    /// Provider-specific metadata to store alongside the connection (e.g. org slug).
228    /// Stored as JSONB in the database and returned via the connection resolver.
229    pub provider_metadata: Option<serde_json::Value>,
230}