camel_core/
startup_validation.rs1use crate::lifecycle::application::route_definition::{BuilderStep, RouteDefinition};
9use camel_api::{CamelError, ConfigValidationError};
10use camel_endpoint::uri::parse_bool_param;
11
12pub trait ConfigCheck: Send + Sync {
20 fn name(&self) -> &'static str;
23
24 fn description(&self) -> &'static str;
26
27 fn run(&self) -> Result<(), CamelError>;
29}
30
31#[derive(Debug, Default, Clone)]
36pub struct StartupValidationReport {
37 pub failures: Vec<String>,
39}
40
41impl StartupValidationReport {
42 pub fn is_ok(&self) -> bool {
45 self.failures.is_empty()
46 }
47}
48
49pub fn run_startup_validation(
54 checks: Vec<Box<dyn ConfigCheck>>,
55) -> Result<StartupValidationReport, CamelError> {
56 let mut report = StartupValidationReport::default();
57 for check in &checks {
58 if let Err(e) = check.run() {
59 report.failures.push(format!("{}: {}", check.name(), e));
60 }
61 }
62 if !report.is_ok() {
63 return Err(CamelError::Config(report.failures.join("; ")));
64 }
65 Ok(report)
66}
67
68pub struct SqlDynamicQueryCheck {
75 pub use_message_body_for_sql: bool,
76 pub allow_dynamic_query: bool,
77}
78
79impl ConfigCheck for SqlDynamicQueryCheck {
80 fn name(&self) -> &'static str {
81 "sql-dynamic-query"
82 }
83 fn description(&self) -> &'static str {
84 "SQL use_message_body_for_sql requires allow_dynamic_query=true"
85 }
86 fn run(&self) -> Result<(), CamelError> {
87 if self.use_message_body_for_sql && !self.allow_dynamic_query {
88 return Err(CamelError::from(
89 ConfigValidationError::SqlDynamicQueryWithoutAllowDynamic,
90 ));
91 }
92 Ok(())
93 }
94}
95
96pub fn scan_route_definitions_for_sql_checks(
111 routes: &[RouteDefinition],
112) -> Vec<Box<dyn ConfigCheck>> {
113 let mut out: Vec<Box<dyn ConfigCheck>> = Vec::new();
114 for route in routes {
115 collect_sql_checks_for_uri(route.from_uri(), &mut out);
116 for step in route.steps() {
117 walk_step_uris(step, &mut out);
118 }
119 }
120 out
121}
122
123fn collect_sql_checks_for_uri(uri: &str, out: &mut Vec<Box<dyn ConfigCheck>>) {
124 let Ok(parts) = camel_endpoint::parse_uri(uri) else {
125 return;
126 };
127 if parts.scheme != "sql" {
128 return;
129 }
130 let use_body = parts
134 .params
135 .get("useMessageBodyForSql")
136 .and_then(|v| parse_bool_param(v).ok())
137 .unwrap_or(false);
138 let allow_dynamic = parts
139 .params
140 .get("allowDynamicQuery")
141 .and_then(|v| parse_bool_param(v).ok())
142 .unwrap_or(false);
143 if use_body || allow_dynamic {
144 out.push(Box::new(SqlDynamicQueryCheck {
148 use_message_body_for_sql: use_body,
149 allow_dynamic_query: allow_dynamic,
150 }));
151 }
152}
153
154fn walk_step_uris(step: &BuilderStep, out: &mut Vec<Box<dyn ConfigCheck>>) {
155 match step {
156 BuilderStep::To(uri) => collect_sql_checks_for_uri(uri, out),
157 BuilderStep::WireTap { uri } | BuilderStep::Enrich { uri, .. } => {
158 collect_sql_checks_for_uri(uri, out);
159 }
160 BuilderStep::PollEnrich { uri, .. } => {
161 collect_sql_checks_for_uri(uri, out);
162 }
163 BuilderStep::Filter { steps, .. }
164 | BuilderStep::DeclarativeFilter { steps, .. }
165 | BuilderStep::Split { steps, .. }
166 | BuilderStep::DeclarativeSplit { steps, .. }
167 | BuilderStep::DeclarativeStreamSplit { steps, .. }
168 | BuilderStep::Multicast { steps, .. }
169 | BuilderStep::Throttle { steps, .. }
170 | BuilderStep::LoadBalance { steps, .. }
171 | BuilderStep::Loop { steps, .. }
172 | BuilderStep::DeclarativeLoop { steps, .. }
173 | BuilderStep::IdempotentConsumer { steps, .. } => {
174 for s in steps {
175 walk_step_uris(s, out);
176 }
177 }
178 BuilderStep::Choice { whens, otherwise } => {
179 for when in whens {
180 for s in &when.steps {
181 walk_step_uris(s, out);
182 }
183 }
184 if let Some(ow) = otherwise {
185 for s in ow {
186 walk_step_uris(s, out);
187 }
188 }
189 }
190 BuilderStep::DeclarativeChoice { whens, otherwise } => {
191 for when in whens {
192 for s in &when.steps {
193 walk_step_uris(s, out);
194 }
195 }
196 if let Some(ow) = otherwise {
197 for s in ow {
198 walk_step_uris(s, out);
199 }
200 }
201 }
202 BuilderStep::DeclarativeDoTry {
203 try_steps,
204 catch,
205 finally,
206 } => {
207 for s in try_steps {
208 walk_step_uris(s, out);
209 }
210 for clause in catch {
211 for s in &clause.steps {
212 walk_step_uris(s, out);
213 }
214 }
215 if let Some(fin) = finally {
216 for s in &fin.steps {
217 walk_step_uris(s, out);
218 }
219 }
220 }
221 _ => {}
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
234 fn empty_registry_returns_empty_ok_report() {
235 let report = run_startup_validation(vec![]).expect("empty registry must return Ok");
236 assert!(report.is_ok());
237 assert!(report.failures.is_empty());
238 }
239
240 #[test]
244 fn trait_is_object_safe_and_dispatchable() {
245 struct AlwaysOk;
246 impl ConfigCheck for AlwaysOk {
247 fn name(&self) -> &'static str {
248 "always-ok"
249 }
250 fn description(&self) -> &'static str {
251 "always-ok skeleton check"
252 }
253 fn run(&self) -> Result<(), CamelError> {
254 Ok(())
255 }
256 }
257
258 let check: Box<dyn ConfigCheck> = Box::new(AlwaysOk);
259 assert_eq!(check.name(), "always-ok");
260 assert!(check.run().is_ok());
261 }
262
263 #[test]
267 fn sql_dynamic_query_check_refuses_startup() {
268 let check = SqlDynamicQueryCheck {
269 use_message_body_for_sql: true,
270 allow_dynamic_query: false,
271 };
272 let result = run_startup_validation(vec![Box::new(check)]);
273 match result {
274 Err(CamelError::Config(msg)) => {
275 assert!(msg.contains("sql-dynamic-query"));
276 assert!(msg.contains("allow_dynamic_query"));
277 }
278 other => panic!("expected CamelError::Config, got {other:?}"),
279 }
280 }
281
282 #[test]
289 fn sql_dynamic_query_check_run_returns_typed_error() {
290 let check = SqlDynamicQueryCheck {
291 use_message_body_for_sql: true,
292 allow_dynamic_query: false,
293 };
294 let result = check.run();
295 assert!(
296 matches!(
297 result,
298 Err(CamelError::ConfigValidation(
299 ConfigValidationError::SqlDynamicQueryWithoutAllowDynamic,
300 ))
301 ),
302 "expected ConfigValidation(SqlDynamicQueryWithoutAllowDynamic), got: {result:?}"
303 );
304 }
305
306 #[test]
308 fn sql_dynamic_query_check_passes_with_opt_in() {
309 let check = SqlDynamicQueryCheck {
310 use_message_body_for_sql: true,
311 allow_dynamic_query: true,
312 };
313 let report =
314 run_startup_validation(vec![Box::new(check)]).expect("opt-in must satisfy the check");
315 assert!(report.is_ok());
316 }
317
318 #[test]
321 fn sql_dynamic_query_check_passes_without_body_sql() {
322 let check = SqlDynamicQueryCheck {
323 use_message_body_for_sql: false,
324 allow_dynamic_query: false,
325 };
326 let report = run_startup_validation(vec![Box::new(check)])
327 .expect("no body-sourced queries → no fail-closed condition");
328 assert!(report.is_ok());
329 }
330
331 #[test]
335 fn scanner_flags_from_sql_endpoint_with_body_but_no_allow() {
336 let route = RouteDefinition::new(
337 "sql:select * from t?db_url=postgres://x/y&useMessageBodyForSql=true",
338 vec![],
339 )
340 .with_route_id("r".to_string());
341 let checks = scan_route_definitions_for_sql_checks(&[route]);
342 assert_eq!(checks.len(), 1);
343 assert!(run_startup_validation(checks).is_err());
344 }
345
346 #[test]
349 fn scanner_walks_top_level_to_sql_step() {
350 let route = RouteDefinition::new(
351 "direct:start",
352 vec![BuilderStep::To(
353 "sql:select 1?db_url=postgres://x/y&useMessageBodyForSql=true".to_string(),
354 )],
355 )
356 .with_route_id("r".to_string());
357 let checks = scan_route_definitions_for_sql_checks(&[route]);
358 assert_eq!(checks.len(), 1);
359 assert!(run_startup_validation(checks).is_err());
360 }
361
362 #[test]
364 fn scanner_emits_nothing_for_non_sql_route() {
365 let route = RouteDefinition::new("timer:tick?period=1000", vec![]);
366 let checks = scan_route_definitions_for_sql_checks(&[route]);
367 assert!(checks.is_empty());
368 }
369
370 #[test]
374 fn scanner_skips_sql_endpoint_without_dynamic_intent() {
375 let route = RouteDefinition::new("sql:select 1?db_url=postgres://x/y", vec![]);
376 let checks = scan_route_definitions_for_sql_checks(&[route]);
377 assert!(checks.is_empty());
378 }
379}