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 protected_prefixes: &'static [&'static str],
167 pub public_prefixes: &'static [&'static str],
168 pub required_scope: &'static str,
169}
170
171#[cfg(feature = "web")]
172#[derive(Debug, Clone)]
173pub struct ExtensionRouter {
174 pub router: axum::Router,
175 pub base_path: &'static str,
176 pub requires_auth: bool,
177}
178
179#[cfg(feature = "web")]
180impl ExtensionRouter {
181 #[must_use]
182 pub const fn new(router: axum::Router, base_path: &'static str) -> Self {
183 Self {
184 router,
185 base_path,
186 requires_auth: true,
187 }
188 }
189
190 #[must_use]
191 pub const fn public(router: axum::Router, base_path: &'static str) -> Self {
192 Self {
193 router,
194 base_path,
195 requires_auth: false,
196 }
197 }
198
199 #[must_use]
200 pub const fn config(&self) -> ExtensionRouterConfig {
201 ExtensionRouterConfig {
202 base_path: self.base_path,
203 requires_auth: self.requires_auth,
204 }
205 }
206}
207
208pub trait Extension: Send + Sync + 'static {
209 fn metadata(&self) -> ExtensionMetadata;
210
211 fn schemas(&self) -> Vec<SchemaDefinition> {
212 vec![]
213 }
214
215 fn migration_weight(&self) -> u32 {
216 100
217 }
218
219 #[cfg(feature = "web")]
220 fn router(&self, ctx: &dyn ExtensionContext) -> Option<ExtensionRouter> {
221 let _ = ctx;
222 None
223 }
224
225 fn router_config(&self) -> Option<ExtensionRouterConfig> {
226 None
227 }
228
229 fn site_auth(&self) -> Option<SiteAuthConfig> {
230 None
231 }
232
233 fn jobs(&self) -> Vec<Arc<dyn Job>> {
234 vec![]
235 }
236
237 fn config_prefix(&self) -> Option<&str> {
238 None
239 }
240
241 fn config_schema(&self) -> Option<JsonValue> {
242 None
243 }
244
245 fn validate_config(&self, config: &JsonValue) -> Result<(), ConfigError> {
246 let _ = config;
247 Ok(())
248 }
249
250 fn llm_providers(&self) -> Vec<Arc<dyn LlmProvider>> {
251 vec![]
252 }
253
254 fn tool_providers(&self) -> Vec<Arc<dyn ToolProvider>> {
255 vec![]
256 }
257
258 fn template_providers(&self) -> Vec<Arc<dyn TemplateProvider>> {
259 vec![]
260 }
261
262 fn component_renderers(&self) -> Vec<Arc<dyn ComponentRenderer>> {
263 vec![]
264 }
265
266 fn template_data_extenders(&self) -> Vec<Arc<dyn TemplateDataExtender>> {
267 vec![]
268 }
269
270 fn page_data_providers(&self) -> Vec<Arc<dyn PageDataProvider>> {
271 vec![]
272 }
273
274 fn page_prerenderers(&self) -> Vec<Arc<dyn PagePrerenderer>> {
275 vec![]
276 }
277
278 fn frontmatter_processors(&self) -> Vec<Arc<dyn FrontmatterProcessor>> {
279 vec![]
280 }
281
282 fn content_data_providers(&self) -> Vec<Arc<dyn ContentDataProvider>> {
283 vec![]
284 }
285
286 fn rss_feed_providers(&self) -> Vec<Arc<dyn RssFeedProvider>> {
287 vec![]
288 }
289
290 fn sitemap_providers(&self) -> Vec<Arc<dyn SitemapProvider>> {
291 vec![]
292 }
293
294 fn required_storage_paths(&self) -> Vec<&'static str> {
295 vec![]
296 }
297
298 fn dependencies(&self) -> Vec<&'static str> {
299 vec![]
300 }
301
302 fn is_required(&self) -> bool {
303 false
304 }
305
306 fn migrations(&self) -> Vec<Migration> {
307 vec![]
308 }
309
310 fn roles(&self) -> Vec<ExtensionRole> {
311 vec![]
312 }
313
314 fn priority(&self) -> u32 {
315 100
316 }
317
318 fn id(&self) -> &'static str {
319 self.metadata().id
320 }
321
322 fn name(&self) -> &'static str {
323 self.metadata().name
324 }
325
326 fn version(&self) -> &'static str {
327 self.metadata().version
328 }
329
330 fn has_schemas(&self) -> bool {
331 !self.schemas().is_empty()
332 }
333
334 #[cfg(feature = "web")]
335 fn has_router(&self, ctx: &dyn ExtensionContext) -> bool {
336 self.router(ctx).is_some()
337 }
338
339 #[cfg(not(feature = "web"))]
340 fn has_router(&self, _ctx: &dyn ExtensionContext) -> bool {
341 false
342 }
343
344 fn has_jobs(&self) -> bool {
345 !self.jobs().is_empty()
346 }
347
348 fn has_config(&self) -> bool {
349 self.config_prefix().is_some()
350 }
351
352 fn has_llm_providers(&self) -> bool {
353 !self.llm_providers().is_empty()
354 }
355
356 fn has_tool_providers(&self) -> bool {
357 !self.tool_providers().is_empty()
358 }
359
360 fn has_template_providers(&self) -> bool {
361 !self.template_providers().is_empty()
362 }
363
364 fn has_component_renderers(&self) -> bool {
365 !self.component_renderers().is_empty()
366 }
367
368 fn has_template_data_extenders(&self) -> bool {
369 !self.template_data_extenders().is_empty()
370 }
371
372 fn has_page_data_providers(&self) -> bool {
373 !self.page_data_providers().is_empty()
374 }
375
376 fn has_page_prerenderers(&self) -> bool {
377 !self.page_prerenderers().is_empty()
378 }
379
380 fn has_frontmatter_processors(&self) -> bool {
381 !self.frontmatter_processors().is_empty()
382 }
383
384 fn has_content_data_providers(&self) -> bool {
385 !self.content_data_providers().is_empty()
386 }
387
388 fn has_rss_feed_providers(&self) -> bool {
389 !self.rss_feed_providers().is_empty()
390 }
391
392 fn has_sitemap_providers(&self) -> bool {
393 !self.sitemap_providers().is_empty()
394 }
395
396 fn has_site_auth(&self) -> bool {
397 self.site_auth().is_some()
398 }
399
400 fn has_storage_paths(&self) -> bool {
401 !self.required_storage_paths().is_empty()
402 }
403
404 fn has_roles(&self) -> bool {
405 !self.roles().is_empty()
406 }
407
408 fn has_migrations(&self) -> bool {
409 !self.migrations().is_empty()
410 }
411
412 fn declares_assets(&self) -> bool {
413 false
414 }
415
416 fn required_assets(&self, _paths: &dyn AssetPaths) -> Vec<AssetDefinition> {
417 vec![]
418 }
419}
420
421#[macro_export]
422macro_rules! register_extension {
423 ($ext_type:ty) => {
424 ::inventory::submit! {
425 $crate::ExtensionRegistration {
426 factory: || ::std::sync::Arc::new(<$ext_type>::default()) as ::std::sync::Arc<dyn $crate::Extension>,
427 }
428 }
429 };
430 ($ext_expr:expr) => {
431 ::inventory::submit! {
432 $crate::ExtensionRegistration {
433 factory: || ::std::sync::Arc::new($ext_expr) as ::std::sync::Arc<dyn $crate::Extension>,
434 }
435 }
436 };
437}
438
439pub mod prelude {
440 pub use crate::asset::{AssetDefinition, AssetDefinitionBuilder, AssetPaths, AssetType};
441 pub use crate::context::{DynExtensionContext, ExtensionContext};
442 pub use crate::error::{ConfigError, LoaderError};
443 pub use crate::registry::ExtensionRegistry;
444 pub use crate::{
445 register_extension, Extension, ExtensionMetadata, ExtensionRole, Migration,
446 SchemaDefinition, SchemaSource, SiteAuthConfig,
447 };
448
449 #[cfg(feature = "web")]
450 pub use crate::ExtensionRouter;
451
452 pub use crate::any::AnyExtension;
453 pub use crate::builder::ExtensionBuilder;
454 pub use crate::capabilities::{
455 CapabilityContext, FullContext, HasConfig, HasDatabase, HasEventBus, HasExtension,
456 };
457
458 #[cfg(feature = "web")]
459 pub use crate::capabilities::HasHttpClient;
460 pub use crate::hlist::{Contains, NotSame, Subset, TypeList};
461 pub use crate::typed::{
462 ApiExtensionTyped, ConfigExtensionTyped, JobExtensionTyped, ProviderExtensionTyped,
463 SchemaDefinitionTyped, SchemaExtensionTyped, SchemaSourceTyped,
464 };
465
466 #[cfg(feature = "web")]
467 pub use crate::typed::ApiExtensionTypedDyn;
468 pub use crate::typed_registry::{TypedExtensionRegistry, RESERVED_PATHS};
469 pub use crate::types::{
470 Dependencies, DependencyList, ExtensionMeta, ExtensionType, MissingDependency,
471 NoDependencies,
472 };
473
474 pub use systemprompt_provider_contracts::{
475 ComponentContext, ComponentRenderer, ContentDataContext, ContentDataProvider,
476 FrontmatterContext, FrontmatterProcessor, PageContext, PageDataProvider,
477 PagePrepareContext, PagePrerenderer, PageRenderSpec, PlaceholderMapping, RenderedComponent,
478 RssFeedContext, RssFeedItem, RssFeedMetadata, RssFeedProvider, RssFeedSpec, SitemapContext,
479 SitemapProvider, SitemapSourceSpec, SitemapUrlEntry, TemplateDataExtender,
480 TemplateDefinition, TemplateProvider, TemplateSource,
481 };
482}
483
484#[cfg(feature = "web")]
485pub use any::ApiExtensionWrapper;
486pub use any::{AnyExtension, ExtensionWrapper, SchemaExtensionWrapper};
487pub use builder::ExtensionBuilder;
488#[cfg(feature = "web")]
489pub use capabilities::HasHttpClient;
490pub use capabilities::{
491 CapabilityContext, FullContext, HasConfig, HasDatabase, HasEventBus, HasExtension,
492};
493pub use hlist::{Contains, NotSame, Subset, TypeList};
494#[cfg(feature = "web")]
495pub use typed::ApiExtensionTypedDyn;
496pub use typed::{
497 ApiExtensionTyped, ConfigExtensionTyped, JobExtensionTyped, ProviderExtensionTyped,
498 SchemaDefinitionTyped, SchemaExtensionTyped, SchemaSourceTyped,
499};
500pub use typed_registry::{TypedExtensionRegistry, RESERVED_PATHS};
501pub use types::{
502 Dependencies, DependencyList, ExtensionMeta, ExtensionType, MissingDependency, NoDependencies,
503};