1use crate::env_config::property::PropertiesKey;
7use crate::env_config::section::EnvConfigSections;
8use aws_types::origin::Origin;
9use aws_types::os_shim_internal::Env;
10use aws_types::service_config::ServiceConfigKey;
11use std::borrow::Cow;
12use std::error::Error;
13use std::fmt;
14
15pub mod error;
16pub mod file;
17mod normalize;
18pub mod parse;
19pub mod property;
20pub mod section;
21pub mod source;
22
23pub fn get_service_env_config<'a, T, E>(
25 key: ServiceConfigKey<'a>,
26 env: &'a Env,
27 shared_config_sections: Option<&'a EnvConfigSections>,
28 validator: impl Fn(&str) -> Result<T, E>,
29) -> Result<Option<T>, EnvConfigError<E>>
30where
31 E: Error + Send + Sync + 'static,
32{
33 EnvConfigValue::default()
34 .env(key.env())
35 .profile(key.profile())
36 .service_id(key.service_id())
37 .validate(env, shared_config_sections, validator)
38}
39
40#[derive(Debug)]
41enum Location<'a> {
42 Environment,
43 Profile { name: Cow<'a, str> },
44}
45
46impl<'a> fmt::Display for Location<'a> {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 match self {
49 Location::Environment => write!(f, "environment variable"),
50 Location::Profile { name } => write!(f, "profile (`{name}`)"),
51 }
52 }
53}
54
55#[derive(Debug)]
56enum Scope<'a> {
57 Global,
58 Service { service_id: Cow<'a, str> },
59}
60
61impl<'a> fmt::Display for Scope<'a> {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 match self {
64 Scope::Global => write!(f, "global"),
65 Scope::Service { service_id } => write!(f, "service-specific (`{service_id}`)"),
66 }
67 }
68}
69
70#[derive(Debug)]
79pub struct EnvConfigSource<'a> {
80 key: Cow<'a, str>,
81 location: Location<'a>,
82 scope: Scope<'a>,
83}
84
85#[allow(clippy::from_over_into)]
86impl Into<Origin> for &EnvConfigSource<'_> {
87 fn into(self) -> Origin {
88 match (&self.scope, &self.location) {
89 (Scope::Global, Location::Environment) => Origin::shared_environment_variable(),
90 (Scope::Global, Location::Profile { .. }) => Origin::shared_profile_file(),
91 (Scope::Service { .. }, Location::Environment) => {
92 Origin::service_environment_variable()
93 }
94 (Scope::Service { .. }, Location::Profile { .. }) => Origin::service_profile_file(),
95 }
96 }
97}
98
99impl<'a> EnvConfigSource<'a> {
100 pub(crate) fn global_from_env(key: Cow<'a, str>) -> Self {
101 Self {
102 key,
103 location: Location::Environment,
104 scope: Scope::Global,
105 }
106 }
107
108 pub(crate) fn global_from_profile(key: Cow<'a, str>, profile_name: Cow<'a, str>) -> Self {
109 Self {
110 key,
111 location: Location::Profile { name: profile_name },
112 scope: Scope::Global,
113 }
114 }
115
116 pub(crate) fn service_from_env(key: Cow<'a, str>, service_id: Cow<'a, str>) -> Self {
117 Self {
118 key,
119 location: Location::Environment,
120 scope: Scope::Service { service_id },
121 }
122 }
123
124 pub(crate) fn service_from_profile(
125 key: Cow<'a, str>,
126 profile_name: Cow<'a, str>,
127 service_id: Cow<'a, str>,
128 ) -> Self {
129 Self {
130 key,
131 location: Location::Profile { name: profile_name },
132 scope: Scope::Service { service_id },
133 }
134 }
135}
136
137impl<'a> fmt::Display for EnvConfigSource<'a> {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 write!(f, "{} {} key: `{}`", self.scope, self.location, self.key)
140 }
141}
142
143#[derive(Debug)]
145pub struct EnvConfigError<E = Box<dyn Error>> {
146 property_source: String,
147 err: E,
148}
149
150impl<E> EnvConfigError<E> {
151 pub fn err(&self) -> &E {
153 &self.err
154 }
155}
156
157impl<E: fmt::Display> fmt::Display for EnvConfigError<E> {
158 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159 write!(f, "{}. source: {}", self.err, self.property_source)
160 }
161}
162
163impl<E: Error> Error for EnvConfigError<E> {
164 fn source(&self) -> Option<&(dyn Error + 'static)> {
165 self.err.source()
166 }
167}
168
169#[derive(Default, Debug)]
174pub struct EnvConfigValue<'a> {
175 environment_variable: Option<Cow<'a, str>>,
176 profile_key: Option<Cow<'a, str>>,
177 service_id: Option<Cow<'a, str>>,
178}
179
180impl<'a> EnvConfigValue<'a> {
181 pub fn new() -> Self {
183 Self::default()
184 }
185
186 pub fn env(mut self, key: &'a str) -> Self {
188 self.environment_variable = Some(Cow::Borrowed(key));
189 self
190 }
191
192 pub fn profile(mut self, key: &'a str) -> Self {
194 self.profile_key = Some(Cow::Borrowed(key));
195 self
196 }
197
198 pub fn service_id(mut self, service_id: &'a str) -> Self {
200 self.service_id = Some(Cow::Borrowed(service_id));
201 self
202 }
203
204 pub fn validate<T, E: Error + Send + Sync + 'static>(
206 self,
207 env: &Env,
208 profiles: Option<&EnvConfigSections>,
209 validator: impl Fn(&str) -> Result<T, E>,
210 ) -> Result<Option<T>, EnvConfigError<E>> {
211 let value = self.load(env, profiles);
212 value
213 .map(|(v, ctx)| {
214 validator(v.as_ref()).map_err(|err| EnvConfigError {
215 property_source: format!("{}", ctx),
216 err,
217 })
218 })
219 .transpose()
220 }
221
222 pub fn validate_and_return_origin<T, E: Error + Send + Sync + 'static>(
226 self,
227 env: &Env,
228 profiles: Option<&EnvConfigSections>,
229 validator: impl Fn(&str) -> Result<T, E>,
230 ) -> Result<(Option<T>, Origin), EnvConfigError<E>> {
231 let value = self.load(env, profiles);
232 match value {
233 Some((v, ctx)) => {
234 let origin: Origin = (&ctx).into();
235 validator(v.as_ref())
236 .map_err(|err| EnvConfigError {
237 property_source: format!("{}", ctx),
238 err,
239 })
240 .map(|value| (Some(value), origin))
241 }
242 None => Ok((None, Origin::unknown())),
243 }
244 }
245
246 pub fn load(
248 &self,
249 env: &'a Env,
250 profiles: Option<&'a EnvConfigSections>,
251 ) -> Option<(Cow<'a, str>, EnvConfigSource<'a>)> {
252 let env_value = self.environment_variable.as_ref().and_then(|env_var| {
253 let service_config =
255 get_service_config_from_env(env, self.service_id.clone(), env_var.clone());
256 let global_config = env.get(env_var).ok().map(|value| {
258 (
259 Cow::Owned(value),
260 EnvConfigSource::global_from_env(env_var.clone()),
261 )
262 });
263
264 if let Some(v) = service_config {
265 tracing::trace!("(service env) {env_var} = {v:?}");
266 Some(v)
267 } else if let Some(v) = global_config {
268 tracing::trace!("(global env) {env_var} = {v:?}");
269 Some(v)
270 } else {
271 tracing::trace!("(env) no value set for {env_var}");
272 None
273 }
274 });
275
276 let profile_value = match (profiles, self.profile_key.as_ref()) {
277 (Some(profiles), Some(profile_key)) => {
278 let service_config = get_service_config_from_profile(
280 profiles,
281 self.service_id.clone(),
282 profile_key.clone(),
283 );
284 let global_config = profiles.get(profile_key.as_ref()).map(|value| {
285 (
286 Cow::Borrowed(value),
287 EnvConfigSource::global_from_profile(
288 profile_key.clone(),
289 Cow::Owned(profiles.selected_profile().to_owned()),
290 ),
291 )
292 });
293
294 if let Some(v) = service_config {
295 tracing::trace!("(service profile) {profile_key} = {v:?}");
296 Some(v)
297 } else if let Some(v) = global_config {
298 tracing::trace!("(global profile) {profile_key} = {v:?}");
299 Some(v)
300 } else {
301 tracing::trace!("(service profile) no value set for {profile_key}");
302 None
303 }
304 }
305 _ => None,
306 };
307
308 env_value.or(profile_value)
309 }
310}
311
312fn get_service_config_from_env<'a>(
313 env: &'a Env,
314 service_id: Option<Cow<'a, str>>,
315 env_var: Cow<'a, str>,
316) -> Option<(Cow<'a, str>, EnvConfigSource<'a>)> {
317 let service_id = service_id?;
318 let env_case_service_id = format_service_id_for_env(service_id.clone());
319 let service_specific_env_key = format!("{env_var}_{env_case_service_id}");
320 let env_var = env.get(&service_specific_env_key).ok()?;
321 let env_var: Cow<'_, str> = Cow::Owned(env_var);
322 let source = EnvConfigSource::service_from_env(env_var.clone(), service_id);
323
324 Some((env_var, source))
325}
326
327const SERVICES: &str = "services";
328
329fn get_service_config_from_profile<'a>(
330 profile: &EnvConfigSections,
331 service_id: Option<Cow<'a, str>>,
332 profile_key: Cow<'a, str>,
333) -> Option<(Cow<'a, str>, EnvConfigSource<'a>)> {
334 let service_id = service_id?.clone();
335 let profile_case_service_id = format_service_id_for_profile(service_id.clone());
336 let services_section_name = profile.get(SERVICES)?;
337 let properties_key = PropertiesKey::builder()
338 .section_key(SERVICES)
339 .section_name(services_section_name)
340 .property_name(profile_case_service_id)
341 .sub_property_name(profile_key.clone())
342 .build()
343 .ok()?;
344 let value = profile.other_sections().get(&properties_key)?;
345 let profile_name = Cow::Owned(profile.selected_profile().to_owned());
346 let source = EnvConfigSource::service_from_profile(profile_key, profile_name, service_id);
347
348 Some((Cow::Owned(value.to_owned()), source))
349}
350
351fn format_service_id_for_env(service_id: impl AsRef<str>) -> String {
352 service_id.as_ref().to_uppercase().replace(' ', "_")
353}
354
355fn format_service_id_for_profile(service_id: impl AsRef<str>) -> String {
356 service_id.as_ref().to_lowercase().replace(' ', "-")
357}
358
359#[cfg(test)]
360mod test {
361 use crate::env_config::property::{Properties, PropertiesKey};
362 use crate::env_config::section::EnvConfigSections;
363 use aws_types::os_shim_internal::Env;
364 use std::borrow::Cow;
365 use std::collections::HashMap;
366 use std::num::ParseIntError;
367
368 use super::EnvConfigValue;
369
370 fn validate_some_key(s: &str) -> Result<i32, ParseIntError> {
371 s.parse()
372 }
373
374 fn new_prop_key(
375 section_key: impl Into<String>,
376 section_name: impl Into<String>,
377 property_name: impl Into<String>,
378 sub_property_name: Option<impl Into<String>>,
379 ) -> PropertiesKey {
380 let mut builder = PropertiesKey::builder()
381 .section_key(section_key)
382 .section_name(section_name)
383 .property_name(property_name);
384
385 if let Some(sub_property_name) = sub_property_name {
386 builder = builder.sub_property_name(sub_property_name);
387 }
388
389 builder.build().unwrap()
390 }
391
392 #[tokio::test]
393 async fn test_service_config_multiple_services() {
394 let env = Env::from_slice(&[
395 ("AWS_CONFIG_FILE", "config"),
396 ("AWS_SOME_KEY", "1"),
397 ("AWS_SOME_KEY_SERVICE", "2"),
398 ("AWS_SOME_KEY_ANOTHER_SERVICE", "3"),
399 ]);
400 let profiles = EnvConfigSections::new(
401 HashMap::from([(
402 "default".to_owned(),
403 HashMap::from([
404 ("some_key".to_owned(), "4".to_owned()),
405 ("services".to_owned(), "dev".to_owned()),
406 ]),
407 )]),
408 Cow::Borrowed("default"),
409 HashMap::new(),
410 Properties::new_from_slice(&[
411 (
412 new_prop_key("services", "dev", "service", Some("some_key")),
413 "5".to_string(),
414 ),
415 (
416 new_prop_key("services", "dev", "another_service", Some("some_key")),
417 "6".to_string(),
418 ),
419 ]),
420 );
421 let profiles = Some(&profiles);
422 let global_from_env = EnvConfigValue::new()
423 .env("AWS_SOME_KEY")
424 .profile("some_key")
425 .validate(&env, profiles, validate_some_key)
426 .expect("config resolution succeeds");
427 assert_eq!(Some(1), global_from_env);
428
429 let service_from_env = EnvConfigValue::new()
430 .env("AWS_SOME_KEY")
431 .profile("some_key")
432 .service_id("service")
433 .validate(&env, profiles, validate_some_key)
434 .expect("config resolution succeeds");
435 assert_eq!(Some(2), service_from_env);
436
437 let other_service_from_env = EnvConfigValue::new()
438 .env("AWS_SOME_KEY")
439 .profile("some_key")
440 .service_id("another_service")
441 .validate(&env, profiles, validate_some_key)
442 .expect("config resolution succeeds");
443 assert_eq!(Some(3), other_service_from_env);
444
445 let global_from_profile = EnvConfigValue::new()
446 .profile("some_key")
447 .validate(&env, profiles, validate_some_key)
448 .expect("config resolution succeeds");
449 assert_eq!(Some(4), global_from_profile);
450
451 let service_from_profile = EnvConfigValue::new()
452 .profile("some_key")
453 .service_id("service")
454 .validate(&env, profiles, validate_some_key)
455 .expect("config resolution succeeds");
456 assert_eq!(Some(5), service_from_profile);
457
458 let service_from_profile = EnvConfigValue::new()
459 .profile("some_key")
460 .service_id("another_service")
461 .validate(&env, profiles, validate_some_key)
462 .expect("config resolution succeeds");
463 assert_eq!(Some(6), service_from_profile);
464 }
465
466 #[tokio::test]
467 async fn test_service_config_precedence() {
468 let env = Env::from_slice(&[
469 ("AWS_CONFIG_FILE", "config"),
470 ("AWS_SOME_KEY", "1"),
471 ("AWS_SOME_KEY_S3", "2"),
472 ]);
473
474 let profiles = EnvConfigSections::new(
475 HashMap::from([(
476 "default".to_owned(),
477 HashMap::from([
478 ("some_key".to_owned(), "3".to_owned()),
479 ("services".to_owned(), "dev".to_owned()),
480 ]),
481 )]),
482 Cow::Borrowed("default"),
483 HashMap::new(),
484 Properties::new_from_slice(&[(
485 new_prop_key("services", "dev", "s3", Some("some_key")),
486 "4".to_string(),
487 )]),
488 );
489 let profiles = Some(&profiles);
490 let global_from_env = EnvConfigValue::new()
491 .env("AWS_SOME_KEY")
492 .profile("some_key")
493 .validate(&env, profiles, validate_some_key)
494 .expect("config resolution succeeds");
495 assert_eq!(Some(1), global_from_env);
496
497 let service_from_env = EnvConfigValue::new()
498 .env("AWS_SOME_KEY")
499 .profile("some_key")
500 .service_id("s3")
501 .validate(&env, profiles, validate_some_key)
502 .expect("config resolution succeeds");
503 assert_eq!(Some(2), service_from_env);
504
505 let global_from_profile = EnvConfigValue::new()
506 .profile("some_key")
507 .validate(&env, profiles, validate_some_key)
508 .expect("config resolution succeeds");
509 assert_eq!(Some(3), global_from_profile);
510
511 let service_from_profile = EnvConfigValue::new()
512 .profile("some_key")
513 .service_id("s3")
514 .validate(&env, profiles, validate_some_key)
515 .expect("config resolution succeeds");
516 assert_eq!(Some(4), service_from_profile);
517 }
518
519 #[tokio::test]
520 async fn test_multiple_services() {
521 let env = Env::from_slice(&[
522 ("AWS_CONFIG_FILE", "config"),
523 ("AWS_SOME_KEY", "1"),
524 ("AWS_SOME_KEY_S3", "2"),
525 ("AWS_SOME_KEY_EC2", "3"),
526 ]);
527
528 let profiles = EnvConfigSections::new(
529 HashMap::from([(
530 "default".to_owned(),
531 HashMap::from([
532 ("some_key".to_owned(), "4".to_owned()),
533 ("services".to_owned(), "dev".to_owned()),
534 ]),
535 )]),
536 Cow::Borrowed("default"),
537 HashMap::new(),
538 Properties::new_from_slice(&[
539 (
540 new_prop_key("services", "dev-wrong", "s3", Some("some_key")),
541 "998".into(),
542 ),
543 (
544 new_prop_key("services", "dev-wrong", "ec2", Some("some_key")),
545 "999".into(),
546 ),
547 (
548 new_prop_key("services", "dev", "s3", Some("some_key")),
549 "5".into(),
550 ),
551 (
552 new_prop_key("services", "dev", "ec2", Some("some_key")),
553 "6".into(),
554 ),
555 ]),
556 );
557 let profiles = Some(&profiles);
558 let global_from_env = EnvConfigValue::new()
559 .env("AWS_SOME_KEY")
560 .profile("some_key")
561 .validate(&env, profiles, validate_some_key)
562 .expect("config resolution succeeds");
563 assert_eq!(Some(1), global_from_env);
564
565 let service_from_env = EnvConfigValue::new()
566 .env("AWS_SOME_KEY")
567 .profile("some_key")
568 .service_id("s3")
569 .validate(&env, profiles, validate_some_key)
570 .expect("config resolution succeeds");
571 assert_eq!(Some(2), service_from_env);
572
573 let service_from_env = EnvConfigValue::new()
574 .env("AWS_SOME_KEY")
575 .profile("some_key")
576 .service_id("ec2")
577 .validate(&env, profiles, validate_some_key)
578 .expect("config resolution succeeds");
579 assert_eq!(Some(3), service_from_env);
580
581 let global_from_profile = EnvConfigValue::new()
582 .profile("some_key")
583 .validate(&env, profiles, validate_some_key)
584 .expect("config resolution succeeds");
585 assert_eq!(Some(4), global_from_profile);
586
587 let service_from_profile = EnvConfigValue::new()
588 .profile("some_key")
589 .service_id("s3")
590 .validate(&env, profiles, validate_some_key)
591 .expect("config resolution succeeds");
592 assert_eq!(Some(5), service_from_profile);
593
594 let service_from_profile = EnvConfigValue::new()
595 .profile("some_key")
596 .service_id("ec2")
597 .validate(&env, profiles, validate_some_key)
598 .expect("config resolution succeeds");
599 assert_eq!(Some(6), service_from_profile);
600 }
601}