use camel_api::{Body, CamelError, Exchange};
use camel_bean::BeanProcessor;
use camel_core::route::{BuilderStep, RouteDefinition};
use super::document::{BeanDeclDoc, BeanKindDoc};
#[derive(Debug)]
pub(crate) enum StubKind {
Echo,
SetBody { body: String },
Fail { message: String },
}
pub(crate) struct StubBean {
name: String,
methods: Vec<String>,
kind: StubKind,
}
#[async_trait::async_trait]
impl BeanProcessor for StubBean {
async fn call(&self, _method: &str, exchange: &mut Exchange) -> Result<(), CamelError> {
match &self.kind {
StubKind::Echo => Ok(()),
StubKind::SetBody { body } => {
exchange.input.body = Body::Text(body.clone());
Ok(())
}
StubKind::Fail { message } => Err(CamelError::ProcessorError(message.clone())),
}
}
fn methods(&self) -> Vec<String> {
self.methods.clone()
}
}
impl std::fmt::Debug for StubBean {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StubBean")
.field("name", &self.name)
.field("methods", &self.methods)
.field("kind", &self.kind)
.finish()
}
}
pub(crate) fn stub_from_decl(name: &str, decl: &BeanDeclDoc, invoked: &[String]) -> StubBean {
let kind = match decl.kind {
BeanKindDoc::Echo => StubKind::Echo,
BeanKindDoc::SetBody => StubKind::SetBody {
body: decl
.config
.as_ref()
.and_then(|config| config.get("body").cloned())
.expect("parse_test_document validated setBody config.body"), },
BeanKindDoc::Fail => StubKind::Fail {
message: decl
.config
.as_ref()
.and_then(|config| config.get("message").cloned())
.unwrap_or_else(|| format!("fail bean {name}")),
},
};
let methods = match decl.methods.as_ref() {
Some(declared) => declared.clone(),
None => {
let mut seen: Vec<String> = Vec::new();
for method in invoked {
if !seen.contains(method) {
seen.push(method.clone());
}
}
seen
}
};
StubBean {
name: name.to_string(),
methods,
kind,
}
}
pub(crate) fn collect_bean_calls(defs: &[RouteDefinition]) -> Vec<(String, String)> {
let mut calls = Vec::new();
for def in defs {
collect_from_steps(def.steps(), &mut calls);
collect_from_steps(def.circuit_breaker_fallback(), &mut calls);
}
calls
}
fn collect_from_steps(steps: &[BuilderStep], calls: &mut Vec<(String, String)>) {
for step in steps {
match step {
BuilderStep::Bean { name, method } => calls.push((name.clone(), method.clone())),
BuilderStep::DeclarativeFilter { steps, .. }
| BuilderStep::DeclarativeSplit { steps, .. }
| BuilderStep::DeclarativeStreamSplit { steps, .. }
| BuilderStep::Split { steps, .. }
| BuilderStep::Filter { steps, .. }
| BuilderStep::Multicast { steps, .. }
| BuilderStep::Throttle { steps, .. }
| BuilderStep::LoadBalance { steps, .. }
| BuilderStep::Loop { steps, .. }
| BuilderStep::DeclarativeLoop { steps, .. }
| BuilderStep::IdempotentConsumer { steps, .. } => collect_from_steps(steps, calls),
BuilderStep::DeclarativeChoice { whens, otherwise } => {
for when in whens {
collect_from_steps(&when.steps, calls);
}
if let Some(steps) = otherwise {
collect_from_steps(steps, calls);
}
}
BuilderStep::Choice { whens, otherwise } => {
for when in whens {
collect_from_steps(&when.steps, calls);
}
if let Some(steps) = otherwise {
collect_from_steps(steps, calls);
}
}
BuilderStep::Cache { on_miss, .. } => collect_from_steps(on_miss, calls),
BuilderStep::DeclarativeDoTry {
try_steps,
catch,
finally,
} => {
collect_from_steps(try_steps, calls);
for clause in catch {
collect_from_steps(&clause.steps, calls);
}
if let Some(finally) = finally {
collect_from_steps(&finally.steps, calls);
}
}
BuilderStep::Processor(_)
| BuilderStep::To(_)
| BuilderStep::Stop
| BuilderStep::Log { .. }
| BuilderStep::DeclarativeSetHeader { .. }
| BuilderStep::DeclarativeSetHeaderIfAbsent { .. }
| BuilderStep::DeclarativeRemoveHeader { .. }
| BuilderStep::DeclarativeSetProperty { .. }
| BuilderStep::DeclarativeSetBody { .. }
| BuilderStep::DeclarativeScript { .. }
| BuilderStep::DeclarativeFunction { .. }
| BuilderStep::DeclarativeDynamicRouter { .. }
| BuilderStep::DeclarativeRoutingSlip { .. }
| BuilderStep::Aggregate { .. }
| BuilderStep::WireTap { .. }
| BuilderStep::DeclarativeLog { .. }
| BuilderStep::Script { .. }
| BuilderStep::DynamicRouter { .. }
| BuilderStep::RoutingSlip { .. }
| BuilderStep::RecipientList { .. }
| BuilderStep::DeclarativeRecipientList { .. }
| BuilderStep::Delay { .. }
| BuilderStep::Enrich { .. }
| BuilderStep::PollEnrich { .. }
| BuilderStep::Validate { .. }
| BuilderStep::ClaimCheck { .. }
| BuilderStep::Sampling { .. }
| BuilderStep::Sort { .. }
| BuilderStep::CacheInvalidate { .. }
| BuilderStep::CacheClear { .. }
| BuilderStep::CacheStats { .. }
| BuilderStep::CachePeekStale { .. }
| BuilderStep::Resequence { .. } => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use camel_api::Message;
use camel_api::declarative::LanguageExpressionDef;
fn echo_decl(methods: Option<Vec<String>>) -> BeanDeclDoc {
BeanDeclDoc {
kind: BeanKindDoc::Echo,
methods,
config: None,
}
}
#[test]
fn stub_from_decl_explicit_methods_uses_declaration() {
let decl = echo_decl(Some(vec!["a".to_string(), "b".to_string()]));
let invoked = vec!["x".to_string(), "y".to_string()];
let bean = stub_from_decl("enricher", &decl, &invoked);
assert_eq!(bean.methods(), vec!["a".to_string(), "b".to_string()]);
}
#[test]
fn stub_from_decl_wildcard_dedupes_invoked() {
let decl = echo_decl(None);
let invoked = vec!["m1".to_string(), "m2".to_string(), "m1".to_string()];
let bean = stub_from_decl("gate", &decl, &invoked);
assert_eq!(bean.methods(), vec!["m1".to_string(), "m2".to_string()]);
}
#[test]
fn collect_bean_calls_walks_fallback_and_cache_on_miss() {
let def = RouteDefinition::new(
"direct:start",
vec![
BuilderStep::Bean {
name: "x".to_string(),
method: "m".to_string(),
},
BuilderStep::Cache {
repository: None,
key: LanguageExpressionDef {
language: "simple".to_string(),
source: "${body}".to_string(),
},
ttl: None,
max_entry_bytes: None,
coalesce_misses: false,
on_miss: vec![BuilderStep::Bean {
name: "y".to_string(),
method: "n".to_string(),
}],
},
],
)
.with_circuit_breaker_fallback(vec![BuilderStep::Bean {
name: "z".to_string(),
method: "o".to_string(),
}]);
let calls = collect_bean_calls(&[def]);
assert_eq!(
calls,
vec![
("x".to_string(), "m".to_string()),
("y".to_string(), "n".to_string()),
("z".to_string(), "o".to_string()),
]
);
}
#[tokio::test]
async fn fail_default_message_exact() {
let decl = BeanDeclDoc {
kind: BeanKindDoc::Fail,
methods: None,
config: None,
};
let bean = stub_from_decl("gate", &decl, &[]);
let mut exchange = Exchange::new(Message::new(Body::Text("orig".to_string())));
let err = bean
.call("check", &mut exchange)
.await
.expect_err("fail stub must error"); match err {
CamelError::ProcessorError(message) => {
assert_eq!(message, "fail bean gate");
}
other => panic!("expected ProcessorError, got {other}"),
}
}
}