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