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