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