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