Skip to main content

EngineModule

Trait EngineModule 

Source
pub trait EngineModule: Send + 'static {
    // Required method
    fn name(&self) -> &'static str;

    // Provided methods
    fn register_pids(&self, _router: &mut PidRouter) { ... }
    fn register_pids_with_roles(
        &self,
        router: &mut PidRouter,
        _roles: &DeploymentRoles,
    ) { ... }
    fn workflow_names(&self) -> &'static [&'static str] { ... }
    fn profile_requirements(&self) -> &'static [ProfileRequirement] { ... }
    fn configure(&self) -> Result<(), String> { ... }
}
Expand description

A self-contained domain module that registers with the engine at startup.

Domain crates implement this trait to declare their presence in the engine. The module name is surfaced in EngineContext::registered_modules for diagnostics, health checks, and log output.

§Startup validation

Override configure to perform adapter coverage checks at engine startup time. The engine calls configure for every registered module during EngineBuilder::build and panics with an actionable message if any module returns Err. This surfaces missing adapter registrations as a startup failure rather than a silent runtime error.

§Example

pub struct GpkeModule;

impl EngineModule for GpkeModule {
    fn name(&self) -> &'static str { "gpke" }

    fn configure(&self) -> Result<(), String> {
        // Validate that every known BDEW format version has an adapter:
        GPKE_ADAPTER_REGISTRY
            .validate_policy(&GpkeWorkflow::version_policy(), &KNOWN_FVS)
            .map_err(|uncovered| format!(
                "gpke: missing adapters for format versions: {:?}",
                uncovered
            ))
    }
}

let ctx = EngineBuilder::new()
    .with_event_store(my_store)
    .register(Box::new(GpkeModule))
    .build(); // panics if GpkeModule::configure returns Err

assert_eq!(ctx.registered_modules(), &["gpke"]);

Required Methods§

Source

fn name(&self) -> &'static str

Stable, unique name for this domain module.

Used in diagnostics, health checks, and structured log output. Choose a short lowercase identifier (e.g. "gpke", "wim", "geli").

Provided Methods§

Source

fn register_pids(&self, _router: &mut PidRouter)

Register all PIDs this module handles into the shared PidRouter.

§Mutability contract

This method is called exactly once by EngineBuilder::build, before the resulting EngineContext is handed to the caller. The &mut PidRouter reference is only available here, at build time. After build returns the router is sealed — the engine provides only a shared &PidRouter reference, with no mutation path at runtime.

Consequence: all PIDs a module will ever need must be registered here. Do not attempt to register PIDs lazily from async handlers or after the engine has started — there is no API for that by design.

Two modules claiming one PID for different workflows panics in PidRouter::register_with_module while the engine is being built, so the conflict stops the daemon rather than routing a message to whichever module registered last.

For role-conditional registration (PIDs that should only be active for specific BDEW Marktrollen), override register_pids_with_roles instead.

§Example
fn register_pids(&self, router: &mut PidRouter) {
    // GPKE Lieferantenwechsel / Lieferbeginn (BK6-22-024, PIDs 55001, 55002, 55017)
    for &pid in &[55001_u32, 55002, 55017] {
        router.register(pid, "gpke-supplier-change");
    }
}
Source

fn register_pids_with_roles( &self, router: &mut PidRouter, _roles: &DeploymentRoles, )

Register PIDs with role-context awareness.

This is the preferred override for modules that have role-conditional PID registrations — PIDs that should only be active when this makod instance holds a specific Marktrolle.

The default implementation calls register_pids (role-agnostic) so existing modules that override register_pids continue to work without changes.

Override this method instead of register_pids when any PID registration should be conditional on the deployment role:

use mako_engine::marktrolle::Marktrolle;

fn register_pids_with_roles(&self, router: &mut PidRouter, roles: &DeploymentRoles) {
    // Always register: 55001, 55002 (not role-specific)
    for pid in [55001_u32, 55002] { router.register_with_module(pid, "gpke-supplier-change", self.name()); }

    // Only when NB role: 19001/19002 inbound ORDRSP from MSB
    if roles.contains(Marktrolle::Nb) {
        for pid in [19001_u32, 19002] { router.register_with_module(pid, "gpke-konfiguration", self.name()); }
    }
}
§Conflict guard

Use PidRouter::register_with_module (not register) inside this method. The conflict guard panics at build time if two modules register the same PID to different workflows — this makes role misconfigurations visible at startup rather than silently misrouting messages.

Source

fn workflow_names(&self) -> &'static [&'static str]

Every workflow this module owns, named.

These names are what EngineContext::registered_workflows collects, and consumers build their deadline-dispatch coverage from that list — so this declaration, not register_pids, is what makes a workflow’s Fristen checkable.

§The invariant

Every name register_pids routes to must appear here. EngineBuilder::build panics otherwise, per module. A workflow that is routed but undeclared still runs — it just becomes invisible to every check made over the declarations, so a deadline it registers is never held against a dispatch arm and fires into nothing.

The converse is deliberately allowed: a command-initiated workflow (one an ERP starts over the command API) declares a name and routes no inbound Prüfidentifikator.

Prefer each module’s own WORKFLOW_NAME constant over a string literal. A literal here can disagree with the name register_pids routes to, and the two are only compared at build time:

fn workflow_names(&self) -> &'static [&'static str] {
    &[wechselprozesse::WORKFLOW_NAME, abrechnung::WORKFLOW_NAME]
}

The default implementation returns an empty slice, which is correct only for a module that routes no PIDs at all.

Source

fn profile_requirements(&self) -> &'static [ProfileRequirement]

Declare the EDIFACT profile types this module requires at runtime.

Returning a non-empty slice causes EngineBuilder::build to call the registered profile validator for each requirement. If no active profile exists for a required message type, build panics with an actionable error so deployment fails fast rather than silently.

Domain crates declare their format requirements here rather than calling edi_energy::registry::ReleaseRegistry::global() inside configure(), so edi-energy stays out of their production [dependencies].

fn profile_requirements(&self) -> &'static [ProfileRequirement] {
    &[
        ProfileRequirement { message_type: "UTILMD", label: "UTILMD Strom (GPKE)" },
        ProfileRequirement { message_type: "INVOIC", label: "INVOIC Abrechnung (GPKE)" },
    ]
}
Source

fn configure(&self) -> Result<(), String>

Validate adapter coverage and configuration at engine startup.

Called by EngineBuilder::build after all modules are registered. Return Ok(()) when the module is fully configured. Return Err(msg) with an actionable description when an adapter or configuration is missing — the engine will panic with that message so the deployment fails early rather than silently.

The default implementation is a no-op (always returns Ok(())). Override it in domain crates to call AdapterRegistry::validate_policy and emit structured errors.

Note: if your validation needs access to the edi-energy profile registry, use profile_requirements instead — it does not require importing edi-energy in domain crates.

§Errors

Returns a descriptive error string when the module’s configuration is invalid.

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§