use crate::lifecycle::application::route_definition::{BuilderStep, RouteDefinition};
use camel_api::{CamelError, ConfigValidationError};
use camel_endpoint::uri::parse_bool_param;
pub trait ConfigCheck: Send + Sync {
fn name(&self) -> &'static str;
fn description(&self) -> &'static str;
fn run(&self) -> Result<(), CamelError>;
}
#[derive(Debug, Default, Clone)]
pub struct StartupValidationReport {
pub failures: Vec<String>,
}
impl StartupValidationReport {
pub fn is_ok(&self) -> bool {
self.failures.is_empty()
}
}
pub fn run_startup_validation(
checks: Vec<Box<dyn ConfigCheck>>,
) -> Result<StartupValidationReport, CamelError> {
let mut report = StartupValidationReport::default();
for check in &checks {
if let Err(e) = check.run() {
report.failures.push(format!("{}: {}", check.name(), e));
}
}
if !report.is_ok() {
return Err(CamelError::Config(report.failures.join("; ")));
}
Ok(report)
}
pub struct SqlDynamicQueryCheck {
pub use_message_body_for_sql: bool,
pub allow_dynamic_query: bool,
}
impl ConfigCheck for SqlDynamicQueryCheck {
fn name(&self) -> &'static str {
"sql-dynamic-query"
}
fn description(&self) -> &'static str {
"SQL use_message_body_for_sql requires allow_dynamic_query=true"
}
fn run(&self) -> Result<(), CamelError> {
if self.use_message_body_for_sql && !self.allow_dynamic_query {
return Err(CamelError::from(
ConfigValidationError::SqlDynamicQueryWithoutAllowDynamic,
));
}
Ok(())
}
}
pub fn scan_route_definitions_for_sql_checks(
routes: &[RouteDefinition],
) -> Vec<Box<dyn ConfigCheck>> {
let mut out: Vec<Box<dyn ConfigCheck>> = Vec::new();
for route in routes {
collect_sql_checks_for_uri(route.from_uri(), &mut out);
for step in route.steps() {
walk_step_uris(step, &mut out);
}
}
out
}
fn collect_sql_checks_for_uri(uri: &str, out: &mut Vec<Box<dyn ConfigCheck>>) {
let Ok(parts) = camel_endpoint::parse_uri(uri) else {
return;
};
if parts.scheme != "sql" {
return;
}
let use_body = parts
.params
.get("useMessageBodyForSql")
.and_then(|v| parse_bool_param(v).ok())
.unwrap_or(false);
let allow_dynamic = parts
.params
.get("allowDynamicQuery")
.and_then(|v| parse_bool_param(v).ok())
.unwrap_or(false);
if use_body || allow_dynamic {
out.push(Box::new(SqlDynamicQueryCheck {
use_message_body_for_sql: use_body,
allow_dynamic_query: allow_dynamic,
}));
}
}
fn walk_step_uris(step: &BuilderStep, out: &mut Vec<Box<dyn ConfigCheck>>) {
match step {
BuilderStep::To(uri) => collect_sql_checks_for_uri(uri, out),
BuilderStep::WireTap { uri } | BuilderStep::Enrich { uri, .. } => {
collect_sql_checks_for_uri(uri, out);
}
BuilderStep::PollEnrich { uri, .. } => {
collect_sql_checks_for_uri(uri, out);
}
BuilderStep::Filter { steps, .. }
| BuilderStep::DeclarativeFilter { steps, .. }
| BuilderStep::Split { steps, .. }
| BuilderStep::DeclarativeSplit { steps, .. }
| BuilderStep::DeclarativeStreamSplit { steps, .. }
| BuilderStep::Multicast { steps, .. }
| BuilderStep::Throttle { steps, .. }
| BuilderStep::LoadBalance { steps, .. }
| BuilderStep::Loop { steps, .. }
| BuilderStep::DeclarativeLoop { steps, .. }
| BuilderStep::IdempotentConsumer { steps, .. } => {
for s in steps {
walk_step_uris(s, out);
}
}
BuilderStep::Choice { whens, otherwise } => {
for when in whens {
for s in &when.steps {
walk_step_uris(s, out);
}
}
if let Some(ow) = otherwise {
for s in ow {
walk_step_uris(s, out);
}
}
}
BuilderStep::DeclarativeChoice { whens, otherwise } => {
for when in whens {
for s in &when.steps {
walk_step_uris(s, out);
}
}
if let Some(ow) = otherwise {
for s in ow {
walk_step_uris(s, out);
}
}
}
BuilderStep::DeclarativeDoTry {
try_steps,
catch,
finally,
} => {
for s in try_steps {
walk_step_uris(s, out);
}
for clause in catch {
for s in &clause.steps {
walk_step_uris(s, out);
}
}
if let Some(fin) = finally {
for s in &fin.steps {
walk_step_uris(s, out);
}
}
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_registry_returns_empty_ok_report() {
let report = run_startup_validation(vec![]).expect("empty registry must return Ok");
assert!(report.is_ok());
assert!(report.failures.is_empty());
}
#[test]
fn trait_is_object_safe_and_dispatchable() {
struct AlwaysOk;
impl ConfigCheck for AlwaysOk {
fn name(&self) -> &'static str {
"always-ok"
}
fn description(&self) -> &'static str {
"always-ok skeleton check"
}
fn run(&self) -> Result<(), CamelError> {
Ok(())
}
}
let check: Box<dyn ConfigCheck> = Box::new(AlwaysOk);
assert_eq!(check.name(), "always-ok");
assert!(check.run().is_ok());
}
#[test]
fn sql_dynamic_query_check_refuses_startup() {
let check = SqlDynamicQueryCheck {
use_message_body_for_sql: true,
allow_dynamic_query: false,
};
let result = run_startup_validation(vec![Box::new(check)]);
match result {
Err(CamelError::Config(msg)) => {
assert!(msg.contains("sql-dynamic-query"));
assert!(msg.contains("allow_dynamic_query"));
}
other => panic!("expected CamelError::Config, got {other:?}"),
}
}
#[test]
fn sql_dynamic_query_check_run_returns_typed_error() {
let check = SqlDynamicQueryCheck {
use_message_body_for_sql: true,
allow_dynamic_query: false,
};
let result = check.run();
assert!(
matches!(
result,
Err(CamelError::ConfigValidation(
ConfigValidationError::SqlDynamicQueryWithoutAllowDynamic,
))
),
"expected ConfigValidation(SqlDynamicQueryWithoutAllowDynamic), got: {result:?}"
);
}
#[test]
fn sql_dynamic_query_check_passes_with_opt_in() {
let check = SqlDynamicQueryCheck {
use_message_body_for_sql: true,
allow_dynamic_query: true,
};
let report =
run_startup_validation(vec![Box::new(check)]).expect("opt-in must satisfy the check");
assert!(report.is_ok());
}
#[test]
fn sql_dynamic_query_check_passes_without_body_sql() {
let check = SqlDynamicQueryCheck {
use_message_body_for_sql: false,
allow_dynamic_query: false,
};
let report = run_startup_validation(vec![Box::new(check)])
.expect("no body-sourced queries → no fail-closed condition");
assert!(report.is_ok());
}
#[test]
fn scanner_flags_from_sql_endpoint_with_body_but_no_allow() {
let route = RouteDefinition::new(
"sql:select * from t?db_url=postgres://x/y&useMessageBodyForSql=true",
vec![],
)
.with_route_id("r".to_string());
let checks = scan_route_definitions_for_sql_checks(&[route]);
assert_eq!(checks.len(), 1);
assert!(run_startup_validation(checks).is_err());
}
#[test]
fn scanner_walks_top_level_to_sql_step() {
let route = RouteDefinition::new(
"direct:start",
vec![BuilderStep::To(
"sql:select 1?db_url=postgres://x/y&useMessageBodyForSql=true".to_string(),
)],
)
.with_route_id("r".to_string());
let checks = scan_route_definitions_for_sql_checks(&[route]);
assert_eq!(checks.len(), 1);
assert!(run_startup_validation(checks).is_err());
}
#[test]
fn scanner_emits_nothing_for_non_sql_route() {
let route = RouteDefinition::new("timer:tick?period=1000", vec![]);
let checks = scan_route_definitions_for_sql_checks(&[route]);
assert!(checks.is_empty());
}
#[test]
fn scanner_skips_sql_endpoint_without_dynamic_intent() {
let route = RouteDefinition::new("sql:select 1?db_url=postgres://x/y", vec![]);
let checks = scan_route_definitions_for_sql_checks(&[route]);
assert!(checks.is_empty());
}
}