Introduction
Modules are dynamically loaded and configured, often utilizing an Adapter pattern to allow for swappable backend implementations (e.g., swapping an in-memory event bus for a Redis-backed one). The engine provides a trait-based system where modules implement theCoreModule trait for lifecycle management and the ConfigurableModule trait for handling configuration and adapter injection.
Module Architecture
The module system is built around two primary traits:CoreModule and ConfigurableModule.
Core Traits
| Trait | Description | Key Methods |
|---|---|---|
| CoreModule | The base trait for all modules. Handles lifecycle, identification, and function registration. | name(), create(), initialize(), register_functions(), start_background_tasks(), destroy() |
| ConfigurableModule | Extends CoreModule to support typed configuration and pluggable adapters. | build(), registry(), adapter_class_from_config() |
Lifecycle Flow
The following diagram illustrates the lifecycle of a module from creation to initialization.Implementing a Configurable Module
Developing a custom module typically involves defining an adapter interface, implementing specific adapters, and then wrapping them in a module structure.Step 1: Define the Adapter Trait
Define anasync_trait that specifies the behavior your module’s backend must implement. This allows users to switch implementations via configuration.
Step 2: Implement Adapter Registration
To make adapters discoverable by the configuration system, you must define a registration struct and use theinventory crate.
Step 3: Create Adapter Factories
Define factory functions that instantiate your specific adapter implementations (e.g.,InMemory or Logging).
Step 4: Implement Adapter Logic
Create the actual adapter implementations.In-Memory Adapter
In-Memory Adapter
Simple in-memory implementation for development and testing.
Logging Wrapper Adapter
Logging Wrapper Adapter
Wrapper adapter that logs all events while delegating to another adapter.
Step 5: Implement the Module Logic
The module struct holds theEngine reference and the injected Adapter.
Registering Functions
Modules expose functionality to the engine (and thus to workers) by registering functions. This is typically done in theinitialize method or register_functions.
Registration Request Structure
When registering a function, you must provide aRegisterFunctionRequest.
| Field | Type | Description |
|---|---|---|
function_id | String | Unique function ID (e.g., “custom::emit”) |
description | Option<String> | Human-readable description of the function |
request_format | Option<Value> | JSON Schema defining the expected input |
response_format | Option<Value> | JSON Schema defining the expected output |
When using the
#[service] macro with #[function] attributes, request_format and response_format are auto-generated as standard JSON Schema from your Rust types (via schemars). Your input/output types must derive JsonSchema. Manual schema specification is only needed for custom module registration.Example Registration
Handling Function Invocations
To handle invocations, the module (or a specific handler struct) must implement theFunctionHandler trait.
Registering Triggers
Modules can also act as sources of events by registeringTriggerTypes. This allows the engine to route external events (like Cron ticks or HTTP requests) to specific functions.
Trigger Architecture
Implementation
To support triggers, a module implementsTriggerRegistrator.
Configuration
Modules are configured viaiii-config.yaml or JSON passed during initialization. The ConfigurableModule trait maps this configuration to a Rust struct.
Configuration Struct
Usage in Config File
Complete Example
Here’s a complete custom module implementation:examples/custom_event_module.rs
iii-config.yaml
Best Practices
Use Adapter Pattern
Use Adapter Pattern
Always use adapters for external integrations to allow swapping implementations.
Implement Graceful Shutdown
Implement Graceful Shutdown
Handle cleanup in the module’s drop implementation or provide shutdown hooks.
Provide JSON Schemas
Provide JSON Schemas
Always define request and response formats for functions to enable validation and documentation.
Use Structured Logging
Use Structured Logging
Use the
tracing crate for structured logging with context.Next Steps
Core Modules
Explore built-in core modules
Adapters
Learn more about the adapter pattern