1use std::collections::BTreeSet;
14
15use camel_core::intercept::InterceptAction;
16use camel_core::{BuilderStep, RouteDefinition};
17
18const LEAN_SCHEMES: [&str; 5] = ["direct", "log", "mock", "seda", "timer"];
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26#[non_exhaustive]
27pub enum Tier {
28 Lean,
31 Full,
33}
34
35#[derive(Debug, Clone, Copy)]
42pub struct DocumentInputs<'a> {
43 pub has_scenario: bool,
46 pub intercepts: &'a [(String, InterceptAction)],
48 pub unit_schemes: &'a [String],
50}
51
52pub fn derive_tier(routes: &[RouteDefinition], doc: &DocumentInputs<'_>) -> Tier {
63 if doc.has_scenario {
64 return Tier::Full;
65 }
66 let mut uris: Vec<&str> = Vec::new();
67 let mut dynamic_dispatch = false;
68 for route in routes {
69 uris.push(route.from_uri());
70 if let Some(dlc) = route
74 .error_handler_config()
75 .and_then(|handler| handler.dlc_uri.as_deref())
76 {
77 uris.push(dlc);
78 }
79 walk_steps(route.steps(), &mut uris, &mut dynamic_dispatch);
80 walk_steps(
83 route.circuit_breaker_fallback(),
84 &mut uris,
85 &mut dynamic_dispatch,
86 );
87 }
88 let replaced_by_skip_to = |uri: &str| {
94 doc.intercepts
95 .iter()
96 .any(|(key, action)| key == uri && matches!(action, InterceptAction::SkipTo { .. }))
97 };
98 let mut schemes: BTreeSet<&str> = doc.unit_schemes.iter().map(String::as_str).collect();
99 let mut placeholder_in_scheme = false;
104 for uri in uris.into_iter().filter(|uri| !replaced_by_skip_to(uri)) {
105 let head = scheme_head(uri);
106 if head.contains("${") || head.contains("{{") {
107 placeholder_in_scheme = true;
108 }
109 schemes.insert(head);
110 }
111 if dynamic_dispatch || placeholder_in_scheme {
112 return Tier::Full;
113 }
114 if schemes.iter().any(|scheme| !LEAN_SCHEMES.contains(scheme)) {
115 return Tier::Full;
116 }
117 Tier::Lean
118}
119
120fn scheme_head(uri: &str) -> &str {
124 match uri.split_once(':') {
125 Some((head, _)) => head,
126 None => uri,
127 }
128}
129
130fn walk_steps<'a>(steps: &'a [BuilderStep], uris: &mut Vec<&'a str>, dynamic_dispatch: &mut bool) {
139 for step in steps {
140 match step {
141 BuilderStep::To(uri)
143 | BuilderStep::WireTap { uri }
144 | BuilderStep::Enrich { uri, .. }
145 | BuilderStep::PollEnrich { uri, .. } => uris.push(uri),
146
147 BuilderStep::RecipientList { .. }
150 | BuilderStep::DeclarativeRecipientList { .. }
151 | BuilderStep::RoutingSlip { .. }
152 | BuilderStep::DeclarativeRoutingSlip { .. }
153 | BuilderStep::DynamicRouter { .. }
154 | BuilderStep::DeclarativeDynamicRouter { .. } => *dynamic_dispatch = true,
155
156 BuilderStep::DeclarativeFilter { steps, .. }
158 | BuilderStep::DeclarativeSplit { steps, .. }
159 | BuilderStep::DeclarativeStreamSplit { steps, .. }
160 | BuilderStep::Split { steps, .. }
161 | BuilderStep::Filter { steps, .. }
162 | BuilderStep::Multicast { steps, .. }
163 | BuilderStep::Throttle { steps, .. }
164 | BuilderStep::LoadBalance { steps, .. }
165 | BuilderStep::Loop { steps, .. }
166 | BuilderStep::DeclarativeLoop { steps, .. }
167 | BuilderStep::IdempotentConsumer { steps, .. } => {
168 walk_steps(steps, uris, dynamic_dispatch);
169 }
170
171 BuilderStep::DeclarativeChoice { whens, otherwise } => {
174 for when in whens {
175 walk_steps(&when.steps, uris, dynamic_dispatch);
176 }
177 if let Some(steps) = otherwise {
178 walk_steps(steps, uris, dynamic_dispatch);
179 }
180 }
181 BuilderStep::Choice { whens, otherwise } => {
182 for when in whens {
183 walk_steps(&when.steps, uris, dynamic_dispatch);
184 }
185 if let Some(steps) = otherwise {
186 walk_steps(steps, uris, dynamic_dispatch);
187 }
188 }
189
190 BuilderStep::Cache { on_miss, .. } => {
191 walk_steps(on_miss, uris, dynamic_dispatch);
192 }
193
194 BuilderStep::DeclarativeDoTry {
195 try_steps,
196 catch,
197 finally,
198 } => {
199 walk_steps(try_steps, uris, dynamic_dispatch);
200 for clause in catch {
201 walk_steps(&clause.steps, uris, dynamic_dispatch);
202 }
203 if let Some(finally) = finally {
204 walk_steps(&finally.steps, uris, dynamic_dispatch);
205 }
206 }
207
208 BuilderStep::Processor(_)
210 | BuilderStep::Stop
211 | BuilderStep::Log { .. }
212 | BuilderStep::DeclarativeSetHeader { .. }
213 | BuilderStep::DeclarativeSetHeaderIfAbsent { .. }
214 | BuilderStep::DeclarativeRemoveHeader { .. }
215 | BuilderStep::DeclarativeSetProperty { .. }
216 | BuilderStep::DeclarativeSetBody { .. }
217 | BuilderStep::DeclarativeScript { .. }
218 | BuilderStep::DeclarativeFunction { .. }
219 | BuilderStep::Aggregate { .. }
220 | BuilderStep::DeclarativeLog { .. }
221 | BuilderStep::Bean { .. }
222 | BuilderStep::Script { .. }
223 | BuilderStep::Delay { .. }
224 | BuilderStep::Validate { .. }
225 | BuilderStep::ClaimCheck { .. }
226 | BuilderStep::Sampling { .. }
227 | BuilderStep::Sort { .. }
228 | BuilderStep::CacheInvalidate { .. }
229 | BuilderStep::CacheClear { .. }
230 | BuilderStep::CacheStats { .. }
231 | BuilderStep::CachePeekStale { .. }
232 | BuilderStep::Resequence { .. } => {}
233 }
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use std::sync::Arc;
240
241 use super::{DocumentInputs, Tier, derive_tier};
242 use camel_api::error_handler::ErrorHandlerConfig;
243 use camel_api::recipient_list::RecipientListConfig;
244 use camel_api::{DynamicRouterConfig, RoutingSlipConfig};
245 use camel_core::intercept::InterceptAction;
246 use camel_core::{BuilderStep, RouteDefinition};
247
248 fn unit_schemes() -> Vec<String> {
251 vec!["direct".to_string(), "mock".to_string()]
252 }
253
254 fn route(from: &str, steps: Vec<BuilderStep>) -> RouteDefinition {
255 RouteDefinition::new(from, steps)
256 }
257
258 fn lean_route() -> RouteDefinition {
259 route("direct:start", vec![BuilderStep::To("mock:out".into())])
260 }
261
262 fn inputs<'a>(
263 has_scenario: bool,
264 intercepts: &'a [(String, InterceptAction)],
265 schemes: &'a [String],
266 ) -> DocumentInputs<'a> {
267 DocumentInputs {
268 has_scenario,
269 intercepts,
270 unit_schemes: schemes,
271 }
272 }
273
274 #[test]
275 fn tier_lean_document_stays_lean() {
276 let routes = [lean_route()];
277 let schemes = unit_schemes();
278 let intercepts = Vec::new();
279 let input = inputs(false, &intercepts, &schemes);
280 assert_eq!(derive_tier(&routes, &input), Tier::Lean);
281 }
282
283 #[test]
284 fn tier_skipto_subtracts_from_closure() {
285 let routes = [route(
286 "direct:start",
287 vec![BuilderStep::To("kafka:orders".into())],
288 )];
289 let schemes = unit_schemes();
290 let intercepts = vec![(
291 "kafka:orders".to_string(),
292 InterceptAction::SkipTo {
293 uri: "mock:orders".into(),
294 },
295 )];
296 let input = inputs(false, &intercepts, &schemes);
297 assert_eq!(derive_tier(&routes, &input), Tier::Lean);
298
299 let mismatched = vec![(
303 "kafka:orders?option=1".to_string(),
304 InterceptAction::SkipTo {
305 uri: "mock:orders".into(),
306 },
307 )];
308 let input = inputs(false, &mismatched, &schemes);
309 assert_eq!(derive_tier(&routes, &input), Tier::Full);
310 }
311
312 #[test]
313 fn tier_dlc_uri_counts_in_closure() {
314 let schemes = unit_schemes();
315 let intercepts = Vec::new();
316 let kafka_dlq = [
320 route("direct:start", vec![BuilderStep::To("mock:out".into())])
321 .with_error_handler(ErrorHandlerConfig::dead_letter_channel("kafka:dlq")),
322 ];
323 let input = inputs(false, &intercepts, &schemes);
324 assert_eq!(derive_tier(&kafka_dlq, &input), Tier::Full);
325
326 let placeholder_dlq = [
330 route("direct:start", vec![BuilderStep::To("mock:out".into())])
331 .with_error_handler(ErrorHandlerConfig::dead_letter_channel("${env:DLQ}:dead")),
332 ];
333 let input = inputs(false, &intercepts, &schemes);
334 assert_eq!(derive_tier(&placeholder_dlq, &input), Tier::Full);
335
336 let mock_dlq = [
340 route("direct:start", vec![BuilderStep::To("mock:out".into())])
341 .with_error_handler(ErrorHandlerConfig::dead_letter_channel("mock:dlc")),
342 ];
343 let input = inputs(false, &intercepts, &schemes);
344 assert_eq!(derive_tier(&mock_dlq, &input), Tier::Lean);
345 }
346
347 #[test]
348 fn tier_divertcopyto_does_not_subtract() {
349 let routes = [route(
350 "direct:start",
351 vec![BuilderStep::To("kafka:orders".into())],
352 )];
353 let schemes = unit_schemes();
354 let intercepts = vec![(
355 "kafka:orders".to_string(),
356 InterceptAction::DivertCopyTo {
357 uri: "mock:mirror".into(),
358 },
359 )];
360 let input = inputs(false, &intercepts, &schemes);
361 assert_eq!(derive_tier(&routes, &input), Tier::Full);
362 }
363
364 #[test]
365 fn tier_placeholder_in_scheme_forces_full() {
366 let routes = [route(
367 "direct:start",
368 vec![BuilderStep::To("${env:TARGET_SCHEME}:host".into())],
369 )];
370 let schemes = unit_schemes();
371 let intercepts = Vec::new();
372 let input = inputs(false, &intercepts, &schemes);
373 assert_eq!(derive_tier(&routes, &input), Tier::Full);
374 }
375
376 #[test]
377 fn tier_dynamic_dispatch_forces_full() {
378 let schemes = unit_schemes();
379 let intercepts = Vec::new();
380 let cases: [(&str, BuilderStep); 4] = [
384 (
385 "recipient_list",
386 BuilderStep::RecipientList {
387 config: RecipientListConfig::new(Arc::new(|_| "mock:one".to_string())),
388 },
389 ),
390 (
391 "routing_slip",
392 BuilderStep::RoutingSlip {
393 config: RoutingSlipConfig::new(Arc::new(|_| Some("mock:one".to_string()))),
394 },
395 ),
396 (
397 "dynamic_router",
398 BuilderStep::DynamicRouter {
399 config: DynamicRouterConfig::new(Arc::new(|_| Some("mock:one".to_string()))),
400 },
401 ),
402 ("to_d", BuilderStep::To("${env:SCHEME}:orders".into())),
403 ];
404 for (name, step) in cases {
405 let routes = [route("direct:start", vec![step])];
406 let input = inputs(false, &intercepts, &schemes);
407 assert_eq!(derive_tier(&routes, &input), Tier::Full, "case {name}");
408 }
409 }
410
411 #[test]
412 fn tier_scenario_section_forces_full() {
413 let routes = [lean_route()];
414 let schemes = unit_schemes();
415 let intercepts = Vec::new();
416 let input = inputs(true, &intercepts, &schemes);
417 assert_eq!(derive_tier(&routes, &input), Tier::Full);
418 }
419
420 #[test]
421 fn tier_all_route_sources_count() {
422 let schemes = unit_schemes();
423 let intercepts = Vec::new();
424 for source in ["inline", "routeFilesFromRoot"] {
429 let all_lean = [
430 lean_route(),
431 route("direct:poll", vec![BuilderStep::To("seda:pool".into())]),
432 ];
433 let input = inputs(false, &intercepts, &schemes);
434 assert_eq!(
435 derive_tier(&all_lean, &input),
436 Tier::Lean,
437 "source {source}"
438 );
439
440 let one_full = [
441 lean_route(),
442 route("direct:ship", vec![BuilderStep::To("kafka:orders".into())]),
443 ];
444 let input = inputs(false, &intercepts, &schemes);
445 assert_eq!(
446 derive_tier(&one_full, &input),
447 Tier::Full,
448 "source {source}"
449 );
450 }
451 }
452}