1use super::format::ALL_ENVIRONMENTS;
2use super::provider::ProviderCache;
3use super::refs::SecretRef;
4use core::fmt;
5use std::collections::BTreeMap;
6
7pub struct SecretView {
15 environment: String,
16 store: BTreeMap<String, BTreeMap<String, String>>,
17 providers: ProviderCache,
18}
19
20impl fmt::Debug for SecretView {
22 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23 f.debug_struct("SecretView")
24 .field("environment", &self.environment)
25 .field("keys", &self.store.len())
26 .field("namespaces", &self.providers.namespace_count())
27 .finish()
28 }
29}
30
31pub enum Resolution<'a> {
40 Found(&'a str),
42 MissingKey,
46 MissingNamespace,
48}
49
50impl fmt::Debug for Resolution<'_> {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 f.write_str(match self {
54 Self::Found(_) => "Found(..)",
55 Self::MissingKey => "MissingKey",
56 Self::MissingNamespace => "MissingNamespace",
57 })
58 }
59}
60
61impl SecretView {
62 #[must_use]
64 pub fn new(
65 environment: String,
66 store: BTreeMap<String, BTreeMap<String, String>>,
67 providers: ProviderCache,
68 ) -> Self {
69 Self {
70 environment,
71 store,
72 providers,
73 }
74 }
75
76 #[must_use]
80 pub fn empty(environment: String) -> Self {
81 Self::new(environment, BTreeMap::new(), ProviderCache::default())
82 }
83
84 #[must_use]
86 pub fn environment(&self) -> &str {
87 &self.environment
88 }
89
90 #[must_use]
104 pub fn resolve(&self, reference: &SecretRef<'_>) -> Resolution<'_> {
105 let table = match reference.namespace {
106 None => Some(&self.store),
107 Some(namespace) => self.providers.namespace(namespace),
108 };
109 if let Some(value) =
110 table
111 .and_then(|table| table.get(reference.key))
112 .and_then(|by_environment| {
113 by_environment
114 .get(&self.environment)
115 .or_else(|| by_environment.get(ALL_ENVIRONMENTS))
116 })
117 {
118 return Resolution::Found(value.as_str());
119 }
120 match reference.namespace {
121 None => Resolution::MissingKey,
122 Some(namespace) if self.is_pushed(namespace) => Resolution::MissingKey,
123 Some(_) => Resolution::MissingNamespace,
124 }
125 }
126
127 fn is_pushed(&self, namespace: &str) -> bool {
137 self.providers
138 .pushed
139 .get(namespace)
140 .is_some_and(|environments| {
141 environments.contains(&self.environment) || environments.contains(ALL_ENVIRONMENTS)
142 })
143 }
144}
145
146#[cfg(test)]
147mod tests {
148
149 use super::super::provider::ProviderCache;
150 use super::super::refs::SecretRef;
151
152 use std::collections::{BTreeMap, BTreeSet};
153
154 use super::super::testing::*;
155 use super::*;
156
157 #[test]
158 fn resolution_prefers_the_exact_environment_then_all_then_gives_up() {
159 let mut store = BTreeMap::new();
160 store.insert(
161 "K".to_string(),
162 BTreeMap::from([
163 ("production".to_string(), "prod".to_string()),
164 ("all".to_string(), "fallback".to_string()),
165 ]),
166 );
167 store.insert(
168 "ONLY_ALL".to_string(),
169 BTreeMap::from([("all".to_string(), "everywhere".to_string())]),
170 );
171 store.insert(
172 "ONLY_PROD".to_string(),
173 BTreeMap::from([("production".to_string(), "prod".to_string())]),
174 );
175
176 let view = SecretView::new("staging".to_string(), store, ProviderCache::default());
177 assert!(matches!(
178 view.resolve(&SecretRef {
179 namespace: None,
180 key: "K"
181 }),
182 Resolution::Found("fallback")
183 ));
184 assert!(matches!(
185 view.resolve(&SecretRef {
186 namespace: None,
187 key: "ONLY_ALL"
188 }),
189 Resolution::Found("everywhere")
190 ));
191 assert!(matches!(
193 view.resolve(&SecretRef {
194 namespace: None,
195 key: "ONLY_PROD"
196 }),
197 Resolution::MissingKey
198 ));
199 assert!(matches!(
200 view.resolve(&SecretRef {
201 namespace: None,
202 key: "ABSENT"
203 }),
204 Resolution::MissingKey
205 ));
206 }
207
208 #[test]
209 fn an_unpopulated_namespace_is_told_apart_from_a_missing_key() {
210 let view = SecretView::new(
211 "production".to_string(),
212 BTreeMap::new(),
213 vercel_production(),
214 );
215
216 assert!(matches!(
217 view.resolve(&SecretRef {
218 namespace: Some("vercel"),
219 key: "PRESENT"
220 }),
221 Resolution::Found("v")
222 ));
223 assert!(matches!(
225 view.resolve(&SecretRef {
226 namespace: Some("vercel"),
227 key: "ABSENT"
228 }),
229 Resolution::MissingKey
230 ));
231 assert!(matches!(
233 view.resolve(&SecretRef {
234 namespace: Some("vault"),
235 key: "ANY"
236 }),
237 Resolution::MissingNamespace
238 ));
239 }
240
241 #[test]
247 fn a_namespace_pushed_for_another_environment_is_not_populated_for_this_one() {
248 let view = SecretView::new("staging".to_string(), BTreeMap::new(), vercel_production());
249
250 assert!(
251 matches!(
252 view.resolve(&SecretRef {
253 namespace: Some("vercel"),
254 key: "PRESENT"
255 }),
256 Resolution::MissingNamespace
257 ),
258 "staging has had no push, so waiting is what fixes this"
259 );
260 }
261
262 #[test]
266 fn a_pushed_pair_missing_a_key_stays_permanent() {
267 let view = SecretView::new(
268 "production".to_string(),
269 BTreeMap::new(),
270 vercel_production(),
271 );
272
273 assert!(matches!(
274 view.resolve(&SecretRef {
275 namespace: Some("vercel"),
276 key: "ABSENT"
277 }),
278 Resolution::MissingKey
279 ));
280 }
281
282 #[test]
287 fn an_all_slot_push_makes_a_genuinely_missing_key_permanent() {
288 let view = SecretView::new("staging".to_string(), BTreeMap::new(), vercel_all());
289
290 assert!(matches!(
291 view.resolve(&SecretRef {
292 namespace: Some("vercel"),
293 key: "ABSENT"
294 }),
295 Resolution::MissingKey
296 ));
297 }
298
299 #[test]
303 fn an_all_slot_push_resolves_its_key_for_every_environment() {
304 let view = SecretView::new("staging".to_string(), BTreeMap::new(), vercel_all());
305
306 assert!(matches!(
307 view.resolve(&SecretRef {
308 namespace: Some("vercel"),
309 key: "PRESENT"
310 }),
311 Resolution::Found("v")
312 ));
313 }
314
315 #[test]
319 fn an_empty_push_populates_the_pair_it_carried() {
320 let view = SecretView::new(
321 "production".to_string(),
322 BTreeMap::new(),
323 ProviderCache::new(
324 BTreeMap::from([("vercel".to_string(), BTreeMap::new())]),
325 BTreeMap::from([(
326 "vercel".to_string(),
327 BTreeSet::from(["production".to_string()]),
328 )]),
329 ),
330 );
331
332 assert!(matches!(
333 view.resolve(&SecretRef {
334 namespace: Some("vercel"),
335 key: "ANY"
336 }),
337 Resolution::MissingKey
338 ));
339 }
340
341 #[test]
344 fn debug_never_prints_a_value() {
345 let store = BTreeMap::from([(
346 "K".to_string(),
347 BTreeMap::from([("production".to_string(), "hunter2".to_string())]),
348 )]);
349 let view = SecretView::new("production".to_string(), store, ProviderCache::default());
350 let rendered = format!("{view:?}");
351 assert_eq!(
352 rendered,
353 "SecretView { environment: \"production\", keys: 1, namespaces: 0 }"
354 );
355 assert!(!rendered.contains("hunter2"));
356 }
357
358 #[test]
360 fn a_resolution_debug_never_prints_the_value_it_found() {
361 assert_eq!(format!("{:?}", Resolution::Found("hunter2")), "Found(..)");
362 assert_eq!(format!("{:?}", Resolution::MissingKey), "MissingKey");
363 assert_eq!(
364 format!("{:?}", Resolution::MissingNamespace),
365 "MissingNamespace"
366 );
367 }
368}