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}