use camel_api::SpanKindHint;
use std::sync::Arc;
use camel_api::{BoxProcessor, CamelError, Exchange, IdentityProcessor, Value};
use camel_language_api::LanguageError;
use camel_processor::claim_check::ClaimCheckFilter;
use camel_processor::{LogProcessor, log::DynamicLog, script_mutator::ScriptMutator, set_property};
use super::{
CompilationContext, CompileOutcome, CompiledStep, StepCompiler, StepCompilerRegistry,
pack_lifecycles,
};
use crate::lifecycle::adapters::step_resolution::{
FunctionStagingMode, compile_filter_predicate, compile_key_expression,
compile_message_id_expression, compile_sort_expression, compile_value_source, eval_meta,
resolve_language,
};
use crate::lifecycle::application::route_definition::{BuilderStep, ValueSourceDef};
pub(crate) struct CoreCompiler;
impl StepCompiler for CoreCompiler {
fn compile(
&self,
step: BuilderStep,
step_index: usize,
ctx: &CompilationContext,
registry: &StepCompilerRegistry,
) -> Result<CompileOutcome, CamelError> {
match step {
BuilderStep::Processor(op) => Ok(CompileOutcome::Matched(CompiledStep::Process {
kind_hint: SpanKindHint::Internal,
processor: op.0,
body_contract: None,
lifecycle: None,
label: None,
to_uri: None,
})),
BuilderStep::Stop => Ok(CompileOutcome::Matched(CompiledStep::Stop)),
BuilderStep::Resequence { .. } => Err(CamelError::RouteError(
"resequence must be a top-level step".into(),
)),
BuilderStep::Delay { config } => {
let svc = camel_processor::delayer::DelayerService::new(config);
Ok(CompileOutcome::Matched(CompiledStep::Process {
kind_hint: SpanKindHint::Internal,
processor: BoxProcessor::new(svc),
body_contract: None,
lifecycle: None,
label: None,
to_uri: None,
}))
}
BuilderStep::Validate { predicate } => {
let meta = eval_meta(ctx, &predicate.language, "validate", step_index, None);
let predicate_arc = compile_filter_predicate(ctx.languages, &predicate, meta)?;
let expression_source = predicate.source.clone();
let svc = camel_processor::ValidateService::from_predicate(
predicate_arc,
expression_source,
);
Ok(CompileOutcome::Matched(CompiledStep::Process {
kind_hint: SpanKindHint::Internal,
processor: BoxProcessor::new(svc),
body_contract: None,
lifecycle: None, label: None,
to_uri: None,
}))
}
BuilderStep::IdempotentConsumer {
repository,
expression,
steps,
eager,
remove_on_failure,
} => {
use crate::lifecycle::adapters::route_compiler::compose_outcome_segment;
let repo = ctx
.idempotent_repositories
.get(&repository)
.ok_or_else(|| {
CamelError::ComponentNotFound(format!(
"idempotent_consumer: repository '{repository}' is not registered"
))
})?;
let message_id = compile_message_id_expression(
ctx.languages,
&expression,
eval_meta(
ctx,
&expression.language,
"idempotent_consumer",
step_index,
None,
),
)?;
let (child_segments, child_lifecycles) =
ctx.compile_children_segments(steps, registry)?;
let child_pipeline = compose_outcome_segment(child_segments);
let svc = camel_processor::IdempotentConsumerSegment::new(
repo,
message_id,
child_pipeline,
eager,
remove_on_failure,
);
Ok(CompileOutcome::Matched(CompiledStep::Segment {
segment: camel_api::OutcomeSegment::new(Box::new(svc)),
body_contract: None,
lifecycle: pack_lifecycles(child_lifecycles),
label: None,
}))
}
BuilderStep::Cache {
repository,
key,
ttl,
max_entry_bytes,
coalesce_misses,
on_miss,
} => {
use crate::lifecycle::adapters::route_compiler::compose_outcome_segment;
let repo_name = repository.as_deref().unwrap_or("memory");
let repo = ctx.cache_repositories.get(repo_name).ok_or_else(|| {
CamelError::ComponentNotFound(format!(
"cache: repository '{repo_name}' is not registered"
))
})?;
let key_expr = compile_message_id_expression(
ctx.languages,
&key,
eval_meta(ctx, &key.language, "cache", step_index, None),
)?;
let ttl_dur = ttl
.as_ref()
.map(|s| humantime::parse_duration(s))
.transpose()
.map_err(|e| {
CamelError::RouteError(format!(
"cache: invalid ttl '{}': {e}",
ttl.as_deref().unwrap_or("<none>")
))
})?;
let (child_segments, child_lifecycles) =
ctx.compile_children_segments(on_miss, registry)?;
let child_pipeline = compose_outcome_segment(child_segments);
let svc = camel_processor::CacheService::new(
repo,
key_expr,
ttl_dur,
max_entry_bytes.unwrap_or(camel_api::body::DEFAULT_MATERIALIZE_LIMIT),
child_pipeline,
ctx.rt.clone(),
)
.with_coalesce(coalesce_misses);
Ok(CompileOutcome::Matched(CompiledStep::Segment {
segment: camel_api::OutcomeSegment::new(Box::new(svc)),
body_contract: None,
lifecycle: pack_lifecycles(child_lifecycles),
label: None,
}))
}
BuilderStep::CacheInvalidate {
repository,
key,
key_prefix,
} => {
let (expr, as_prefix) = match (&key, &key_prefix) {
(Some(expr), None) => (expr, false),
(None, Some(expr)) => (expr, true),
_ => {
return Err(CamelError::Config(
"cache_invalidate: exactly one of 'key' or 'key_prefix' is required"
.into(),
));
}
};
if expr.source.trim().is_empty() {
return Err(CamelError::Config(
"cache_invalidate: key/key_prefix must not be empty".into(),
));
}
let meta = eval_meta(ctx, &expr.language, "cache_invalidate", step_index, None);
let target =
if as_prefix {
camel_processor::CacheInvalidateTarget::Prefix(
compile_message_id_expression(ctx.languages, expr, meta)?,
)
} else {
camel_processor::CacheInvalidateTarget::Key(compile_message_id_expression(
ctx.languages,
expr,
meta,
)?)
};
let repo_name = repository.as_deref().unwrap_or("memory");
let repo = ctx.cache_repositories.get(repo_name).ok_or_else(|| {
CamelError::ComponentNotFound(format!(
"cache_invalidate: repository '{repo_name}' is not registered"
))
})?;
let svc =
camel_processor::CacheInvalidateService::new(repo, target, ctx.rt.clone());
Ok(CompileOutcome::Matched(CompiledStep::Segment {
segment: camel_api::OutcomeSegment::new(Box::new(svc)),
body_contract: None,
lifecycle: None,
label: None,
}))
}
BuilderStep::CachePeekStale {
repository,
key,
on_miss,
} => {
let repo_name = repository.as_deref().unwrap_or("memory");
let repo = ctx.cache_repositories.get(repo_name).ok_or_else(|| {
CamelError::ComponentNotFound(format!(
"cache_peek_stale: repository '{repo_name}' is not registered"
))
})?;
let key_expr = compile_message_id_expression(
ctx.languages,
&key,
eval_meta(ctx, &key.language, "cache_peek_stale", step_index, None),
)?;
let svc = camel_processor::CachePeekStaleService::new(
repo,
key_expr,
on_miss,
ctx.rt.clone(),
);
Ok(CompileOutcome::Matched(CompiledStep::Segment {
segment: camel_api::OutcomeSegment::new(Box::new(svc)),
body_contract: None,
lifecycle: None,
label: None,
}))
}
BuilderStep::CacheClear { repository } => {
let repo_name = repository.as_deref().unwrap_or("memory");
let repo = ctx.cache_repositories.get(repo_name).ok_or_else(|| {
CamelError::ComponentNotFound(format!(
"cache_clear: repository '{repo_name}' is not registered"
))
})?;
let svc = camel_processor::CacheClearService::new(repo);
Ok(CompileOutcome::Matched(CompiledStep::Segment {
segment: camel_api::OutcomeSegment::new(Box::new(svc)),
body_contract: None,
lifecycle: None,
label: None,
}))
}
BuilderStep::CacheStats { repository } => {
let repo_name = repository.as_deref().unwrap_or("memory");
let repo = ctx.cache_repositories.get(repo_name).ok_or_else(|| {
CamelError::ComponentNotFound(format!(
"cache_stats: repository '{repo_name}' is not registered"
))
})?;
let svc = camel_processor::CacheStatsService::new(repo);
Ok(CompileOutcome::Matched(CompiledStep::Segment {
segment: camel_api::OutcomeSegment::new(Box::new(svc)),
body_contract: None,
lifecycle: None,
label: None,
}))
}
BuilderStep::Log { level, message } => {
let svc = LogProcessor::new(level, message);
Ok(CompileOutcome::Matched(CompiledStep::Process {
kind_hint: SpanKindHint::Internal,
processor: BoxProcessor::new(svc),
body_contract: None,
lifecycle: None,
label: None,
to_uri: None,
}))
}
BuilderStep::DeclarativeLog { level, message } => {
let ValueSourceDef::Expression(expression) = message else {
unreachable!(
"DeclarativeLog with Literal should have been compiled to a Processor"
);
};
let source = compile_value_source(ctx, &expression, "log", step_index, None)?;
let svc = DynamicLog::new(level, source);
Ok(CompileOutcome::Matched(CompiledStep::Process {
kind_hint: SpanKindHint::Internal,
processor: BoxProcessor::new(svc),
body_contract: None,
lifecycle: None,
label: None,
to_uri: None,
}))
}
BuilderStep::DeclarativeSetHeader { key, value } => match value {
ValueSourceDef::Literal(value) => {
let svc = camel_processor::SetHeader::new(IdentityProcessor, key, value);
Ok(CompileOutcome::Matched(CompiledStep::Process {
kind_hint: SpanKindHint::Internal,
processor: BoxProcessor::new(svc),
body_contract: None,
lifecycle: None,
label: None,
to_uri: None,
}))
}
ValueSourceDef::Expression(expression) => {
let source = compile_value_source(
ctx,
&expression,
"set_header",
step_index,
Some(key.clone()),
)?;
let svc =
camel_processor::DynamicSetHeader::new(IdentityProcessor, key, source);
Ok(CompileOutcome::Matched(CompiledStep::Process {
kind_hint: SpanKindHint::Internal,
processor: BoxProcessor::new(svc),
body_contract: None,
lifecycle: None,
label: None,
to_uri: None,
}))
}
_ => Err(CamelError::Config("unsupported value source".into())),
},
BuilderStep::DeclarativeRemoveHeader { key } => {
let svc = camel_processor::RemoveHeader::new(IdentityProcessor, key);
Ok(CompileOutcome::Matched(CompiledStep::Process {
kind_hint: SpanKindHint::Internal,
processor: BoxProcessor::new(svc),
body_contract: None,
lifecycle: None,
label: None,
to_uri: None,
}))
}
BuilderStep::DeclarativeSetHeaderIfAbsent { key, value } => match value {
ValueSourceDef::Literal(value) => {
let svc =
camel_processor::SetHeaderIfAbsent::new(IdentityProcessor, key, value);
Ok(CompileOutcome::Matched(CompiledStep::Process {
kind_hint: SpanKindHint::Internal,
processor: BoxProcessor::new(svc),
body_contract: None,
lifecycle: None,
label: None,
to_uri: None,
}))
}
ValueSourceDef::Expression(expression) => {
let source = compile_value_source(
ctx,
&expression,
"set_header_if_absent",
step_index,
Some(key.clone()),
)?;
let svc = camel_processor::DynamicSetHeaderIfAbsent::new(
IdentityProcessor,
key,
source,
);
Ok(CompileOutcome::Matched(CompiledStep::Process {
kind_hint: SpanKindHint::Internal,
processor: BoxProcessor::new(svc),
body_contract: None,
lifecycle: None,
label: None,
to_uri: None,
}))
}
_ => Err(CamelError::Config("unsupported value source".into())),
},
BuilderStep::DeclarativeSetProperty { key, value_source } => match value_source {
ValueSourceDef::Literal(value) => {
let svc = set_property::SetProperty::new(IdentityProcessor, key, value);
Ok(CompileOutcome::Matched(CompiledStep::Process {
kind_hint: SpanKindHint::Internal,
processor: BoxProcessor::new(svc),
body_contract: None,
lifecycle: None,
label: None,
to_uri: None,
}))
}
ValueSourceDef::Expression(expression) => {
let source = compile_value_source(
ctx,
&expression,
"set_property",
step_index,
Some(key.clone()),
)?;
let svc =
camel_processor::DynamicSetProperty::new(IdentityProcessor, key, source);
Ok(CompileOutcome::Matched(CompiledStep::Process {
kind_hint: SpanKindHint::Internal,
processor: BoxProcessor::new(svc),
body_contract: None,
lifecycle: None,
label: None,
to_uri: None,
}))
}
_ => Err(CamelError::Config("unsupported value source".into())),
},
BuilderStep::DeclarativeSetBody { value } => match value {
ValueSourceDef::Literal(value) => {
let source = Arc::new(move |_: &Exchange| value.clone())
as Arc<dyn Fn(&Exchange) -> Value + Send + Sync>;
let svc = camel_processor::SetBody::new(IdentityProcessor, source);
Ok(CompileOutcome::Matched(CompiledStep::Process {
kind_hint: SpanKindHint::Internal,
processor: BoxProcessor::new(svc),
body_contract: None,
lifecycle: None,
label: None,
to_uri: None,
}))
}
ValueSourceDef::Expression(expression) => {
let source = compile_value_source(
ctx,
&expression,
"set_body",
step_index,
Some("body".to_string()),
)?;
let svc = camel_processor::SetBody::new(IdentityProcessor, source);
Ok(CompileOutcome::Matched(CompiledStep::Process {
kind_hint: SpanKindHint::Internal,
processor: BoxProcessor::new(svc),
body_contract: None,
lifecycle: None,
label: None,
to_uri: None,
}))
}
_ => Err(CamelError::Config("unsupported value source".into())),
},
BuilderStep::DeclarativeScript { expression } => {
compile_script(ctx, &expression.language, &expression.source, step_index)
}
BuilderStep::DeclarativeFunction { mut definition } => {
let Some(invoker) = ctx.function_invoker.clone() else {
return Err(CamelError::Config(
"function: step requires FunctionRuntimeService registered via with_lifecycle"
.into(),
));
};
definition.route_id = ctx.route_id.map(|s| s.to_string());
definition.step_index = Some(step_index);
match ctx.staging_mode {
FunctionStagingMode::DirectAdd => {
invoker.stage_pending(definition.clone(), ctx.route_id, 0);
}
FunctionStagingMode::HotReload { generation } => {
invoker.stage_pending(definition.clone(), ctx.route_id, *generation);
}
FunctionStagingMode::DryCompile => {}
}
let step = crate::step::function_step::FunctionStep::new(invoker, definition);
Ok(CompileOutcome::Matched(CompiledStep::Process {
kind_hint: SpanKindHint::Internal,
processor: BoxProcessor::new(step),
body_contract: None,
lifecycle: None,
label: None,
to_uri: None,
}))
}
BuilderStep::Bean { name, method } => {
let beans = ctx
.beans
.lock()
.map_err(|_| CamelError::ProcessorError("beans mutex poisoned".into()))?;
let bean = beans.get(&name).ok_or_else(|| {
CamelError::ProcessorError(format!("Bean not found: {}", name))
})?;
let processor = tower::service_fn(move |mut exchange: Exchange| {
let bean = Arc::clone(&bean);
let method = method.clone();
async move {
if !bean.methods().iter().any(|m| m == &method) {
return Err(
camel_bean::BeanError::MethodNotFound(method.clone()).into()
);
}
bean.call(&method, &mut exchange).await?;
Ok(exchange)
}
});
Ok(CompileOutcome::Matched(CompiledStep::Process {
kind_hint: SpanKindHint::Internal,
processor: BoxProcessor::new(processor),
body_contract: None,
lifecycle: None,
label: None,
to_uri: None,
}))
}
BuilderStep::Script { language, script } => {
compile_script(ctx, &language, &script, step_index)
}
BuilderStep::ClaimCheck {
repository,
operation,
key,
filter,
} => {
let repo = ctx
.claim_check_repositories
.get(&repository)
.ok_or_else(|| {
CamelError::ComponentNotFound(format!(
"claim_check: repository '{repository}' is not registered"
))
})?;
let op = match operation.as_str() {
"set" => camel_processor::claim_check::ClaimCheckOp::Set,
"get" => camel_processor::claim_check::ClaimCheckOp::Get,
"get_and_remove" => camel_processor::claim_check::ClaimCheckOp::GetAndRemove,
"push" => camel_processor::claim_check::ClaimCheckOp::Push,
"pop" => camel_processor::claim_check::ClaimCheckOp::Pop,
other => {
return Err(CamelError::RouteError(format!(
"claim_check: unsupported operation '{other}'"
)));
}
};
let key_expr = compile_key_expression(
ctx.languages,
&key,
eval_meta(ctx, &key.language, "claim_check", step_index, None),
)?;
let mut svc = camel_processor::ClaimCheckService::new(repo, op, key_expr);
if let Some(filter_str) = filter {
let f = ClaimCheckFilter::parse(&filter_str).map_err(|e| {
CamelError::RouteError(format!(
"claim_check: invalid filter '{filter_str}': {e}"
))
})?;
svc = svc.with_filter(f);
}
Ok(CompileOutcome::Matched(CompiledStep::Process {
kind_hint: SpanKindHint::Internal,
processor: BoxProcessor::new(svc),
body_contract: None,
lifecycle: None,
label: None,
to_uri: None,
}))
}
BuilderStep::Sampling { period } => {
let svc = camel_processor::SamplingService::new(period);
Ok(CompileOutcome::Matched(CompiledStep::Process {
kind_hint: SpanKindHint::Internal,
processor: BoxProcessor::new(svc),
body_contract: None,
lifecycle: None,
label: None,
to_uri: None,
}))
}
BuilderStep::Sort {
expression,
reverse,
} => {
let sort_expr = compile_sort_expression(
ctx.languages,
&expression,
eval_meta(ctx, &expression.language, "sort", step_index, None),
)?;
let svc = camel_processor::SortService::new(sort_expr, reverse);
Ok(CompileOutcome::Matched(CompiledStep::Process {
kind_hint: SpanKindHint::Internal,
processor: BoxProcessor::new(svc),
body_contract: None,
lifecycle: None,
label: None,
to_uri: None,
}))
}
_ => Ok(CompileOutcome::NotHandled(step)),
}
}
}
fn compile_script(
ctx: &CompilationContext,
language: &str,
source: &str,
step_index: usize,
) -> Result<CompileOutcome, CamelError> {
let meta = eval_meta(ctx, language, "script", step_index, None);
let lang = resolve_language(ctx.languages, language)?;
match lang.create_mutating_expression(source) {
Ok(mut_expr) => Ok(CompileOutcome::Matched(CompiledStep::Process {
kind_hint: SpanKindHint::Internal,
processor: BoxProcessor::new(ScriptMutator::with_meta(mut_expr, meta)),
body_contract: None,
lifecycle: None,
label: None,
to_uri: None,
})),
Err(LanguageError::NotSupported { .. }) => Err(CamelError::RouteError(format!(
"script: language '{language}' does not support mutating expressions; a `script` step requires a language that implements MutatingExpression. To replace the body with a read-only expression, use a `set_body`/`transform` step with this language instead"
))),
Err(e) => Err(CamelError::RouteError(format!(
"Failed to create mutating expression for language '{language}': {e}"
))),
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use camel_api::{CamelError, Exchange, LanguageExpressionDef, Message, ProducerContext, Value};
use camel_bean::BeanRegistry;
use camel_component_api::{
ComponentContext, NoOpComponentContext, RuntimeObservability,
test_support::NoopRuntimeObservability,
};
use camel_language_api::Language;
use camel_processor::aggregator::SharedLanguageRegistry;
use crate::idempotent::MemoryIdempotentRepository;
use crate::lifecycle::adapters::step_compilers::{
CompilationContext, CompiledStep, StepCompilerRegistry,
};
use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
use crate::lifecycle::application::route_definition::BuilderStep;
#[tokio::test]
async fn compile_idempotent_consumer_produces_segment() {
let languages: SharedLanguageRegistry = {
let mut map: HashMap<String, Arc<dyn Language>> = HashMap::new();
map.insert(
"simple".to_string(),
Arc::new(camel_language_simple::SimpleLanguage::new()),
);
Arc::new(Mutex::new(map))
};
let idempotent_repositories = crate::IdempotentRegistry::new();
let repo: Arc<dyn camel_api::IdempotentRepository> =
Arc::new(MemoryIdempotentRepository::new("memory"));
idempotent_repositories
.register("memory", repo)
.expect("register repo");
let claim_check_repositories = crate::ClaimCheckRegistry::new();
let cache_repositories = crate::CacheRegistry::new();
let mut reg = StepCompilerRegistry::new();
reg.register(Box::new(super::CoreCompiler));
let pc = ProducerContext::default();
let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
let staging = FunctionStagingMode::DirectAdd;
let ctx = CompilationContext {
producer_ctx: &pc,
rt,
languages: &languages,
beans: &beans,
function_invoker: None,
component_ctx,
route_id: None,
staging_mode: &staging,
idempotent_repositories: &idempotent_repositories,
claim_check_repositories: &claim_check_repositories,
cache_repositories: &cache_repositories,
intercept: crate::intercept::InterceptRules::default(),
};
let step = BuilderStep::IdempotentConsumer {
repository: "memory".into(),
expression: LanguageExpressionDef {
language: "simple".into(),
source: "${header.id}".into(),
},
steps: vec![BuilderStep::Log {
level: camel_processor::LogLevel::Info,
message: "duplicate check passed".into(),
}],
eager: false,
remove_on_failure: true,
};
let result = reg.compile_step(step, 0, &ctx);
let compiled = result
.expect("compilation should succeed")
.expect("should match");
assert!(
matches!(compiled, CompiledStep::Segment { .. }),
"Expected CompiledStep::Segment, got {compiled:?}"
);
}
#[test]
fn nested_resequence_rejected_at_compile_children() {
use camel_api::CamelError;
let mut reg = StepCompilerRegistry::new();
reg.register(Box::new(super::CoreCompiler));
let step = BuilderStep::Resequence {
policy_config: camel_api::ResequencePolicyConfig::default(),
};
let result = reg.compile_step(step, 0, &dummy_context());
let err = result.expect_err("compiler should produce an error for nested resequence");
assert!(
matches!(err, CamelError::RouteError(_)),
"should be RouteError, got {err:?}"
);
assert!(
err.to_string().contains("top-level"),
"error should mention 'top-level', got: {err}"
);
}
struct PermissiveBean;
#[async_trait::async_trait]
impl camel_bean::BeanProcessor for PermissiveBean {
async fn call(
&self,
_method: &str,
_exchange: &mut camel_api::Exchange,
) -> Result<(), CamelError> {
Ok(())
}
fn methods(&self) -> Vec<String> {
vec!["handle".to_string()]
}
}
#[tokio::test]
async fn bean_step_rejects_method_not_in_allowlist() {
use tower::ServiceExt;
let beans = BeanRegistry::new();
beans.register("test_bean", PermissiveBean).unwrap();
let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(beans));
let producer_ctx: &'static ProducerContext =
Box::leak(Box::new(ProducerContext::default()));
let staging: &'static FunctionStagingMode =
Box::leak(Box::new(FunctionStagingMode::DirectAdd));
let languages: &'static SharedLanguageRegistry =
Box::leak(Box::new(Arc::new(Mutex::new(HashMap::new()))));
let beans_ref: &'static Arc<Mutex<BeanRegistry>> = Box::leak(Box::new(beans));
let idempotent: &'static crate::IdempotentRegistry =
Box::leak(Box::new(crate::IdempotentRegistry::new()));
let claim_check: &'static crate::ClaimCheckRegistry =
Box::leak(Box::new(crate::ClaimCheckRegistry::new()));
let cache: &'static crate::CacheRegistry = Box::leak(Box::new(crate::CacheRegistry::new()));
let ctx = CompilationContext {
producer_ctx,
rt: Arc::new(NoopRuntimeObservability),
languages,
beans: beans_ref,
function_invoker: None,
component_ctx: Arc::new(NoOpComponentContext),
route_id: None,
staging_mode: staging,
idempotent_repositories: idempotent,
claim_check_repositories: claim_check,
cache_repositories: cache,
intercept: crate::intercept::InterceptRules::default(),
};
let mut reg = StepCompilerRegistry::new();
reg.register(Box::new(super::CoreCompiler));
let step = BuilderStep::Bean {
name: "test_bean".into(),
method: "unauthorized_method".into(),
};
let result = reg.compile_step(step, 0, &ctx);
let compiled = result
.expect("compilation should succeed")
.expect("should match");
let processor = match compiled {
CompiledStep::Process { processor, .. } => processor,
other => panic!("expected CompiledStep::Process, got {other:?}"),
};
let exchange = Exchange::new(Message::default());
let result = processor.oneshot(exchange).await;
assert!(
result.is_err(),
"bean step should reject method not in allowlist"
);
let err = result.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("unauthorized_method") && msg.contains("not found"),
"error should mention method name and 'not found', got: {msg}"
);
}
#[tokio::test]
async fn test_remove_header_end_to_end() {
use tower::ServiceExt;
let mut reg = StepCompilerRegistry::new();
reg.register(Box::new(super::CoreCompiler));
let step = BuilderStep::DeclarativeRemoveHeader {
key: "CamelHttpPath".into(),
};
let compiled = reg
.compile_step(step, 0, &dummy_context())
.expect("compilation should succeed")
.expect("should match");
let processor = match compiled {
CompiledStep::Process { processor, .. } => processor,
other => panic!("expected CompiledStep::Process, got {other:?}"),
};
let mut exchange = Exchange::new(Message::default());
exchange.input.set_header("CamelHttpPath", "some-path");
let result = processor.oneshot(exchange).await.unwrap();
assert!(
!result.input.headers.contains_key("CamelHttpPath"),
"CamelHttpPath should be removed from input headers"
);
}
#[tokio::test]
async fn test_remove_header_input_only_preserves_output() {
use tower::ServiceExt;
let mut reg = StepCompilerRegistry::new();
reg.register(Box::new(super::CoreCompiler));
let step = BuilderStep::DeclarativeRemoveHeader {
key: "X-Shared".into(),
};
let compiled = reg
.compile_step(step, 0, &dummy_context())
.expect("compilation should succeed")
.expect("should match");
let processor = match compiled {
CompiledStep::Process { processor, .. } => processor,
other => panic!("expected CompiledStep::Process, got {other:?}"),
};
let mut output = Message::default();
output.set_header("X-Shared", "output-value");
let mut exchange = Exchange::new(Message::default());
exchange.input.set_header("X-Shared", "input-value");
exchange.output = Some(output);
let result = processor.oneshot(exchange).await.unwrap();
assert!(
!result.input.headers.contains_key("X-Shared"),
"X-Shared should be removed from input headers"
);
let output = result
.output
.as_ref()
.expect("output message should be preserved");
assert!(
output.headers.contains_key("X-Shared"),
"X-Shared should remain on output headers"
);
}
#[test]
fn cache_clear_unknown_repository_fails_compile() {
let mut reg = StepCompilerRegistry::new();
reg.register(Box::new(super::CoreCompiler));
let step = BuilderStep::CacheClear {
repository: Some("nope".into()),
};
let result = reg.compile_step(step, 0, &dummy_context());
let err = result.expect_err("compilation should fail for unknown repository");
match err {
CamelError::ComponentNotFound(msg) => {
assert!(
msg.contains("cache_clear"),
"error must name cache_clear: {msg}"
);
assert!(
msg.contains("nope"),
"error must name the repository: {msg}"
);
}
other => panic!("expected ComponentNotFound, got {other:?}"),
}
}
fn compile_cache_step(step: BuilderStep) -> Result<Option<CompiledStep>, CamelError> {
let languages: SharedLanguageRegistry = {
let mut map: HashMap<String, Arc<dyn Language>> = HashMap::new();
map.insert(
"simple".to_string(),
Arc::new(camel_language_simple::SimpleLanguage::new()),
);
Arc::new(Mutex::new(map))
};
let cache_repositories = crate::CacheRegistry::new();
let repo: Arc<dyn camel_api::CacheRepository> =
Arc::new(crate::cache::MemoryCacheRepository::new("memory", 100));
cache_repositories
.register("memory", repo)
.expect("register cache repo");
let idempotent_repositories = crate::IdempotentRegistry::new();
let claim_check_repositories = crate::ClaimCheckRegistry::new();
let mut reg = StepCompilerRegistry::new();
reg.register(Box::new(super::CoreCompiler));
let pc = ProducerContext::default();
let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
let staging = FunctionStagingMode::DirectAdd;
let ctx = CompilationContext {
producer_ctx: &pc,
rt,
languages: &languages,
beans: &beans,
function_invoker: None,
component_ctx,
route_id: None,
staging_mode: &staging,
idempotent_repositories: &idempotent_repositories,
claim_check_repositories: &claim_check_repositories,
cache_repositories: &cache_repositories,
intercept: crate::intercept::InterceptRules::default(),
};
reg.compile_step(step, 0, &ctx)
}
#[test]
fn cache_invalidate_both_key_and_prefix_rejected() {
for step in [
BuilderStep::CacheInvalidate {
repository: None,
key: Some(LanguageExpressionDef {
language: "simple".into(),
source: "k".into(),
}),
key_prefix: Some(LanguageExpressionDef {
language: "simple".into(),
source: "ns:".into(),
}),
},
BuilderStep::CacheInvalidate {
repository: None,
key: None,
key_prefix: None,
},
] {
let err =
compile_cache_step(step).expect_err("key/key_prefix conflict must be rejected");
match err {
CamelError::Config(msg) => {
assert!(
msg.contains("cache_invalidate"),
"error must name the invalidate step: {msg}"
);
assert!(
msg.contains("exactly one of"),
"error must require exactly one of key/key_prefix: {msg}"
);
}
other => panic!("expected CamelError::Config, got {other:?}"),
}
}
}
#[test]
fn cache_invalidate_prefix_compiles_to_target() {
let step = BuilderStep::CacheInvalidate {
repository: None,
key: None,
key_prefix: Some(LanguageExpressionDef {
language: "simple".into(),
source: "ns:".into(),
}),
};
let compiled = compile_cache_step(step)
.expect("compilation should succeed")
.expect("should match");
assert!(
matches!(compiled, CompiledStep::Segment { .. }),
"expected CompiledStep::Segment, got {compiled:?}"
);
}
#[test]
fn cache_invalidate_empty() {
let step = BuilderStep::CacheInvalidate {
repository: None,
key: Some(LanguageExpressionDef {
language: "simple".into(),
source: "".into(),
}),
key_prefix: None,
};
let err = compile_cache_step(step).expect_err("empty key must be rejected");
match err {
CamelError::Config(msg) => {
assert!(
msg.contains("must not be empty"),
"error must name the empty guard: {msg}"
);
}
other => panic!("expected CamelError::Config, got {other:?}"),
}
}
#[test]
fn cache_coalesce_misses_compiles() {
let step = BuilderStep::Cache {
repository: None,
key: LanguageExpressionDef {
language: "simple".into(),
source: "k".into(),
},
ttl: None,
max_entry_bytes: None,
coalesce_misses: true,
on_miss: vec![BuilderStep::Log {
level: camel_processor::LogLevel::Info,
message: "miss".into(),
}],
};
let compiled = compile_cache_step(step)
.expect("compilation should succeed")
.expect("should match");
assert!(
matches!(compiled, CompiledStep::Segment { .. }),
"expected CompiledStep::Segment, got {compiled:?}"
);
}
fn script_ctx(langs: Vec<(&str, Arc<dyn Language>)>) -> CompilationContext<'static> {
use crate::lifecycle::adapters::route_controller::SharedLanguageRegistry;
let mut map: HashMap<String, Arc<dyn Language>> = HashMap::new();
for (name, lang) in langs {
map.insert(name.to_string(), lang);
}
let languages: &'static SharedLanguageRegistry =
Box::leak(Box::new(Arc::new(Mutex::new(map))));
let producer_ctx: &'static ProducerContext =
Box::leak(Box::new(ProducerContext::default()));
let staging: &'static FunctionStagingMode =
Box::leak(Box::new(FunctionStagingMode::DirectAdd));
let beans: &'static Arc<Mutex<BeanRegistry>> =
Box::leak(Box::new(Arc::new(Mutex::new(BeanRegistry::new()))));
let idempotent: &'static crate::IdempotentRegistry =
Box::leak(Box::new(crate::IdempotentRegistry::new()));
let claim_check: &'static crate::ClaimCheckRegistry =
Box::leak(Box::new(crate::ClaimCheckRegistry::new()));
let cache: &'static crate::CacheRegistry = Box::leak(Box::new(crate::CacheRegistry::new()));
CompilationContext {
producer_ctx,
rt: Arc::new(NoopRuntimeObservability),
languages,
beans,
function_invoker: None,
component_ctx: Arc::new(NoOpComponentContext),
route_id: None,
staging_mode: staging,
idempotent_repositories: idempotent,
claim_check_repositories: claim_check,
cache_repositories: cache,
intercept: crate::intercept::InterceptRules::default(),
}
}
fn simple_language() -> Arc<dyn Language> {
Arc::new(camel_language_simple::SimpleLanguage::new())
}
fn compile_script_step(
reg: &StepCompilerRegistry,
step: BuilderStep,
ctx: &CompilationContext<'_>,
) -> Result<CompiledStep, CamelError> {
reg.compile_step(step, 0, ctx)?
.ok_or_else(|| CamelError::Config("step was not handled by CoreCompiler".into()))
}
struct CreationFailLanguage;
impl Language for CreationFailLanguage {
fn name(&self) -> &'static str {
"creationfail"
}
fn create_expression(
&self,
_script: &str,
) -> Result<Box<dyn camel_language_api::Expression>, camel_language_api::LanguageError>
{
Ok(Box::new(ScriptStubExpression))
}
fn create_predicate(
&self,
_script: &str,
) -> Result<Box<dyn camel_language_api::Predicate>, camel_language_api::LanguageError>
{
Ok(Box::new(ScriptStubPredicate))
}
fn create_mutating_expression(
&self,
_script: &str,
) -> Result<
Box<dyn camel_language_api::MutatingExpression>,
camel_language_api::LanguageError,
> {
Err(camel_language_api::LanguageError::EvalError("boom".into()))
}
}
struct EvalFailLanguage;
impl Language for EvalFailLanguage {
fn name(&self) -> &'static str {
"evalfail"
}
fn create_expression(
&self,
_script: &str,
) -> Result<Box<dyn camel_language_api::Expression>, camel_language_api::LanguageError>
{
Ok(Box::new(ScriptStubExpression))
}
fn create_predicate(
&self,
_script: &str,
) -> Result<Box<dyn camel_language_api::Predicate>, camel_language_api::LanguageError>
{
Ok(Box::new(ScriptStubPredicate))
}
fn create_mutating_expression(
&self,
_script: &str,
) -> Result<
Box<dyn camel_language_api::MutatingExpression>,
camel_language_api::LanguageError,
> {
Ok(Box::new(EvalFailExpr))
}
}
struct EvalFailExpr;
#[async_trait::async_trait]
impl camel_language_api::MutatingExpression for EvalFailExpr {
async fn evaluate(
&self,
_exchange: &mut Exchange,
) -> Result<Value, camel_language_api::LanguageError> {
Err(camel_language_api::LanguageError::EvalError("boom".into()))
}
}
struct ScriptStubExpression;
#[async_trait::async_trait]
impl camel_language_api::Expression for ScriptStubExpression {
async fn evaluate(
&self,
_exchange: &Exchange,
) -> Result<Value, camel_language_api::LanguageError> {
Ok(Value::Null)
}
}
struct ScriptStubPredicate;
#[async_trait::async_trait]
impl camel_language_api::Predicate for ScriptStubPredicate {
async fn matches(
&self,
_exchange: &Exchange,
) -> Result<bool, camel_language_api::LanguageError> {
Ok(false)
}
}
#[test]
fn compile_script_rejects_non_mutating_language() {
let ctx = script_ctx(vec![("simple", simple_language())]);
let mut reg = StepCompilerRegistry::new();
reg.register(Box::new(super::CoreCompiler));
let err = reg
.compile_step(
BuilderStep::DeclarativeScript {
expression: LanguageExpressionDef {
language: "simple".into(),
source: "${body}".into(),
},
},
0,
&ctx,
)
.expect_err("a read-only language must be rejected at compile time");
const EXPECTED: &str = "script: language 'simple' does not support mutating expressions; a `script` step requires a language that implements MutatingExpression. To replace the body with a read-only expression, use a `set_body`/`transform` step with this language instead";
match err {
CamelError::RouteError(msg) => assert_eq!(msg, EXPECTED),
other => panic!("expected RouteError, got {other:?}"),
}
}
#[test]
fn compile_script_both_variants_byte_identical() {
let ctx = script_ctx(vec![("simple", simple_language())]);
let mut reg = StepCompilerRegistry::new();
reg.register(Box::new(super::CoreCompiler));
let err_script = reg
.compile_step(
BuilderStep::Script {
language: "simple".into(),
script: "${body}".into(),
},
0,
&ctx,
)
.expect_err("Script with a read-only language must fail");
let err_declarative = reg
.compile_step(
BuilderStep::DeclarativeScript {
expression: LanguageExpressionDef {
language: "simple".into(),
source: "${body}".into(),
},
},
0,
&ctx,
)
.expect_err("DeclarativeScript with a read-only language must fail");
assert_eq!(err_script.to_string(), err_declarative.to_string());
}
#[tokio::test]
async fn compile_script_custom_mutating_language_executes_mutation() {
use crate::lifecycle::adapters::step_resolution::MutatingTestLanguage;
use tower::ServiceExt;
let ctx = script_ctx(vec![(
"testmut",
Arc::new(MutatingTestLanguage) as Arc<dyn Language>,
)]);
let mut reg = StepCompilerRegistry::new();
reg.register(Box::new(super::CoreCompiler));
let compiled = compile_script_step(
®,
BuilderStep::DeclarativeScript {
expression: LanguageExpressionDef {
language: "testmut".into(),
source: "body = 1;".into(),
},
},
&ctx,
)
.expect("mutating language must compile");
let processor = match compiled {
CompiledStep::Process { processor, .. } => processor,
other => panic!("expected CompiledStep::Process, got {other:?}"),
};
let result = processor
.oneshot(Exchange::default())
.await
.expect("mutating script must execute");
assert_eq!(
result.property("testmut"),
Some(&Value::String("ran".into()))
);
}
#[tokio::test]
async fn compile_script_mutating_eval_error_propagates_with_metadata() {
use tower::ServiceExt;
let ctx = script_ctx(vec![(
"evalfail",
Arc::new(EvalFailLanguage) as Arc<dyn Language>,
)]);
let mut reg = StepCompilerRegistry::new();
reg.register(Box::new(super::CoreCompiler));
let compiled = compile_script_step(
®,
BuilderStep::DeclarativeScript {
expression: LanguageExpressionDef {
language: "evalfail".into(),
source: "x".into(),
},
},
&ctx,
)
.expect("creation succeeds; failure happens at evaluation");
let processor = match compiled {
CompiledStep::Process { processor, .. } => processor,
other => panic!("expected CompiledStep::Process, got {other:?}"),
};
let result = processor.oneshot(Exchange::default()).await;
match result {
Err(CamelError::ExpressionFailed { language, verb, .. }) => {
assert_eq!(language, "evalfail");
assert_eq!(verb, "script");
}
other => panic!("expected ExpressionFailed, got {other:?}"),
}
}
#[test]
fn compile_script_creation_error_unchanged() {
let ctx = script_ctx(vec![(
"creationfail",
Arc::new(CreationFailLanguage) as Arc<dyn Language>,
)]);
let mut reg = StepCompilerRegistry::new();
reg.register(Box::new(super::CoreCompiler));
let err = reg
.compile_step(
BuilderStep::DeclarativeScript {
expression: LanguageExpressionDef {
language: "creationfail".into(),
source: "x".into(),
},
},
0,
&ctx,
)
.expect_err("creation failure must fail compilation");
let msg = err.to_string();
assert!(
msg.contains("Failed to create mutating expression for language 'creationfail'"),
"unexpected error: {msg}"
);
assert!(msg.contains("boom"), "unexpected error: {msg}");
}
#[test]
fn compile_script_unknown_language_error_unchanged() {
let ctx = script_ctx(vec![]);
let mut reg = StepCompilerRegistry::new();
reg.register(Box::new(super::CoreCompiler));
let err = reg
.compile_step(
BuilderStep::Script {
language: "nope".into(),
script: "x".into(),
},
0,
&ctx,
)
.expect_err("unknown language must fail resolution");
assert!(
err.to_string().contains("not registered"),
"unexpected error: {err}"
);
}
fn dummy_context() -> CompilationContext<'static> {
use std::collections::HashMap;
use std::sync::Mutex;
use camel_bean::BeanRegistry;
use camel_component_api::{NoOpComponentContext, test_support::NoopRuntimeObservability};
use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
let producer_ctx: &'static ProducerContext =
Box::leak(Box::new(ProducerContext::default()));
let staging: &'static FunctionStagingMode =
Box::leak(Box::new(FunctionStagingMode::DirectAdd));
let languages: &'static SharedLanguageRegistry =
Box::leak(Box::new(Arc::new(Mutex::new(HashMap::new()))));
let beans: &'static Arc<Mutex<BeanRegistry>> =
Box::leak(Box::new(Arc::new(Mutex::new(BeanRegistry::new()))));
let idempotent: &'static crate::IdempotentRegistry =
Box::leak(Box::new(crate::IdempotentRegistry::new()));
let claim_check: &'static crate::ClaimCheckRegistry =
Box::leak(Box::new(crate::ClaimCheckRegistry::new()));
let cache: &'static crate::CacheRegistry = Box::leak(Box::new(crate::CacheRegistry::new()));
CompilationContext {
producer_ctx,
rt: Arc::new(NoopRuntimeObservability),
languages,
beans,
function_invoker: None,
component_ctx: Arc::new(NoOpComponentContext),
route_id: None,
staging_mode: staging,
idempotent_repositories: idempotent,
claim_check_repositories: claim_check,
cache_repositories: cache,
intercept: crate::intercept::InterceptRules::default(),
}
}
}