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