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