1pub mod any;
2mod asset;
3pub mod builder;
4pub mod capabilities;
5pub mod context;
6pub mod error;
7pub mod hlist;
8pub mod registry;
9pub mod runtime_config;
10pub mod typed;
11pub mod typed_registry;
12pub mod types;
13
14use std::path::PathBuf;
15use std::sync::Arc;
16
17use serde::{Deserialize, Serialize};
18use serde_json::Value as JsonValue;
19use systemprompt_provider_contracts::{
20 ComponentRenderer, Job, LlmProvider, PageDataProvider, TemplateDataExtender, TemplateProvider,
21 ToolProvider,
22};
23
24pub use asset::{AssetDefinition, AssetDefinitionBuilder, AssetType};
25pub use context::{DynExtensionContext, ExtensionContext};
26pub use error::{ConfigError, LoaderError};
27pub use registry::{ExtensionRegistration, ExtensionRegistry};
28
29#[derive(Debug, Clone)]
30pub struct Migration {
31 pub version: u32,
32 pub name: String,
33 pub sql: &'static str,
34}
35
36impl Migration {
37 #[must_use]
38 pub fn new(version: u32, name: impl Into<String>, sql: &'static str) -> Self {
39 Self {
40 version,
41 name: name.into(),
42 sql,
43 }
44 }
45
46 #[must_use]
47 pub fn checksum(&self) -> String {
48 use std::hash::{Hash, Hasher};
49 let mut hasher = std::collections::hash_map::DefaultHasher::new();
50 self.sql.hash(&mut hasher);
51 format!("{:x}", hasher.finish())
52 }
53}
54
55#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
56pub struct ExtensionMetadata {
57 pub id: &'static str,
58 pub name: &'static str,
59 pub version: &'static str,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct SchemaDefinition {
64 pub table: String,
65 pub sql: SchemaSource,
66 pub required_columns: Vec<String>,
67}
68
69impl SchemaDefinition {
70 #[must_use]
71 pub fn inline(table: impl Into<String>, sql: impl Into<String>) -> Self {
72 Self {
73 table: table.into(),
74 sql: SchemaSource::Inline(sql.into()),
75 required_columns: Vec::new(),
76 }
77 }
78
79 #[must_use]
80 pub fn file(table: impl Into<String>, path: impl Into<PathBuf>) -> Self {
81 Self {
82 table: table.into(),
83 sql: SchemaSource::File(path.into()),
84 required_columns: Vec::new(),
85 }
86 }
87
88 #[must_use]
89 pub fn with_required_columns(mut self, columns: Vec<String>) -> Self {
90 self.required_columns = columns;
91 self
92 }
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub enum SchemaSource {
97 Inline(String),
98 File(PathBuf),
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub enum SeedSource {
103 Inline(String),
104 File(PathBuf),
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct ExtensionRole {
109 pub name: String,
110 pub display_name: String,
111 pub description: String,
112 #[serde(default)]
113 pub permissions: Vec<String>,
114}
115
116impl ExtensionRole {
117 #[must_use]
118 pub fn new(
119 name: impl Into<String>,
120 display_name: impl Into<String>,
121 description: impl Into<String>,
122 ) -> Self {
123 Self {
124 name: name.into(),
125 display_name: display_name.into(),
126 description: description.into(),
127 permissions: Vec::new(),
128 }
129 }
130
131 #[must_use]
132 pub fn with_permissions(mut self, permissions: Vec<String>) -> Self {
133 self.permissions = permissions;
134 self
135 }
136}
137
138#[derive(Debug, Clone, Copy)]
139pub struct ExtensionRouterConfig {
140 pub base_path: &'static str,
141 pub requires_auth: bool,
142}
143
144impl ExtensionRouterConfig {
145 #[must_use]
146 pub const fn new(base_path: &'static str) -> Self {
147 Self {
148 base_path,
149 requires_auth: true,
150 }
151 }
152
153 #[must_use]
154 pub const fn public(base_path: &'static str) -> Self {
155 Self {
156 base_path,
157 requires_auth: false,
158 }
159 }
160}
161
162#[cfg(feature = "web")]
163#[derive(Debug, Clone)]
164pub struct ExtensionRouter {
165 pub router: axum::Router,
166 pub base_path: &'static str,
167 pub requires_auth: bool,
168}
169
170#[cfg(feature = "web")]
171impl ExtensionRouter {
172 #[must_use]
173 pub const fn new(router: axum::Router, base_path: &'static str) -> Self {
174 Self {
175 router,
176 base_path,
177 requires_auth: true,
178 }
179 }
180
181 #[must_use]
182 pub const fn public(router: axum::Router, base_path: &'static str) -> Self {
183 Self {
184 router,
185 base_path,
186 requires_auth: false,
187 }
188 }
189
190 #[must_use]
191 pub const fn config(&self) -> ExtensionRouterConfig {
192 ExtensionRouterConfig {
193 base_path: self.base_path,
194 requires_auth: self.requires_auth,
195 }
196 }
197}
198
199pub trait Extension: Send + Sync + 'static {
200 fn metadata(&self) -> ExtensionMetadata;
201
202 fn schemas(&self) -> Vec<SchemaDefinition> {
203 vec![]
204 }
205
206 fn migration_weight(&self) -> u32 {
207 100
208 }
209
210 #[cfg(feature = "web")]
211 fn router(&self, ctx: &dyn ExtensionContext) -> Option<ExtensionRouter> {
212 let _ = ctx;
213 None
214 }
215
216 fn router_config(&self) -> Option<ExtensionRouterConfig> {
217 None
218 }
219
220 fn jobs(&self) -> Vec<Arc<dyn Job>> {
221 vec![]
222 }
223
224 fn config_prefix(&self) -> Option<&str> {
225 None
226 }
227
228 fn config_schema(&self) -> Option<JsonValue> {
229 None
230 }
231
232 fn validate_config(&self, config: &JsonValue) -> Result<(), ConfigError> {
233 let _ = config;
234 Ok(())
235 }
236
237 fn llm_providers(&self) -> Vec<Arc<dyn LlmProvider>> {
238 vec![]
239 }
240
241 fn tool_providers(&self) -> Vec<Arc<dyn ToolProvider>> {
242 vec![]
243 }
244
245 fn template_providers(&self) -> Vec<Arc<dyn TemplateProvider>> {
246 vec![]
247 }
248
249 fn component_renderers(&self) -> Vec<Arc<dyn ComponentRenderer>> {
250 vec![]
251 }
252
253 fn template_data_extenders(&self) -> Vec<Arc<dyn TemplateDataExtender>> {
254 vec![]
255 }
256
257 fn page_data_providers(&self) -> Vec<Arc<dyn PageDataProvider>> {
258 vec![]
259 }
260
261 fn required_storage_paths(&self) -> Vec<&'static str> {
262 vec![]
263 }
264
265 fn dependencies(&self) -> Vec<&'static str> {
266 vec![]
267 }
268
269 fn is_required(&self) -> bool {
270 false
271 }
272
273 fn migrations(&self) -> Vec<Migration> {
274 vec![]
275 }
276
277 fn roles(&self) -> Vec<ExtensionRole> {
278 vec![]
279 }
280
281 fn priority(&self) -> u32 {
282 100
283 }
284
285 fn id(&self) -> &'static str {
286 self.metadata().id
287 }
288
289 fn name(&self) -> &'static str {
290 self.metadata().name
291 }
292
293 fn version(&self) -> &'static str {
294 self.metadata().version
295 }
296
297 fn has_schemas(&self) -> bool {
298 !self.schemas().is_empty()
299 }
300
301 #[cfg(feature = "web")]
302 fn has_router(&self, ctx: &dyn ExtensionContext) -> bool {
303 self.router(ctx).is_some()
304 }
305
306 #[cfg(not(feature = "web"))]
307 fn has_router(&self, _ctx: &dyn ExtensionContext) -> bool {
308 false
309 }
310
311 fn has_jobs(&self) -> bool {
312 !self.jobs().is_empty()
313 }
314
315 fn has_config(&self) -> bool {
316 self.config_prefix().is_some()
317 }
318
319 fn has_llm_providers(&self) -> bool {
320 !self.llm_providers().is_empty()
321 }
322
323 fn has_tool_providers(&self) -> bool {
324 !self.tool_providers().is_empty()
325 }
326
327 fn has_template_providers(&self) -> bool {
328 !self.template_providers().is_empty()
329 }
330
331 fn has_component_renderers(&self) -> bool {
332 !self.component_renderers().is_empty()
333 }
334
335 fn has_template_data_extenders(&self) -> bool {
336 !self.template_data_extenders().is_empty()
337 }
338
339 fn has_page_data_providers(&self) -> bool {
340 !self.page_data_providers().is_empty()
341 }
342
343 fn has_storage_paths(&self) -> bool {
344 !self.required_storage_paths().is_empty()
345 }
346
347 fn has_roles(&self) -> bool {
348 !self.roles().is_empty()
349 }
350
351 fn has_migrations(&self) -> bool {
352 !self.migrations().is_empty()
353 }
354
355 fn required_assets(&self) -> Vec<AssetDefinition> {
356 vec![]
357 }
358
359 fn has_assets(&self) -> bool {
360 !self.required_assets().is_empty()
361 }
362}
363
364#[macro_export]
365macro_rules! register_extension {
366 ($ext_type:ty) => {
367 ::inventory::submit! {
368 $crate::ExtensionRegistration {
369 factory: || ::std::sync::Arc::new(<$ext_type>::default()) as ::std::sync::Arc<dyn $crate::Extension>,
370 }
371 }
372 };
373 ($ext_expr:expr) => {
374 ::inventory::submit! {
375 $crate::ExtensionRegistration {
376 factory: || ::std::sync::Arc::new($ext_expr) as ::std::sync::Arc<dyn $crate::Extension>,
377 }
378 }
379 };
380}
381
382pub mod prelude {
383 pub use crate::asset::{AssetDefinition, AssetDefinitionBuilder, AssetType};
384 pub use crate::context::{DynExtensionContext, ExtensionContext};
385 pub use crate::error::{ConfigError, LoaderError};
386 pub use crate::registry::ExtensionRegistry;
387 pub use crate::{
388 register_extension, Extension, ExtensionMetadata, ExtensionRole, Migration,
389 SchemaDefinition, SchemaSource,
390 };
391
392 #[cfg(feature = "web")]
393 pub use crate::ExtensionRouter;
394
395 pub use crate::any::AnyExtension;
396 pub use crate::builder::ExtensionBuilder;
397 pub use crate::capabilities::{
398 CapabilityContext, FullContext, HasConfig, HasDatabase, HasEventBus, HasExtension,
399 };
400
401 #[cfg(feature = "web")]
402 pub use crate::capabilities::HasHttpClient;
403 pub use crate::hlist::{Contains, NotSame, Subset, TypeList};
404 pub use crate::typed::{
405 ApiExtensionTyped, ConfigExtensionTyped, JobExtensionTyped, ProviderExtensionTyped,
406 SchemaDefinitionTyped, SchemaExtensionTyped, SchemaSourceTyped,
407 };
408
409 #[cfg(feature = "web")]
410 pub use crate::typed::ApiExtensionTypedDyn;
411 pub use crate::typed_registry::{TypedExtensionRegistry, RESERVED_PATHS};
412 pub use crate::types::{
413 Dependencies, DependencyList, ExtensionMeta, ExtensionType, MissingDependency,
414 NoDependencies,
415 };
416
417 pub use systemprompt_provider_contracts::{
418 ComponentContext, ComponentRenderer, PageContext, PageDataProvider, RenderedComponent,
419 TemplateDataExtender, TemplateDefinition, TemplateProvider, TemplateSource,
420 };
421}
422
423#[cfg(feature = "web")]
424pub use any::ApiExtensionWrapper;
425pub use any::{AnyExtension, ExtensionWrapper, SchemaExtensionWrapper};
426pub use builder::ExtensionBuilder;
427#[cfg(feature = "web")]
428pub use capabilities::HasHttpClient;
429pub use capabilities::{
430 CapabilityContext, FullContext, HasConfig, HasDatabase, HasEventBus, HasExtension,
431};
432pub use hlist::{Contains, NotSame, Subset, TypeList};
433#[cfg(feature = "web")]
434pub use typed::ApiExtensionTypedDyn;
435pub use typed::{
436 ApiExtensionTyped, ConfigExtensionTyped, JobExtensionTyped, ProviderExtensionTyped,
437 SchemaDefinitionTyped, SchemaExtensionTyped, SchemaSourceTyped,
438};
439pub use typed_registry::{TypedExtensionRegistry, RESERVED_PATHS};
440pub use types::{
441 Dependencies, DependencyList, ExtensionMeta, ExtensionType, MissingDependency, NoDependencies,
442};