Creating a Ferron 3 module
Ferron modules are Rust library crates that implement ModuleLoader. You add them to a custom binary that depends on ferron-entrypoint. Ferron then calls your loader during start-up to register stages, providers, validators, and directives.
1. Create the crate#
cargo new ferron-http-example --lib
cd ferron-http-exampleCargo.toml:
[package]
name = "ferron-http-example"
version = "0.1.0"
edition = "2021"
[dependencies]
async-trait = "0.1"
ferron-core = { git = "https://github.com/ferronweb/ferron", branch = "3.x" }
ferron-http = { git = "https://github.com/ferronweb/ferron", branch = "3.x" }For local development inside a clone of ferron, add a patch that redirects the git source to the local checkout so cargo build works offline. See the example modules repository for a complete workspace example: https://github.com/ferronweb/ferron3-example-modules.
2. Implement ModuleLoader#
use std::sync::Arc;
use async_trait::async_trait;
use ferron_core::loader::ModuleLoader;
use ferron_core::pipeline::{Stage, PipelineError};
use ferron_core::registry::{RegistryBuilder, StageConstraint};
use ferron_http::HttpContext;
struct ExampleStage;
#[async_trait(?Send)]
impl Stage<HttpContext> for ExampleStage {
fn name(&self) -> &str { "example" }
fn constraints(&self) -> Vec<StageConstraint> {
vec![StageConstraint::Before("reverse_proxy".into())]
}
async fn run(&self, ctx: &mut HttpContext) -> Result<bool, PipelineError> {
// read ctx.configuration, inspect ctx.req, set ctx.res
Ok(true)
}
}
#[derive(Default)]
pub struct ExampleModuleLoader;
impl ModuleLoader for ExampleModuleLoader {
fn register_stages(&mut self, registry: RegistryBuilder) -> RegistryBuilder {
registry.with_stage::<HttpContext, _>(|| Arc::new(ExampleStage))
}
}Registration order#
Ferron calls ModuleLoader methods in this order:
register_configuration_adaptersregister_per_protocol_configuration_blocksregister_global_configuration_validatorsregister_per_protocol_configuration_validatorsregister_scoped_configuration_validatorsregister_stagesregister_providersregister_directivesregister_modules(createsModuleinstances, may read finalized config)
All methods have default no-op impls. Override only what you need.
3. Add directives and validators#
Register editor metadata and validation:
use ferron_core::directives::{Directive, DirectiveRegistry, DirectiveSubblock};
use ferron_core::config::validator::{ConfigurationValidator, ConfigurationValidationError};
impl ModuleLoader for ExampleModuleLoader {
fn register_directives(&mut self, registry: &mut DirectiveRegistry) {
registry.register(
Directive {
name: "example_directive",
usage: "example_directive <value>",
description: "Example directive that does something.",
applicable_protocols: Some(&["http"]),
global_only: false,
subblock_link: None,
},
DirectiveSubblock::default(),
);
}
fn register_global_configuration_validators(
&mut self,
registry: &mut Vec<Box<dyn ConfigurationValidator>>,
) {
registry.push(Box::new(ExampleValidator));
}
}
struct ExampleValidator;
impl ConfigurationValidator for ExampleValidator {
fn validate_block(
&self,
config: &ferron_core::config::ServerConfigurationBlock,
ctx: &mut ferron_core::config::validator::ConfigurationValidatorContext,
) -> Result<(), ConfigurationValidationError> {
ferron_core::validate_directive!(
config, ctx.used_directives, example_directive,
optional args(1) => [ferron_core::config::ServerConfigurationValue::String(_, _)],
{}
);
Ok(())
}
}Mark directives as used so UnknownDirective diagnostics are accurate. Use validate_directive! and validate_nested! helpers from ferron-core.
4. Wire the module into a binary#
Create a binary crate that depends on ferron-entrypoint and your module:
[dependencies]
ferron-entrypoint = { git = "https://github.com/ferronweb/ferron", branch = "3.x", features = ["profile-default"] }
ferron-http-example = { path = "../ferron-http-example" }src/main.rs:
fn main() {
ferron_entrypoint::init();
let mut profile = ferron_entrypoint::default_profile();
profile.push(Box::new(ferron_http_example::ExampleModuleLoader::default()));
ferron_entrypoint::main(profile);
}The binary must recompile when the module list changes. Modules are statically linked. Use cargo build -r -p your-binary and run with ./target/release/your-binary run -c ferron.conf.
5. Test the module#
- Unit tests: test
Stage::runwith a syntheticHttpContext(seeferron-http-header-appendtests in the example repo). ferron validate -c ferron.conf: checks validation without starting.ferron directives | jq .: confirms directives appear.- E2E: build an image with
e2e/Dockerfile.testand runtestcontainerstests that issue real HTTP requests.
Choosing the right extension point#
| Need | Use |
|---|---|
| Per-request logic | Stage<C> |
| Long-lived listener or background task | Module + Runtime |
| Pluggable backend (TLS, DNS, observability, log format) | Provider<C> |
| New config source | ConfigurationAdapter |
| Editor support | Directive + DirectiveRegistry |
| Config errors / warnings | ConfigurationValidator |
Next steps#
- Read Module API for trait details and helpers.
- Check Examples for runnable crates to copy.
- See Naming conventions for directive and crate names.