Skip to main content

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}