cratefield_core/
config.rs1use std::fmt;
9
10use crate::ports::signer::Kid;
11use crate::signer::{HmacSigner, KeyRing, MIN_SECRET_BYTES};
12use crate::venture::VentureEnv;
13
14pub trait Config: Send + Sync {
20 fn get(&self, key: &str) -> Option<String>;
21}
22
23#[derive(Debug, Default, Clone, Copy)]
25pub struct EmptyConfig;
26
27impl Config for EmptyConfig {
28 fn get(&self, _key: &str) -> Option<String> {
29 None
30 }
31}
32
33#[derive(Debug, Default, Clone)]
36pub struct ConfigError {
37 pub problems: Vec<String>,
39}
40
41impl ConfigError {
42 pub fn new() -> Self {
43 Self::default()
44 }
45
46 pub fn push(&mut self, problem: impl Into<String>) {
47 self.problems.push(problem.into());
48 }
49
50 pub fn is_empty(&self) -> bool {
51 self.problems.is_empty()
52 }
53
54 pub fn into_result(self) -> Result<(), Self> {
60 if self.is_empty() { Ok(()) } else { Err(self) }
61 }
62}
63
64impl fmt::Display for ConfigError {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 write!(f, "invalid harness configuration:")?;
67 for problem in &self.problems {
68 write!(f, "\n - {problem}")?;
69 }
70 Ok(())
71 }
72}
73
74impl std::error::Error for ConfigError {}
75
76pub struct ModuleConfig<'a> {
99 prefix: String,
100 config: &'a dyn Config,
101}
102
103fn screaming_snake(name: &str) -> String {
104 let mut out = String::with_capacity(name.len() + 8);
105 for ch in name.chars() {
106 if ch == '-' || ch == '_' {
107 out.push('_');
108 } else {
109 out.extend(ch.to_uppercase());
110 }
111 }
112 out
113}
114
115impl<'a> ModuleConfig<'a> {
116 pub fn new(module_name: &str, config: &'a dyn Config) -> Self {
117 Self {
118 prefix: screaming_snake(module_name),
119 config,
120 }
121 }
122
123 pub fn key(&self, suffix: &str) -> String {
125 format!("{}_{}", self.prefix, screaming_snake(suffix))
126 }
127
128 pub fn get_str(&self, key_suffix: &str, default: &str) -> String {
129 self.config
130 .get(&self.key(key_suffix))
131 .unwrap_or_else(|| default.to_string())
132 }
133
134 pub fn get_u32(&self, key_suffix: &str, default: u32) -> u32 {
135 self.config
136 .get(&self.key(key_suffix))
137 .and_then(|raw| raw.parse().ok())
138 .unwrap_or(default)
139 }
140
141 pub fn get_bool(&self, key_suffix: &str, default: bool) -> bool {
142 match self.config.get(&self.key(key_suffix)) {
143 Some(raw) => matches!(
144 raw.to_ascii_lowercase().as_str(),
145 "1" | "true" | "yes" | "on"
146 ),
147 None => default,
148 }
149 }
150
151 pub fn get_opt(&self, key_suffix: &str) -> Option<String> {
153 self.config.get(&self.key(key_suffix))
154 }
155}
156
157#[derive(Debug, Clone)]
170pub struct HarnessConfig {
171 pub harness_secret: String,
172 pub harness_secret_previous: Option<String>,
173 pub harness_secret_revoked: Vec<String>,
176 pub harness_venture: Option<String>,
178 pub admin_token: Option<String>,
179 pub env: VentureEnv,
180}
181
182impl HarnessConfig {
183 pub fn from_config(config: &dyn Config) -> Result<Self, ConfigError> {
188 let mut errors = ConfigError::default();
189
190 let harness_secret = match config.get("HARNESS_SECRET") {
191 Some(secret) if secret.len() >= MIN_SECRET_BYTES => Some(secret),
192 Some(_) => {
193 errors.push(format!(
194 "HARNESS_SECRET must be at least {MIN_SECRET_BYTES} bytes"
195 ));
196 None
197 }
198 None => {
199 errors.push(format!(
200 "HARNESS_SECRET is required (min {MIN_SECRET_BYTES} bytes)"
201 ));
202 None
203 }
204 };
205 let env = match config.get("ENV").as_deref() {
206 None | Some("") => Some(VentureEnv::Development),
207 Some(raw) => {
208 let parsed = VentureEnv::parse(raw);
209 if parsed.is_none() {
210 errors.push(format!(
211 "ENV must be one of development|staging|production, got {raw:?}"
212 ));
213 }
214 parsed
215 }
216 };
217
218 let harness_secret_revoked: Vec<String> = config
222 .get("HARNESS_SECRET_REVOKED")
223 .map(|raw| {
224 raw.split(',')
225 .map(str::trim)
226 .filter(|kid| !kid.is_empty() && *kid != "cur")
227 .map(<str>::to_owned)
228 .collect()
229 })
230 .unwrap_or_default();
231
232 errors.into_result()?;
233 Ok(Self {
234 harness_secret: harness_secret.unwrap_or_default(),
235 harness_secret_previous: config.get("HARNESS_SECRET_PREVIOUS"),
236 harness_secret_revoked,
237 harness_venture: config
238 .get("HARNESS_VENTURE")
239 .map(|value| value.trim().to_owned())
240 .filter(|value| !value.is_empty()),
241 admin_token: config.get("ADMIN_TOKEN"),
242 env: env.unwrap_or_default(),
243 })
244 }
245
246 pub fn signer(&self) -> HmacSigner {
258 let mut ring = KeyRing::new();
259 ring.rotate_signing(Kid::Cur, self.harness_secret.clone().into_bytes())
260 .expect("from_config validated the secret");
261 if let Some(previous) = &self.harness_secret_previous {
262 let _ = ring.add_verifying_only(Kid::Prev, previous.clone().into_bytes());
263 }
264 for kid in &self.harness_secret_revoked {
265 let key = match kid.as_str() {
266 "prev" => Kid::Prev,
267 other => Kid::named(other.to_owned()),
268 };
269 ring.revoke(&key);
270 }
271 HmacSigner::from_ring(ring).with_binding(
272 self.harness_venture.clone(),
273 Some(self.env.as_str().to_owned()),
274 )
275 }
276}
277
278#[derive(Debug, Clone, Default)]
280pub struct MapConfig(pub std::collections::HashMap<String, String>);
281
282impl MapConfig {
283 pub fn from_pairs(
284 pairs: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
285 ) -> Self {
286 Self(
287 pairs
288 .into_iter()
289 .map(|(k, v)| (k.into(), v.into()))
290 .collect(),
291 )
292 }
293}
294
295impl Config for MapConfig {
296 fn get(&self, key: &str) -> Option<String> {
297 self.0.get(key).cloned()
298 }
299}