Skip to main content

systemprompt_extension/
lib.rs

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};