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, Job, LlmProvider, PageDataProvider, TemplateDataExtender, TemplateProvider,
21    ToolProvider,
22};
23
24pub use asset::{AssetDefinition, AssetDefinitionBuilder, AssetType};
25pub use context::{DynExtensionContext, ExtensionContext};
26pub use error::{ConfigError, LoaderError};
27pub use registry::{ExtensionRegistration, ExtensionRegistry};
28
29#[derive(Debug, Clone)]
30pub struct Migration {
31    pub version: u32,
32    pub name: String,
33    pub sql: &'static str,
34}
35
36impl Migration {
37    #[must_use]
38    pub fn new(version: u32, name: impl Into<String>, sql: &'static str) -> Self {
39        Self {
40            version,
41            name: name.into(),
42            sql,
43        }
44    }
45
46    #[must_use]
47    pub fn checksum(&self) -> String {
48        use std::hash::{Hash, Hasher};
49        let mut hasher = std::collections::hash_map::DefaultHasher::new();
50        self.sql.hash(&mut hasher);
51        format!("{:x}", hasher.finish())
52    }
53}
54
55#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
56pub struct ExtensionMetadata {
57    pub id: &'static str,
58    pub name: &'static str,
59    pub version: &'static str,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct SchemaDefinition {
64    pub table: String,
65    pub sql: SchemaSource,
66    pub required_columns: Vec<String>,
67}
68
69impl SchemaDefinition {
70    #[must_use]
71    pub fn inline(table: impl Into<String>, sql: impl Into<String>) -> Self {
72        Self {
73            table: table.into(),
74            sql: SchemaSource::Inline(sql.into()),
75            required_columns: Vec::new(),
76        }
77    }
78
79    #[must_use]
80    pub fn file(table: impl Into<String>, path: impl Into<PathBuf>) -> Self {
81        Self {
82            table: table.into(),
83            sql: SchemaSource::File(path.into()),
84            required_columns: Vec::new(),
85        }
86    }
87
88    #[must_use]
89    pub fn with_required_columns(mut self, columns: Vec<String>) -> Self {
90        self.required_columns = columns;
91        self
92    }
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub enum SchemaSource {
97    Inline(String),
98    File(PathBuf),
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub enum SeedSource {
103    Inline(String),
104    File(PathBuf),
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct ExtensionRole {
109    pub name: String,
110    pub display_name: String,
111    pub description: String,
112    #[serde(default)]
113    pub permissions: Vec<String>,
114}
115
116impl ExtensionRole {
117    #[must_use]
118    pub fn new(
119        name: impl Into<String>,
120        display_name: impl Into<String>,
121        description: impl Into<String>,
122    ) -> Self {
123        Self {
124            name: name.into(),
125            display_name: display_name.into(),
126            description: description.into(),
127            permissions: Vec::new(),
128        }
129    }
130
131    #[must_use]
132    pub fn with_permissions(mut self, permissions: Vec<String>) -> Self {
133        self.permissions = permissions;
134        self
135    }
136}
137
138#[derive(Debug, Clone, Copy)]
139pub struct ExtensionRouterConfig {
140    pub base_path: &'static str,
141    pub requires_auth: bool,
142}
143
144impl ExtensionRouterConfig {
145    #[must_use]
146    pub const fn new(base_path: &'static str) -> Self {
147        Self {
148            base_path,
149            requires_auth: true,
150        }
151    }
152
153    #[must_use]
154    pub const fn public(base_path: &'static str) -> Self {
155        Self {
156            base_path,
157            requires_auth: false,
158        }
159    }
160}
161
162#[cfg(feature = "web")]
163#[derive(Debug, Clone)]
164pub struct ExtensionRouter {
165    pub router: axum::Router,
166    pub base_path: &'static str,
167    pub requires_auth: bool,
168}
169
170#[cfg(feature = "web")]
171impl ExtensionRouter {
172    #[must_use]
173    pub const fn new(router: axum::Router, base_path: &'static str) -> Self {
174        Self {
175            router,
176            base_path,
177            requires_auth: true,
178        }
179    }
180
181    #[must_use]
182    pub const fn public(router: axum::Router, base_path: &'static str) -> Self {
183        Self {
184            router,
185            base_path,
186            requires_auth: false,
187        }
188    }
189
190    #[must_use]
191    pub const fn config(&self) -> ExtensionRouterConfig {
192        ExtensionRouterConfig {
193            base_path: self.base_path,
194            requires_auth: self.requires_auth,
195        }
196    }
197}
198
199pub trait Extension: Send + Sync + 'static {
200    fn metadata(&self) -> ExtensionMetadata;
201
202    fn schemas(&self) -> Vec<SchemaDefinition> {
203        vec![]
204    }
205
206    fn migration_weight(&self) -> u32 {
207        100
208    }
209
210    #[cfg(feature = "web")]
211    fn router(&self, ctx: &dyn ExtensionContext) -> Option<ExtensionRouter> {
212        let _ = ctx;
213        None
214    }
215
216    fn router_config(&self) -> Option<ExtensionRouterConfig> {
217        None
218    }
219
220    fn jobs(&self) -> Vec<Arc<dyn Job>> {
221        vec![]
222    }
223
224    fn config_prefix(&self) -> Option<&str> {
225        None
226    }
227
228    fn config_schema(&self) -> Option<JsonValue> {
229        None
230    }
231
232    fn validate_config(&self, config: &JsonValue) -> Result<(), ConfigError> {
233        let _ = config;
234        Ok(())
235    }
236
237    fn llm_providers(&self) -> Vec<Arc<dyn LlmProvider>> {
238        vec![]
239    }
240
241    fn tool_providers(&self) -> Vec<Arc<dyn ToolProvider>> {
242        vec![]
243    }
244
245    fn template_providers(&self) -> Vec<Arc<dyn TemplateProvider>> {
246        vec![]
247    }
248
249    fn component_renderers(&self) -> Vec<Arc<dyn ComponentRenderer>> {
250        vec![]
251    }
252
253    fn template_data_extenders(&self) -> Vec<Arc<dyn TemplateDataExtender>> {
254        vec![]
255    }
256
257    fn page_data_providers(&self) -> Vec<Arc<dyn PageDataProvider>> {
258        vec![]
259    }
260
261    fn required_storage_paths(&self) -> Vec<&'static str> {
262        vec![]
263    }
264
265    fn dependencies(&self) -> Vec<&'static str> {
266        vec![]
267    }
268
269    fn is_required(&self) -> bool {
270        false
271    }
272
273    fn migrations(&self) -> Vec<Migration> {
274        vec![]
275    }
276
277    fn roles(&self) -> Vec<ExtensionRole> {
278        vec![]
279    }
280
281    fn priority(&self) -> u32 {
282        100
283    }
284
285    fn id(&self) -> &'static str {
286        self.metadata().id
287    }
288
289    fn name(&self) -> &'static str {
290        self.metadata().name
291    }
292
293    fn version(&self) -> &'static str {
294        self.metadata().version
295    }
296
297    fn has_schemas(&self) -> bool {
298        !self.schemas().is_empty()
299    }
300
301    #[cfg(feature = "web")]
302    fn has_router(&self, ctx: &dyn ExtensionContext) -> bool {
303        self.router(ctx).is_some()
304    }
305
306    #[cfg(not(feature = "web"))]
307    fn has_router(&self, _ctx: &dyn ExtensionContext) -> bool {
308        false
309    }
310
311    fn has_jobs(&self) -> bool {
312        !self.jobs().is_empty()
313    }
314
315    fn has_config(&self) -> bool {
316        self.config_prefix().is_some()
317    }
318
319    fn has_llm_providers(&self) -> bool {
320        !self.llm_providers().is_empty()
321    }
322
323    fn has_tool_providers(&self) -> bool {
324        !self.tool_providers().is_empty()
325    }
326
327    fn has_template_providers(&self) -> bool {
328        !self.template_providers().is_empty()
329    }
330
331    fn has_component_renderers(&self) -> bool {
332        !self.component_renderers().is_empty()
333    }
334
335    fn has_template_data_extenders(&self) -> bool {
336        !self.template_data_extenders().is_empty()
337    }
338
339    fn has_page_data_providers(&self) -> bool {
340        !self.page_data_providers().is_empty()
341    }
342
343    fn has_storage_paths(&self) -> bool {
344        !self.required_storage_paths().is_empty()
345    }
346
347    fn has_roles(&self) -> bool {
348        !self.roles().is_empty()
349    }
350
351    fn has_migrations(&self) -> bool {
352        !self.migrations().is_empty()
353    }
354
355    fn required_assets(&self) -> Vec<AssetDefinition> {
356        vec![]
357    }
358
359    fn has_assets(&self) -> bool {
360        !self.required_assets().is_empty()
361    }
362}
363
364#[macro_export]
365macro_rules! register_extension {
366    ($ext_type:ty) => {
367        ::inventory::submit! {
368            $crate::ExtensionRegistration {
369                factory: || ::std::sync::Arc::new(<$ext_type>::default()) as ::std::sync::Arc<dyn $crate::Extension>,
370            }
371        }
372    };
373    ($ext_expr:expr) => {
374        ::inventory::submit! {
375            $crate::ExtensionRegistration {
376                factory: || ::std::sync::Arc::new($ext_expr) as ::std::sync::Arc<dyn $crate::Extension>,
377            }
378        }
379    };
380}
381
382pub mod prelude {
383    pub use crate::asset::{AssetDefinition, AssetDefinitionBuilder, AssetType};
384    pub use crate::context::{DynExtensionContext, ExtensionContext};
385    pub use crate::error::{ConfigError, LoaderError};
386    pub use crate::registry::ExtensionRegistry;
387    pub use crate::{
388        register_extension, Extension, ExtensionMetadata, ExtensionRole, Migration,
389        SchemaDefinition, SchemaSource,
390    };
391
392    #[cfg(feature = "web")]
393    pub use crate::ExtensionRouter;
394
395    pub use crate::any::AnyExtension;
396    pub use crate::builder::ExtensionBuilder;
397    pub use crate::capabilities::{
398        CapabilityContext, FullContext, HasConfig, HasDatabase, HasEventBus, HasExtension,
399    };
400
401    #[cfg(feature = "web")]
402    pub use crate::capabilities::HasHttpClient;
403    pub use crate::hlist::{Contains, NotSame, Subset, TypeList};
404    pub use crate::typed::{
405        ApiExtensionTyped, ConfigExtensionTyped, JobExtensionTyped, ProviderExtensionTyped,
406        SchemaDefinitionTyped, SchemaExtensionTyped, SchemaSourceTyped,
407    };
408
409    #[cfg(feature = "web")]
410    pub use crate::typed::ApiExtensionTypedDyn;
411    pub use crate::typed_registry::{TypedExtensionRegistry, RESERVED_PATHS};
412    pub use crate::types::{
413        Dependencies, DependencyList, ExtensionMeta, ExtensionType, MissingDependency,
414        NoDependencies,
415    };
416
417    pub use systemprompt_provider_contracts::{
418        ComponentContext, ComponentRenderer, PageContext, PageDataProvider, RenderedComponent,
419        TemplateDataExtender, TemplateDefinition, TemplateProvider, TemplateSource,
420    };
421}
422
423#[cfg(feature = "web")]
424pub use any::ApiExtensionWrapper;
425pub use any::{AnyExtension, ExtensionWrapper, SchemaExtensionWrapper};
426pub use builder::ExtensionBuilder;
427#[cfg(feature = "web")]
428pub use capabilities::HasHttpClient;
429pub use capabilities::{
430    CapabilityContext, FullContext, HasConfig, HasDatabase, HasEventBus, HasExtension,
431};
432pub use hlist::{Contains, NotSame, Subset, TypeList};
433#[cfg(feature = "web")]
434pub use typed::ApiExtensionTypedDyn;
435pub use typed::{
436    ApiExtensionTyped, ConfigExtensionTyped, JobExtensionTyped, ProviderExtensionTyped,
437    SchemaDefinitionTyped, SchemaExtensionTyped, SchemaSourceTyped,
438};
439pub use typed_registry::{TypedExtensionRegistry, RESERVED_PATHS};
440pub use types::{
441    Dependencies, DependencyList, ExtensionMeta, ExtensionType, MissingDependency, NoDependencies,
442};