Skip to main content

platform_module/
lib.rs

1//! Module framework contracts: the data/behavior split a module exposes to the
2//! composition root.
3//!
4//! - [`ModuleManifest`]: serializable data (name, story display, reserved
5//!   seams). Produced by every loading source.
6//! - [`ModuleBinding`]: behavior (register functions/event handlers). One impl
7//!   per loading source; [`LinkedBinding`] is the compile-time one.
8//! - [`Module`]: a loaded module bundling manifest + binding + internal config.
9//! - [`AdminDataSource`]: the schema-admin read seam — a module's read access
10//!   to its admin entities. [`AdminSchema`] is the declared admin surface data.
11//! - [`AdminActionSource`]: executable behavior for manifest-declared admin
12//!   actions.
13//! - [`AdminQuerySource`]: read-only behavior for manifest-declared admin
14//!   queries.
15
16mod admin_data;
17mod binding;
18mod host;
19mod linked;
20mod module;
21mod source;
22
23pub use admin_data::{
24    AdminActionSource, AdminDataSource, AdminListQuery, AdminPage, AdminQuerySource,
25};
26pub use binding::{EventHandlerRegistrationContext, EventHandlerRuntimeContext, ModuleBinding};
27pub use host::{HostContribution, HostLinkedModule};
28pub use lenso_contracts::{
29    AdminAction, AdminActionConfirmation, AdminActionDangerLevel, AdminActionInputField,
30    AdminActionInputSchema, AdminDeclarativeComponent, AdminDeclarativePage,
31    AdminDeclarativeSection, AdminDeclarativeSurface, AdminEmbeddedEntry, AdminEmbeddedRuntime,
32    AdminEmbeddedSurface, AdminMetricBinding, AdminPermission, AdminSandboxPolicy, AdminSchema,
33    AdminSurface, CONSOLE_BRIDGE_PROTOCOL, CONSOLE_MODULE_PROTOCOL, CONSOLE_MODULE_PROTOCOL_MAJOR,
34    CONSOLE_UI_ESM_FORMAT, ConsoleActionInputBinding, ConsoleActionInputValue, ConsoleContribution,
35    ConsoleContributionAction, ConsoleContributionKind, ConsoleModuleManifest,
36    ConsoleModuleSurface, ConsoleNavigation, ConsoleNavigationGroup, ConsolePermissionGrant,
37    ConsolePermissionRequest, ConsoleSlot, ConsoleSlotContext, ConsoleSlotContextField,
38    ConsoleSlotContextFieldType, ConsoleSurface, ConsoleSurfaceArea, ConsoleSurfacePresentation,
39    ConsoleUiArtifact, ConsoleUiArtifactEntry, ConsoleUiArtifactFormat,
40    ConsoleUiArtifactStyleAsset, ConsoleWorkspaceRef, CronParseError, CronSchedule, EntitySchema,
41    EventHandlerDeclaration, EventSurface, FieldSchema, FieldType,
42    LifecycleActivationJobDeclaration, LifecycleActivationRunPolicy,
43    LifecycleStartupCheckDeclaration, LifecycleStartupCheckKind, LifecycleSurface,
44    ModuleCapabilityReference, ModuleHttpMethod, ModuleHttpRoute, ModuleManifest,
45    ModuleManifestBuilder, ModuleManifestLint, ModuleManifestLintSeverity, ModuleRequirement,
46    ModuleRouteLint, ModuleRouteLintSeverity, RuntimeFunctionDeclaration,
47    RuntimeRetryPolicyDeclaration, RuntimeSurface, StoryDisplayDescriptor, StoryDisplaySource,
48    lint_module_http_routes, lint_module_manifest, lint_module_manifest_parts,
49    module_capability_references, validate_cron_expression,
50};
51pub use linked::{
52    LinkedBinding, LinkedBindingBuilder, LinkedHttpContribution, LinkedHttpRouteMerger,
53};
54pub use module::{Module, ModuleLoadStatus};
55pub use source::ModuleSource;
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60    use platform_core::{
61        AppConfig, AppContext, AppError, AuthConfig, DatabaseConfig, ErrorCode, HttpConfig,
62        LoggingEventPublisher, Migration, ModuleSourcesConfig, RedisConfig, ServiceConfig,
63        TelemetryConfig,
64    };
65    use std::collections::BTreeMap;
66    use std::sync::Arc;
67
68    #[derive(Debug, Clone, PartialEq, Eq)]
69    struct TestContribution(&'static str);
70
71    const TEST_MIGRATIONS: &[Migration] = &[Migration {
72        name: "test/0001_init",
73        sql: "select 1;",
74    }];
75
76    fn manifest() -> ModuleManifest {
77        ModuleManifest::builder("fixture/test").build()
78    }
79
80    fn module(_ctx: &AppContext) -> Module {
81        Module::linked(manifest(), LinkedBinding::builder().build())
82    }
83
84    fn fallible_module(_ctx: &AppContext) -> platform_core::AppResult<Module> {
85        Err(AppError::new(
86            ErrorCode::ExternalDependency,
87            "object storage configuration is invalid",
88        ))
89    }
90
91    #[test]
92    fn host_linked_module_keeps_typed_contributions() {
93        let linked_module = HostLinkedModule::linked("test", manifest, module, TEST_MIGRATIONS)
94            .with_contribution(TestContribution("wired"));
95
96        let contributions = linked_module
97            .contributions::<TestContribution>()
98            .collect::<Vec<_>>();
99
100        assert_eq!(contributions, vec![&TestContribution("wired")]);
101    }
102
103    #[tokio::test]
104    async fn fallible_host_linked_module_preserves_structured_loader_error() {
105        let linked_module =
106            HostLinkedModule::try_linked("test", manifest, fallible_module, TEST_MIGRATIONS);
107        let db = platform_core::DbPool::connect_lazy("postgres://localhost/lenso_test")
108            .expect("lazy pool should build");
109        let ctx = AppContext::new(
110            AppConfig {
111                service: ServiceConfig::default(),
112                database: DatabaseConfig {
113                    url: "postgres://localhost/lenso_test".to_owned(),
114                    max_connections: 1,
115                },
116                redis: RedisConfig::default(),
117                http: HttpConfig::default(),
118                telemetry: TelemetryConfig::default(),
119                auth: AuthConfig::default(),
120                module_sources: ModuleSourcesConfig {
121                    linked_profile: "core".to_owned(),
122                },
123                modules: BTreeMap::new(),
124            },
125            db,
126            Arc::new(LoggingEventPublisher),
127        );
128
129        let error = linked_module
130            .try_load_module(&ctx)
131            .expect_err("fallible loader error must remain structured");
132
133        assert_eq!(error.code, ErrorCode::ExternalDependency);
134        assert_eq!(
135            error.public_message,
136            "object storage configuration is invalid"
137        );
138    }
139}