camel_integration_test/env_layers.rs
1//! Layered hermetic environment source (ADR-0069 §4).
2//!
3//! Resolution order for [`LayeredEnv::lookup`]: harness-provisioned
4//! bindings (the `bindVar` keys of `provisioning: harness` endpoints,
5//! guaranteed absent from the document `env` map by the parser's
6//! reserved-key rule), then the document `env` map, then the injected
7//! ambient lookup — but only for keys listed in the passthrough
8//! allowlist. Everything else resolves to `None`.
9//!
10//! Hermeticity contract: nothing in this crate writes the process
11//! environment, and the only ambient reads go through the closure
12//! injected at construction. Production callers pass [`ambient_std`];
13//! tests inject map closures.
14
15use std::collections::BTreeMap;
16use std::sync::Arc;
17
18/// Ambient lookup closure: maps a variable name to its value.
19pub type AmbientLookup = Arc<dyn Fn(&str) -> Option<String> + Send + Sync>;
20
21pub struct LayeredEnv {
22 doc: BTreeMap<String, String>,
23 harness_provisioned: BTreeMap<String, String>,
24 passthrough: Vec<String>,
25 ambient: AmbientLookup,
26}
27
28impl LayeredEnv {
29 pub fn new(
30 doc: BTreeMap<String, String>,
31 harness_provisioned: BTreeMap<String, String>,
32 passthrough: Vec<String>,
33 ambient: AmbientLookup,
34 ) -> Self {
35 Self {
36 doc,
37 harness_provisioned,
38 passthrough,
39 ambient,
40 }
41 }
42
43 /// Resolves `key` through the layer precedence: harness-provisioned
44 /// bindings first, then the document `env` map, then the injected
45 /// ambient lookup iff `key` is listed in the passthrough allowlist.
46 pub fn lookup(&self, key: &str) -> Option<String> {
47 if let Some(value) = self.harness_provisioned.get(key) {
48 return Some(value.clone());
49 }
50 if let Some(value) = self.doc.get(key) {
51 return Some(value.clone());
52 }
53 if self.passthrough.iter().any(|allowed| allowed == key) {
54 return (self.ambient)(key);
55 }
56 None
57 }
58
59 /// Returns a copy of this environment with one extra
60 /// harness-provisioned binding layered on top: `lookup(key)`
61 /// resolves `value` with the same precedence as partner bindVars
62 /// (harness layer wins over the document `env` map). The receiver
63 /// is untouched — `boot_scenario` derives the extended discovery
64 /// environment from the caller's env for the inbound listener's
65 /// bindVar (rc-5yon), so route-file `${env:NAME}` templates resolve
66 /// to the staged socket.
67 pub fn with_harness_var(&self, key: &str, value: String) -> LayeredEnv {
68 let mut harness_provisioned = self.harness_provisioned.clone();
69 harness_provisioned.insert(key.to_string(), value);
70 LayeredEnv {
71 doc: self.doc.clone(),
72 harness_provisioned,
73 passthrough: self.passthrough.clone(),
74 ambient: Arc::clone(&self.ambient),
75 }
76 }
77}
78
79/// Wires `std::env::var` as the ambient lookup for production callers.
80pub fn ambient_std() -> AmbientLookup {
81 Arc::new(|key| std::env::var(key).ok())
82}