camel_core/startup_validation.rs
1//! ADR-0033 startup-validation phase (skeleton).
2//!
3//! Batch 1 ships the *skeleton* — the public type/trait surface and a stub
4//! `run_startup_validation()` that returns `Ok(())`. Later batches (Batch 5+)
5//! register `ConfigCheck` impls for Require-Explicit-Choice members and wire
6//! `CamelContext::start()` to call the phase. See ADR-0033 in `docs/adr/0033-…`.
7
8use camel_api::CamelError;
9
10/// A single startup-time configuration check.
11///
12/// Each `ConfigCheck` is run as part of the fail-closed startup-validation phase
13/// established by ADR-0033. The check owns a name (for error reporting), a human
14/// description, and a synchronous `run` that returns `Ok(())` if the check passes
15/// or `Err(CamelError::Config)` if the check fails and the operator config is
16/// invalid. Batch 1 ships the trait; later batches register impls.
17pub trait ConfigCheck: Send + Sync {
18 /// Stable identifier for this check (e.g. `"grpc-tls"`, `"sql-dynamic-query"`).
19 /// Used in error messages and `camel doctor` output.
20 fn name(&self) -> &'static str;
21
22 /// One-line human description of what this check enforces.
23 fn description(&self) -> &'static str;
24
25 /// Run the check. Returns `Err(CamelError::Config(_))` to fail-closed.
26 fn run(&self) -> Result<(), CamelError>;
27}
28
29/// Aggregated result of running every registered `ConfigCheck`.
30///
31/// Batch 1 ships the type so later batches can populate `failures`. Skeleton
32/// phase always produces a `StartupValidationReport { failures: vec![] }`.
33#[derive(Debug, Default, Clone)]
34pub struct StartupValidationReport {
35 /// Names of every `ConfigCheck` that returned `Err`, in registration order.
36 pub failures: Vec<String>,
37}
38
39impl StartupValidationReport {
40 /// True when no check failed. Caller MUST refuse to start the runtime when
41 /// this is `false`.
42 pub fn is_ok(&self) -> bool {
43 self.failures.is_empty()
44 }
45}
46
47/// Entry point for the startup-validation phase.
48///
49/// **Batch 1 (skeleton):** returns `Ok(StartupValidationReport::default())`.
50/// No `ConfigCheck` impls are registered yet — the registry is empty by design.
51///
52/// **Later batches:** register `ConfigCheck` impls (gRPC TLS, SQL `allow_dynamic_query`,
53/// aggregator completion bound, …) and convert this function to a registry walk
54/// that fails closed on the first error.
55pub fn run_startup_validation() -> Result<StartupValidationReport, CamelError> {
56 Ok(StartupValidationReport::default())
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 /// Smoke test: the skeleton compiles, exports its public surface, and the
64 /// entry point returns `Ok` with an empty failure list. Later batches will
65 /// replace this with a registry walk; the test will then grow.
66 #[test]
67 fn skeleton_run_returns_empty_ok_report() {
68 let report = run_startup_validation().expect("skeleton must return Ok");
69 assert!(report.is_ok());
70 assert!(report.failures.is_empty());
71 }
72
73 /// Trait smoke test: a trivial `ConfigCheck` impl can be defined by callers
74 /// and dispatched dynamically. This guards the trait surface against
75 /// accidental breaking changes between batches.
76 #[test]
77 fn trait_is_object_safe_and_dispatchable() {
78 struct AlwaysOk;
79 impl ConfigCheck for AlwaysOk {
80 fn name(&self) -> &'static str {
81 "always-ok"
82 }
83 fn description(&self) -> &'static str {
84 "always-ok skeleton check"
85 }
86 fn run(&self) -> Result<(), CamelError> {
87 Ok(())
88 }
89 }
90
91 let check: Box<dyn ConfigCheck> = Box::new(AlwaysOk);
92 assert_eq!(check.name(), "always-ok");
93 assert!(check.run().is_ok());
94 }
95}