Custom component
A custom component connects rust-camel to a system the built-in components do not cover. You implement the Component trait, wrap it in a ComponentBundle, and register the bundle against a TOML config key.
Implement the Component and Endpoint
pub struct EchoComponent {
prefix: String,
}
impl EchoComponent {
pub fn new(prefix: impl Into<String>) -> Self {
Self {
prefix: prefix.into(),
}
}
}
impl Component for EchoComponent {
fn scheme(&self) -> &str {
"echo"
}
fn create_endpoint(
&self,
uri: &str,
_ctx: &dyn ComponentContext,
) -> Result<Box<dyn Endpoint>, CamelError> {
Ok(Box::new(EchoEndpoint {
uri: uri.to_string(),
prefix: self.prefix.clone(),
}))
}
}
struct EchoEndpoint {
uri: String,
prefix: String,
}
impl Endpoint for EchoEndpoint {
fn uri(&self) -> &str {
&self.uri
}
fn create_consumer(
&self,
_rt: Arc<dyn camel_component_api::RuntimeObservability>,
) -> Result<Box<dyn Consumer>, CamelError> {
Err(CamelError::RouteError(
"echo component is producer-only".into(),
))
}
fn create_producer(
&self,
_rt: Arc<dyn camel_component_api::RuntimeObservability>,
_ctx: &ProducerContext,
) -> Result<BoxProcessor, CamelError> {
let prefix = self.prefix.clone();
// Log the exchange body with the configured prefix.
Ok(BoxProcessor::from_fn(move |exchange| {
let prefix = prefix.clone();
Box::pin(async move {
let body = exchange
.input
.body
.as_text()
.unwrap_or("<non-text body>")
.to_string();
tracing::info!("{}{}", prefix, body);
Ok(exchange)
})
}))
}
}
The contract layers from factory to worker. A Component is a factory for one URI scheme. create_endpoint builds an Endpoint for a specific URI. The Endpoint creates a Consumer for inbound traffic or a Producer for outbound traffic. A Producer is a Service<Exchange> that does the actual work.
EchoComponent::scheme returns "echo", so the runtime resolves any echo:... URI to this component. create_endpoint stamps the configured prefix onto each EchoEndpoint. This endpoint is producer-only. create_consumer returns an error to signal that inbound traffic is unsupported. create_producer returns a BoxProcessor that logs the exchange body with the prefix. The exchange passes through unchanged.
Wrap the component in a bundle
pub struct EchoBundle {
config: EchoConfig,
}
impl ComponentBundle for EchoBundle {
fn config_key() -> &'static str {
"echo"
}
fn from_toml(raw: toml::Value) -> Result<Self, CamelError> {
let config: EchoConfig = raw
.try_into()
.map_err(|e: toml::de::Error| CamelError::Config(e.to_string()))?;
Ok(Self { config })
}
fn register_all(self, registrar: &mut dyn ComponentRegistrar) {
registrar.register_component_dyn(Arc::new(EchoComponent::new(self.config.prefix)));
}
}
A ComponentBundle owns one TOML config key and registers every scheme the bundle owns. config_key returns "echo", which maps to [components.echo] in Camel.toml. from_toml deserializes the raw TOML block into EchoConfig. register_all receives a ComponentRegistrar and calls register_component_dyn for each component the bundle owns.
Register and use the component
if let Some(raw) = config.components.raw.get(EchoBundle::config_key()).cloned() {
EchoBundle::from_toml(raw)?.register_all(&mut ctx);
} else {
// No config block โ use defaults
EchoBundle {
config: EchoConfig::default(),
}
.register_all(&mut ctx);
}
- route:
id: echo-demo
from: timer:tick?period=2000
steps:
- to: echo:hello
In main, read the config block from CamelConfig and call register_all. Fall back to defaults when the block is absent. The route references echo:hello like any built-in scheme. The timer fires every two seconds, the producer logs the body, and the exchange continues down the pipeline.
Reference: Component SPI ยท Example source