use std::collections::BTreeSet;
use camel_core::intercept::InterceptAction;
use camel_core::{BuilderStep, RouteDefinition};
const LEAN_SCHEMES: [&str; 5] = ["direct", "log", "mock", "seda", "timer"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Tier {
Lean,
Full,
}
#[derive(Debug, Clone, Copy)]
pub struct DocumentInputs<'a> {
pub has_scenario: bool,
pub intercepts: &'a [(String, InterceptAction)],
pub unit_schemes: &'a [String],
}
pub fn derive_tier(routes: &[RouteDefinition], doc: &DocumentInputs<'_>) -> Tier {
if doc.has_scenario {
return Tier::Full;
}
let mut uris: Vec<&str> = Vec::new();
let mut dynamic_dispatch = false;
for route in routes {
uris.push(route.from_uri());
if let Some(dlc) = route
.error_handler_config()
.and_then(|handler| handler.dlc_uri.as_deref())
{
uris.push(dlc);
}
walk_steps(route.steps(), &mut uris, &mut dynamic_dispatch);
walk_steps(
route.circuit_breaker_fallback(),
&mut uris,
&mut dynamic_dispatch,
);
}
let replaced_by_skip_to = |uri: &str| {
doc.intercepts
.iter()
.any(|(key, action)| key == uri && matches!(action, InterceptAction::SkipTo { .. }))
};
let mut schemes: BTreeSet<&str> = doc.unit_schemes.iter().map(String::as_str).collect();
let mut placeholder_in_scheme = false;
for uri in uris.into_iter().filter(|uri| !replaced_by_skip_to(uri)) {
let head = scheme_head(uri);
if head.contains("${") || head.contains("{{") {
placeholder_in_scheme = true;
}
schemes.insert(head);
}
if dynamic_dispatch || placeholder_in_scheme {
return Tier::Full;
}
if schemes.iter().any(|scheme| !LEAN_SCHEMES.contains(scheme)) {
return Tier::Full;
}
Tier::Lean
}
fn scheme_head(uri: &str) -> &str {
match uri.split_once(':') {
Some((head, _)) => head,
None => uri,
}
}
fn walk_steps<'a>(steps: &'a [BuilderStep], uris: &mut Vec<&'a str>, dynamic_dispatch: &mut bool) {
for step in steps {
match step {
BuilderStep::To(uri)
| BuilderStep::WireTap { uri }
| BuilderStep::Enrich { uri, .. }
| BuilderStep::PollEnrich { uri, .. } => uris.push(uri),
BuilderStep::RecipientList { .. }
| BuilderStep::DeclarativeRecipientList { .. }
| BuilderStep::RoutingSlip { .. }
| BuilderStep::DeclarativeRoutingSlip { .. }
| BuilderStep::DynamicRouter { .. }
| BuilderStep::DeclarativeDynamicRouter { .. } => *dynamic_dispatch = true,
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, .. } => {
walk_steps(steps, uris, dynamic_dispatch);
}
BuilderStep::DeclarativeChoice { whens, otherwise } => {
for when in whens {
walk_steps(&when.steps, uris, dynamic_dispatch);
}
if let Some(steps) = otherwise {
walk_steps(steps, uris, dynamic_dispatch);
}
}
BuilderStep::Choice { whens, otherwise } => {
for when in whens {
walk_steps(&when.steps, uris, dynamic_dispatch);
}
if let Some(steps) = otherwise {
walk_steps(steps, uris, dynamic_dispatch);
}
}
BuilderStep::Cache { on_miss, .. } => {
walk_steps(on_miss, uris, dynamic_dispatch);
}
BuilderStep::DeclarativeDoTry {
try_steps,
catch,
finally,
} => {
walk_steps(try_steps, uris, dynamic_dispatch);
for clause in catch {
walk_steps(&clause.steps, uris, dynamic_dispatch);
}
if let Some(finally) = finally {
walk_steps(&finally.steps, uris, dynamic_dispatch);
}
}
BuilderStep::Processor(_)
| BuilderStep::Stop
| BuilderStep::Log { .. }
| BuilderStep::DeclarativeSetHeader { .. }
| BuilderStep::DeclarativeSetHeaderIfAbsent { .. }
| BuilderStep::DeclarativeRemoveHeader { .. }
| BuilderStep::DeclarativeSetProperty { .. }
| BuilderStep::DeclarativeSetBody { .. }
| BuilderStep::DeclarativeScript { .. }
| BuilderStep::DeclarativeFunction { .. }
| BuilderStep::Aggregate { .. }
| BuilderStep::DeclarativeLog { .. }
| BuilderStep::Bean { .. }
| BuilderStep::Script { .. }
| BuilderStep::Delay { .. }
| BuilderStep::Validate { .. }
| BuilderStep::ClaimCheck { .. }
| BuilderStep::Sampling { .. }
| BuilderStep::Sort { .. }
| BuilderStep::CacheInvalidate { .. }
| BuilderStep::CacheClear { .. }
| BuilderStep::CacheStats { .. }
| BuilderStep::CachePeekStale { .. }
| BuilderStep::Resequence { .. } => {}
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::{DocumentInputs, Tier, derive_tier};
use camel_api::error_handler::ErrorHandlerConfig;
use camel_api::recipient_list::RecipientListConfig;
use camel_api::{DynamicRouterConfig, RoutingSlipConfig};
use camel_core::intercept::InterceptAction;
use camel_core::{BuilderStep, RouteDefinition};
fn unit_schemes() -> Vec<String> {
vec!["direct".to_string(), "mock".to_string()]
}
fn route(from: &str, steps: Vec<BuilderStep>) -> RouteDefinition {
RouteDefinition::new(from, steps)
}
fn lean_route() -> RouteDefinition {
route("direct:start", vec![BuilderStep::To("mock:out".into())])
}
fn inputs<'a>(
has_scenario: bool,
intercepts: &'a [(String, InterceptAction)],
schemes: &'a [String],
) -> DocumentInputs<'a> {
DocumentInputs {
has_scenario,
intercepts,
unit_schemes: schemes,
}
}
#[test]
fn tier_lean_document_stays_lean() {
let routes = [lean_route()];
let schemes = unit_schemes();
let intercepts = Vec::new();
let input = inputs(false, &intercepts, &schemes);
assert_eq!(derive_tier(&routes, &input), Tier::Lean);
}
#[test]
fn tier_skipto_subtracts_from_closure() {
let routes = [route(
"direct:start",
vec![BuilderStep::To("kafka:orders".into())],
)];
let schemes = unit_schemes();
let intercepts = vec![(
"kafka:orders".to_string(),
InterceptAction::SkipTo {
uri: "mock:orders".into(),
},
)];
let input = inputs(false, &intercepts, &schemes);
assert_eq!(derive_tier(&routes, &input), Tier::Lean);
let mismatched = vec![(
"kafka:orders?option=1".to_string(),
InterceptAction::SkipTo {
uri: "mock:orders".into(),
},
)];
let input = inputs(false, &mismatched, &schemes);
assert_eq!(derive_tier(&routes, &input), Tier::Full);
}
#[test]
fn tier_dlc_uri_counts_in_closure() {
let schemes = unit_schemes();
let intercepts = Vec::new();
let kafka_dlq = [
route("direct:start", vec![BuilderStep::To("mock:out".into())])
.with_error_handler(ErrorHandlerConfig::dead_letter_channel("kafka:dlq")),
];
let input = inputs(false, &intercepts, &schemes);
assert_eq!(derive_tier(&kafka_dlq, &input), Tier::Full);
let placeholder_dlq = [
route("direct:start", vec![BuilderStep::To("mock:out".into())])
.with_error_handler(ErrorHandlerConfig::dead_letter_channel("${env:DLQ}:dead")),
];
let input = inputs(false, &intercepts, &schemes);
assert_eq!(derive_tier(&placeholder_dlq, &input), Tier::Full);
let mock_dlq = [
route("direct:start", vec![BuilderStep::To("mock:out".into())])
.with_error_handler(ErrorHandlerConfig::dead_letter_channel("mock:dlc")),
];
let input = inputs(false, &intercepts, &schemes);
assert_eq!(derive_tier(&mock_dlq, &input), Tier::Lean);
}
#[test]
fn tier_divertcopyto_does_not_subtract() {
let routes = [route(
"direct:start",
vec![BuilderStep::To("kafka:orders".into())],
)];
let schemes = unit_schemes();
let intercepts = vec![(
"kafka:orders".to_string(),
InterceptAction::DivertCopyTo {
uri: "mock:mirror".into(),
},
)];
let input = inputs(false, &intercepts, &schemes);
assert_eq!(derive_tier(&routes, &input), Tier::Full);
}
#[test]
fn tier_placeholder_in_scheme_forces_full() {
let routes = [route(
"direct:start",
vec![BuilderStep::To("${env:TARGET_SCHEME}:host".into())],
)];
let schemes = unit_schemes();
let intercepts = Vec::new();
let input = inputs(false, &intercepts, &schemes);
assert_eq!(derive_tier(&routes, &input), Tier::Full);
}
#[test]
fn tier_dynamic_dispatch_forces_full() {
let schemes = unit_schemes();
let intercepts = Vec::new();
let cases: [(&str, BuilderStep); 4] = [
(
"recipient_list",
BuilderStep::RecipientList {
config: RecipientListConfig::new(Arc::new(|_| "mock:one".to_string())),
},
),
(
"routing_slip",
BuilderStep::RoutingSlip {
config: RoutingSlipConfig::new(Arc::new(|_| Some("mock:one".to_string()))),
},
),
(
"dynamic_router",
BuilderStep::DynamicRouter {
config: DynamicRouterConfig::new(Arc::new(|_| Some("mock:one".to_string()))),
},
),
("to_d", BuilderStep::To("${env:SCHEME}:orders".into())),
];
for (name, step) in cases {
let routes = [route("direct:start", vec![step])];
let input = inputs(false, &intercepts, &schemes);
assert_eq!(derive_tier(&routes, &input), Tier::Full, "case {name}");
}
}
#[test]
fn tier_scenario_section_forces_full() {
let routes = [lean_route()];
let schemes = unit_schemes();
let intercepts = Vec::new();
let input = inputs(true, &intercepts, &schemes);
assert_eq!(derive_tier(&routes, &input), Tier::Full);
}
#[test]
fn tier_all_route_sources_count() {
let schemes = unit_schemes();
let intercepts = Vec::new();
for source in ["inline", "routeFilesFromRoot"] {
let all_lean = [
lean_route(),
route("direct:poll", vec![BuilderStep::To("seda:pool".into())]),
];
let input = inputs(false, &intercepts, &schemes);
assert_eq!(
derive_tier(&all_lean, &input),
Tier::Lean,
"source {source}"
);
let one_full = [
lean_route(),
route("direct:ship", vec![BuilderStep::To("kafka:orders".into())]),
];
let input = inputs(false, &intercepts, &schemes);
assert_eq!(
derive_tier(&one_full, &input),
Tier::Full,
"source {source}"
);
}
}
}