cratefield_core/
config.rs1use std::fmt;
9
10use crate::signer::{HmacSigner, MIN_SECRET_BYTES};
11use crate::venture::VentureEnv;
12
13pub trait Config: Send + Sync {
19 fn get(&self, key: &str) -> Option<String>;
20}
21
22#[derive(Debug, Default, Clone, Copy)]
24pub struct EmptyConfig;
25
26impl Config for EmptyConfig {
27 fn get(&self, _key: &str) -> Option<String> {
28 None
29 }
30}
31
32#[derive(Debug, Default, Clone)]
35pub struct ConfigError {
36 pub problems: Vec<String>,
38}
39
40impl ConfigError {
41 pub fn new() -> Self {
42 Self::default()
43 }
44
45 pub fn push(&mut self, problem: impl Into<String>) {
46 self.problems.push(problem.into());
47 }
48
49 pub fn is_empty(&self) -> bool {
50 self.problems.is_empty()
51 }
52
53 pub fn into_result(self) -> Result<(), Self> {
59 if self.is_empty() { Ok(()) } else { Err(self) }
60 }
61}
62
63impl fmt::Display for ConfigError {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 write!(f, "invalid harness configuration:")?;
66 for problem in &self.problems {
67 write!(f, "\n - {problem}")?;
68 }
69 Ok(())
70 }
71}
72
73impl std::error::Error for ConfigError {}
74
75pub struct ModuleConfig<'a> {
98 prefix: String,
99 config: &'a dyn Config,
100}
101
102fn screaming_snake(name: &str) -> String {
103 let mut out = String::with_capacity(name.len() + 8);
104 for ch in name.chars() {
105 if ch == '-' || ch == '_' {
106 out.push('_');
107 } else {
108 out.extend(ch.to_uppercase());
109 }
110 }
111 out
112}
113
114impl<'a> ModuleConfig<'a> {
115 pub fn new(module_name: &str, config: &'a dyn Config) -> Self {
116 Self {
117 prefix: screaming_snake(module_name),
118 config,
119 }
120 }
121
122 pub fn key(&self, suffix: &str) -> String {
124 format!("{}_{}", self.prefix, screaming_snake(suffix))
125 }
126
127 pub fn get_str(&self, key_suffix: &str, default: &str) -> String {
128 self.config
129 .get(&self.key(key_suffix))
130 .unwrap_or_else(|| default.to_string())
131 }
132
133 pub fn get_u32(&self, key_suffix: &str, default: u32) -> u32 {
134 self.config
135 .get(&self.key(key_suffix))
136 .and_then(|raw| raw.parse().ok())
137 .unwrap_or(default)
138 }
139
140 pub fn get_bool(&self, key_suffix: &str, default: bool) -> bool {
141 match self.config.get(&self.key(key_suffix)) {
142 Some(raw) => matches!(
143 raw.to_ascii_lowercase().as_str(),
144 "1" | "true" | "yes" | "on"
145 ),
146 None => default,
147 }
148 }
149
150 pub fn get_opt(&self, key_suffix: &str) -> Option<String> {
152 self.config.get(&self.key(key_suffix))
153 }
154}
155
156#[derive(Debug, Clone)]
164pub struct HarnessConfig {
165 pub harness_secret: String,
166 pub harness_secret_previous: Option<String>,
167 pub admin_token: Option<String>,
168 pub env: VentureEnv,
169}
170
171impl HarnessConfig {
172 pub fn from_config(config: &dyn Config) -> Result<Self, ConfigError> {
177 let mut errors = ConfigError::default();
178
179 let harness_secret = match config.get("HARNESS_SECRET") {
180 Some(secret) if secret.len() >= MIN_SECRET_BYTES => Some(secret),
181 Some(_) => {
182 errors.push(format!(
183 "HARNESS_SECRET must be at least {MIN_SECRET_BYTES} bytes"
184 ));
185 None
186 }
187 None => {
188 errors.push(format!(
189 "HARNESS_SECRET is required (min {MIN_SECRET_BYTES} bytes)"
190 ));
191 None
192 }
193 };
194 let env = match config.get("ENV").as_deref() {
195 None | Some("") => Some(VentureEnv::Development),
196 Some(raw) => {
197 let parsed = VentureEnv::parse(raw);
198 if parsed.is_none() {
199 errors.push(format!(
200 "ENV must be one of development|staging|production, got {raw:?}"
201 ));
202 }
203 parsed
204 }
205 };
206
207 errors.into_result()?;
208 Ok(Self {
209 harness_secret: harness_secret.unwrap_or_default(),
210 harness_secret_previous: config.get("HARNESS_SECRET_PREVIOUS"),
211 admin_token: config.get("ADMIN_TOKEN"),
212 env: env.unwrap_or_default(),
213 })
214 }
215
216 pub fn signer(&self) -> HmacSigner {
222 HmacSigner::new(
223 self.harness_secret.clone(),
224 self.harness_secret_previous.clone(),
225 )
226 .expect("from_config validated the secret")
227 }
228}
229
230#[derive(Debug, Clone, Default)]
232pub struct MapConfig(pub std::collections::HashMap<String, String>);
233
234impl MapConfig {
235 pub fn from_pairs(
236 pairs: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
237 ) -> Self {
238 Self(
239 pairs
240 .into_iter()
241 .map(|(k, v)| (k.into(), v.into()))
242 .collect(),
243 )
244 }
245}
246
247impl Config for MapConfig {
248 fn get(&self, key: &str) -> Option<String> {
249 self.0.get(key).cloned()
250 }
251}