1use std::string::{String, ToString};
38use std::vec::Vec;
39
40use facet_reflect::Span;
41use indexmap::IndexMap;
42
43use crate::config_value::{ConfigValue, ConfigValueVisitorMut};
44use crate::driver::LayerOutput;
45use crate::path::Path;
46use crate::provenance::Provenance;
47use crate::schema::{ConfigStructSchema, ConfigValueSchema, Schema};
48use crate::value_builder::{LeafValue, ValueBuilder};
49
50pub trait EnvSource {
58 fn get(&self, name: &str) -> Option<String>;
60
61 fn vars(&self) -> Box<dyn Iterator<Item = (String, String)> + '_>;
63}
64
65#[derive(Debug, Clone, Copy, Default)]
67pub struct StdEnv;
68
69impl EnvSource for StdEnv {
70 fn get(&self, name: &str) -> Option<String> {
71 std::env::var(name).ok()
72 }
73
74 fn vars(&self) -> Box<dyn Iterator<Item = (String, String)> + '_> {
75 Box::new(std::env::vars())
76 }
77}
78
79#[derive(Debug, Clone, Default)]
81pub struct MockEnv {
82 vars: IndexMap<String, String, std::hash::RandomState>,
83}
84
85impl MockEnv {
86 pub fn new() -> Self {
88 Self::default()
89 }
90
91 pub fn from_pairs<I, K, V>(iter: I) -> Self
93 where
94 I: IntoIterator<Item = (K, V)>,
95 K: Into<String>,
96 V: Into<String>,
97 {
98 Self {
99 vars: iter
100 .into_iter()
101 .map(|(k, v)| (k.into(), v.into()))
102 .collect(),
103 }
104 }
105
106 pub fn set(&mut self, name: impl Into<String>, value: impl Into<String>) {
108 self.vars.insert(name.into(), value.into());
109 }
110}
111
112impl EnvSource for MockEnv {
113 fn get(&self, name: &str) -> Option<String> {
114 self.vars.get(name).cloned()
115 }
116
117 fn vars(&self) -> Box<dyn Iterator<Item = (String, String)> + '_> {
118 Box::new(self.vars.iter().map(|(k, v)| (k.clone(), v.clone())))
119 }
120}
121
122pub struct EnvConfig {
128 pub prefix: String,
131
132 pub strict: bool,
135
136 pub source: Option<Box<dyn EnvSource>>,
138}
139
140impl EnvConfig {
141 pub fn new(prefix: impl Into<String>) -> Self {
143 Self {
144 prefix: prefix.into(),
145 strict: false,
146 source: None,
147 }
148 }
149
150 pub fn strict(mut self) -> Self {
152 self.strict = true;
153 self
154 }
155
156 pub fn source(&self) -> &dyn EnvSource {
158 self.source.as_ref().map(|s| s.as_ref()).unwrap_or(&StdEnv)
159 }
160}
161
162#[derive(Default)]
164pub struct EnvConfigBuilder {
165 prefix: String,
166 strict: bool,
167 source: Option<Box<dyn EnvSource>>,
168}
169
170impl EnvConfigBuilder {
171 pub fn new() -> Self {
173 Self::default()
174 }
175
176 pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
178 self.prefix = prefix.into();
179 self
180 }
181
182 pub fn strict(mut self) -> Self {
184 self.strict = true;
185 self
186 }
187
188 pub fn source(mut self, source: impl EnvSource + 'static) -> Self {
190 self.source = Some(Box::new(source));
191 self
192 }
193
194 pub fn build(self) -> EnvConfig {
196 let mut config = EnvConfig::new(self.prefix);
197 if self.strict {
198 config = config.strict();
199 }
200 config.source = self.source;
201 config
202 }
203}
204
205pub fn parse_env(schema: &Schema, env_config: &EnvConfig, source: &dyn EnvSource) -> LayerOutput {
210 let Some(config_schema) = schema.config() else {
212 return parse_env_no_config(env_config, source);
213 };
214
215 let prefix = if env_config.prefix.is_empty() {
217 config_schema.env_prefix().unwrap_or("")
218 } else {
219 &env_config.prefix
220 };
221
222 let prefix_with_sep = format!("{}__", prefix);
223
224 let mut builder = ValueBuilder::new(config_schema);
226
227 let mut prefixed_paths: Vec<Vec<String>> = Vec::new();
229
230 for (name, value) in source.vars() {
232 if !name.starts_with(&prefix_with_sep) {
234 continue;
235 }
236
237 let rest = &name[prefix_with_sep.len()..];
239 if rest.is_empty() {
240 builder.warn(format!(
241 "invalid environment variable name: {} (empty after prefix)",
242 name
243 ));
244 continue;
245 }
246
247 let segments: Vec<&str> = rest.split("__").collect();
249
250 if segments.iter().any(|s| s.is_empty()) {
252 builder.warn(format!(
253 "invalid environment variable name: {} (contains empty segment)",
254 name
255 ));
256 continue;
257 }
258
259 let path: Vec<String> = segments.iter().map(|s| s.to_lowercase()).collect();
261
262 let prov = Provenance::env(&name, &value);
264
265 validate_enum_value_if_applicable(&mut builder, config_schema, &path, &value, &name);
267
268 let leaf_value = parse_env_value(&value);
270
271 if builder.set(&path, leaf_value, None, prov) {
275 prefixed_paths.push(path);
276 }
277 }
278
279 check_env_aliases(&mut builder, config_schema, source, &[], &prefixed_paths);
281
282 let mut output = builder.into_output(config_schema.field_name());
283
284 if let Some(ref mut value) = output.value {
286 let source_text = assign_env_spans(value);
287 if !source_text.is_empty() {
288 output.source_text = Some(source_text);
289 }
290 }
291
292 output
293}
294
295fn check_env_aliases(
297 builder: &mut ValueBuilder,
298 schema: &ConfigStructSchema,
299 source: &dyn EnvSource,
300 parent_path: &[String],
301 prefixed_paths: &[Vec<String>],
302) {
303 for (field_name, field_schema) in schema.fields() {
304 let mut field_path = parent_path.to_vec();
305 field_path.push(field_name.clone());
306
307 let already_set = prefixed_paths.contains(&field_path);
309 if !already_set {
310 for alias in field_schema.env_aliases() {
311 if let Some(value) = source.get(alias) {
312 let prov = Provenance::env(alias, &value);
313 let leaf_value = parse_env_value(&value);
314 builder.set(&field_path, leaf_value, None, prov);
315 break;
317 }
318 }
319 }
320
321 match field_schema.value() {
323 ConfigValueSchema::Struct(nested) => {
324 check_env_aliases(builder, nested, source, &field_path, prefixed_paths);
325 }
326 ConfigValueSchema::Option { value, .. } => {
327 if let ConfigValueSchema::Struct(nested) = value.as_ref() {
328 check_env_aliases(builder, nested, source, &field_path, prefixed_paths);
329 }
330 }
331 _ => {}
332 }
333 }
334}
335
336fn parse_env_no_config(env_config: &EnvConfig, source: &dyn EnvSource) -> LayerOutput {
338 use crate::config_value::{ConfigValue, Sourced};
339 use crate::driver::UnusedKey;
340
341 let prefix = &env_config.prefix;
342 let prefix_with_sep = format!("{}__", prefix);
343
344 let mut unused_keys = Vec::new();
345
346 for (name, _value) in source.vars() {
347 if name.starts_with(&prefix_with_sep) {
348 let rest = &name[prefix_with_sep.len()..];
349 if !rest.is_empty() {
350 let segments: Vec<&str> = rest.split("__").collect();
351 if !segments.iter().any(|s| s.is_empty()) {
352 let path: Vec<String> = segments.iter().map(|s| s.to_lowercase()).collect();
353 unused_keys.push(UnusedKey {
354 key: path,
355 provenance: Provenance::env(&name, ""),
356 });
357 }
358 }
359 }
360 }
361
362 LayerOutput {
363 value: Some(ConfigValue::Object(Sourced::new(IndexMap::default()))),
364 unused_keys,
365 diagnostics: Vec::new(),
366 source_text: None,
367 config_file_path: None,
368 help_list_mode: None,
369 }
370}
371
372fn parse_env_value(value: &str) -> LeafValue {
374 if value.contains(',') {
375 let elements = parse_comma_separated(value);
376 if elements.len() > 1 {
377 return LeafValue::StringArray(elements);
378 } else if elements.len() == 1 {
379 return LeafValue::String(elements.into_iter().next().unwrap());
380 }
381 }
382 LeafValue::String(value.to_string())
383}
384
385fn validate_enum_value_if_applicable(
387 builder: &mut ValueBuilder,
388 schema: &ConfigStructSchema,
389 path: &[String],
390 value: &str,
391 var_name: &str,
392) {
393 if let Some(value_schema) = schema.get_by_path(&path.to_vec()) {
394 let inner_schema = match value_schema {
396 ConfigValueSchema::Option { value: inner, .. } => inner.as_ref(),
397 other => other,
398 };
399
400 if let ConfigValueSchema::Enum(enum_schema) = inner_schema {
402 let variants = enum_schema.variants();
403 if !variants.contains_key(value) {
404 let valid_variants: Vec<&str> = variants.keys().map(|s| s.as_str()).collect();
405
406 let suggestion =
408 crate::suggest::format_suggestion(value, valid_variants.iter().copied());
409
410 builder.warn(format!(
411 "{}: unknown variant '{}' for {}{} Valid variants are: {}",
412 var_name,
413 value,
414 path.join("."),
415 suggestion,
416 valid_variants
417 .iter()
418 .map(|v| format!("'{}'", v))
419 .collect::<Vec<_>>()
420 .join(", ")
421 ));
422 }
423 }
424 }
425}
426
427fn parse_comma_separated(input: &str) -> Vec<String> {
429 let mut result = Vec::new();
430 let mut current = String::new();
431 let mut chars = input.chars().peekable();
432
433 while let Some(ch) = chars.next() {
434 if ch == '\\' {
435 if let Some(&next) = chars.peek() {
436 if next == ',' {
437 chars.next();
438 current.push(',');
439 } else {
440 current.push(ch);
441 }
442 } else {
443 current.push(ch);
444 }
445 } else if ch == ',' {
446 let trimmed = current.trim().to_string();
447 if !trimmed.is_empty() {
448 result.push(trimmed);
449 }
450 current.clear();
451 } else {
452 current.push(ch);
453 }
454 }
455
456 let trimmed = current.trim().to_string();
457 if !trimmed.is_empty() {
458 result.push(trimmed);
459 }
460
461 if result.is_empty() {
462 result.push(input.to_string());
463 }
464
465 result
466}
467
468pub fn assign_env_spans(value: &mut ConfigValue) -> String {
485 let mut visitor = EnvSpanVisitor::new();
486 let mut path = Path::new();
487 value.visit_mut(&mut visitor, &mut path);
488 visitor.document
489}
490
491struct EnvSpanVisitor {
493 document: String,
495 var_spans: IndexMap<String, (usize, usize), std::hash::RandomState>,
497}
498
499impl EnvSpanVisitor {
500 fn new() -> Self {
501 Self {
502 document: String::new(),
503 var_spans: IndexMap::default(),
504 }
505 }
506
507 fn ensure_var(&mut self, var: &str, env_value: &str) -> Span {
509 if let Some(&(offset, len)) = self.var_spans.get(var) {
510 return Span::new(offset, len);
511 }
512
513 self.document.push_str(var);
516 self.document.push_str("=\"");
517 let value_offset = self.document.len();
518 self.document.push_str(env_value);
519 let value_len = env_value.len();
520 self.document.push_str("\"\n");
521
522 self.var_spans
523 .insert(var.to_string(), (value_offset, value_len));
524
525 Span::new(value_offset, value_len)
526 }
527}
528
529impl ConfigValueVisitorMut for EnvSpanVisitor {
530 fn visit_value(&mut self, _path: &Path, value: &mut ConfigValue) {
531 if let Some(Provenance::Env {
532 var,
533 value: env_value,
534 }) = value.provenance().cloned()
535 {
536 *value.span_mut() = Some(self.ensure_var(&var, &env_value));
537 }
538 }
539}
540
541#[cfg(test)]
542mod tests {
543 use facet::Facet;
544 use figue_attrs as args;
545
546 use crate::config_value::ConfigValue;
547 use crate::driver::Severity;
548 use crate::schema::Schema;
549
550 use super::*;
551
552 #[derive(Facet)]
557 struct ArgsWithConfig {
558 #[facet(args::named)]
559 verbose: bool,
560
561 #[facet(args::config)]
562 config: ServerConfig,
563 }
564
565 #[derive(Facet)]
566 struct ServerConfig {
567 port: u16,
568 host: String,
569 }
570
571 #[derive(Facet)]
572 struct ArgsWithNestedConfig {
573 #[facet(args::config)]
574 settings: AppSettings,
575 }
576
577 #[derive(Facet)]
578 struct AppSettings {
579 port: u16,
580 smtp: SmtpConfig,
581 }
582
583 #[derive(Facet)]
584 struct SmtpConfig {
585 host: String,
586 connection_timeout: u64,
587 }
588
589 #[derive(Facet)]
590 struct ArgsWithListConfig {
591 #[facet(args::config)]
592 config: ListConfig,
593 }
594
595 #[derive(Facet)]
596 struct ListConfig {
597 ports: Vec<u16>,
598 allowed_hosts: Vec<String>,
599 }
600
601 fn env_config(prefix: &str) -> EnvConfig {
606 EnvConfigBuilder::new().prefix(prefix).build()
607 }
608
609 fn env_config_strict(prefix: &str) -> EnvConfig {
610 EnvConfigBuilder::new().prefix(prefix).strict().build()
611 }
612
613 fn get_nested<'a>(cv: &'a ConfigValue, path: &[&str]) -> Option<&'a ConfigValue> {
614 let mut current = cv;
615 for key in path {
616 match current {
617 ConfigValue::Object(obj) => {
618 current = obj.value.get(*key)?;
619 }
620 _ => return None,
621 }
622 }
623 Some(current)
624 }
625
626 fn get_string(cv: &ConfigValue) -> Option<&str> {
627 match cv {
628 ConfigValue::String(s) => Some(&s.value),
629 _ => None,
630 }
631 }
632
633 fn get_array_len(cv: &ConfigValue) -> Option<usize> {
634 match cv {
635 ConfigValue::Array(arr) => Some(arr.value.len()),
636 _ => None,
637 }
638 }
639
640 #[test]
645 fn test_empty_env() {
646 let schema = Schema::from_shape(ArgsWithConfig::SHAPE).unwrap();
647 let env = MockEnv::new();
648 let config = env_config("REEF");
649
650 let output = parse_env(&schema, &config, &env);
651
652 assert!(output.diagnostics.is_empty());
653 assert!(output.unused_keys.is_empty());
654 }
657
658 #[test]
659 fn test_single_flat_field() {
660 let schema = Schema::from_shape(ArgsWithConfig::SHAPE).unwrap();
661 let env = MockEnv::from_pairs([("REEF__PORT", "8080")]);
662 let config = env_config("REEF");
663
664 let output = parse_env(&schema, &config, &env);
665
666 assert!(output.diagnostics.is_empty());
667 let value = output.value.expect("should have value");
668
669 let port = get_nested(&value, &["config", "port"]).expect("should have config.port");
671 assert_eq!(get_string(port), Some("8080"));
672 }
673
674 #[test]
675 fn test_multiple_flat_fields() {
676 let schema = Schema::from_shape(ArgsWithConfig::SHAPE).unwrap();
677 let env = MockEnv::from_pairs([("REEF__PORT", "8080"), ("REEF__HOST", "localhost")]);
678 let config = env_config("REEF");
679
680 let output = parse_env(&schema, &config, &env);
681
682 assert!(output.diagnostics.is_empty());
683 let value = output.value.expect("should have value");
684
685 let port = get_nested(&value, &["config", "port"]).expect("should have config.port");
686 assert_eq!(get_string(port), Some("8080"));
687
688 let host = get_nested(&value, &["config", "host"]).expect("should have config.host");
689 assert_eq!(get_string(host), Some("localhost"));
690 }
691
692 #[test]
693 fn test_nested_field() {
694 let schema = Schema::from_shape(ArgsWithNestedConfig::SHAPE).unwrap();
695 let env = MockEnv::from_pairs([("REEF__SMTP__HOST", "mail.example.com")]);
696 let config = env_config("REEF");
697
698 let output = parse_env(&schema, &config, &env);
699
700 assert!(output.diagnostics.is_empty());
701 let value = output.value.expect("should have value");
702
703 let host = get_nested(&value, &["settings", "smtp", "host"])
705 .expect("should have settings.smtp.host");
706 assert_eq!(get_string(host), Some("mail.example.com"));
707 }
708
709 #[test]
710 fn test_deeply_nested() {
711 let schema = Schema::from_shape(ArgsWithNestedConfig::SHAPE).unwrap();
712 let env = MockEnv::from_pairs([
713 ("REEF__PORT", "8080"),
714 ("REEF__SMTP__HOST", "mail.example.com"),
715 ("REEF__SMTP__CONNECTION_TIMEOUT", "30"),
716 ]);
717 let config = env_config("REEF");
718
719 let output = parse_env(&schema, &config, &env);
720
721 assert!(output.diagnostics.is_empty());
722 let value = output.value.expect("should have value");
723
724 let port = get_nested(&value, &["settings", "port"]).expect("port");
725 assert_eq!(get_string(port), Some("8080"));
726
727 let host = get_nested(&value, &["settings", "smtp", "host"]).expect("smtp.host");
728 assert_eq!(get_string(host), Some("mail.example.com"));
729
730 let timeout = get_nested(&value, &["settings", "smtp", "connection_timeout"])
731 .expect("smtp.connection_timeout");
732 assert_eq!(get_string(timeout), Some("30"));
733 }
734
735 #[test]
740 fn test_comma_separated_list() {
741 let schema = Schema::from_shape(ArgsWithListConfig::SHAPE).unwrap();
742 let env = MockEnv::from_pairs([("REEF__PORTS", "8080,8081,8082")]);
743 let config = env_config("REEF");
744
745 let output = parse_env(&schema, &config, &env);
746
747 assert!(output.diagnostics.is_empty());
748 let value = output.value.expect("should have value");
749
750 let ports = get_nested(&value, &["config", "ports"]).expect("config.ports");
751 assert_eq!(get_array_len(ports), Some(3));
752 }
753
754 #[test]
755 fn test_escaped_comma() {
756 let schema = Schema::from_shape(ArgsWithConfig::SHAPE).unwrap();
757 let env = MockEnv::from_pairs([("REEF__HOST", r"hello\, world")]);
758 let config = env_config("REEF");
759
760 let output = parse_env(&schema, &config, &env);
761
762 assert!(output.diagnostics.is_empty());
763 let value = output.value.expect("should have value");
764
765 let host = get_nested(&value, &["config", "host"]).expect("config.host");
766 assert_eq!(get_string(host), Some("hello, world"));
767 }
768
769 #[test]
770 fn test_values_stay_as_strings() {
771 let schema = Schema::from_shape(ArgsWithConfig::SHAPE).unwrap();
773 let env = MockEnv::from_pairs([("REEF__PORT", "8080")]);
774 let config = env_config("REEF");
775
776 let output = parse_env(&schema, &config, &env);
777
778 let value = output.value.expect("should have value");
779 let port = get_nested(&value, &["config", "port"]).expect("config.port");
780
781 assert!(matches!(port, ConfigValue::String(_)));
783 }
784
785 #[test]
790 fn test_provenance_is_set() {
791 use crate::provenance::Provenance;
792
793 let schema = Schema::from_shape(ArgsWithConfig::SHAPE).unwrap();
794 let env = MockEnv::from_pairs([("REEF__PORT", "8080")]);
795 let config = env_config("REEF");
796
797 let output = parse_env(&schema, &config, &env);
798 let value = output.value.expect("should have value");
799
800 let port = get_nested(&value, &["config", "port"]).expect("config.port");
801 if let ConfigValue::String(s) = port {
802 let prov = s.provenance.as_ref().expect("should have provenance");
803 assert!(matches!(prov, Provenance::Env { .. }));
804 if let Provenance::Env { var, value } = prov {
805 assert_eq!(var, "REEF__PORT");
806 assert_eq!(value, "8080");
807 }
808 } else {
809 panic!("expected string");
810 }
811 }
812
813 #[test]
818 fn test_empty_segment_diagnostic() {
819 let schema = Schema::from_shape(ArgsWithConfig::SHAPE).unwrap();
820 let env = MockEnv::from_pairs([("REEF__FOO____BAR", "x")]);
821 let config = env_config("REEF");
822
823 let output = parse_env(&schema, &config, &env);
824
825 assert!(!output.diagnostics.is_empty());
827 assert!(
828 output
829 .diagnostics
830 .iter()
831 .any(|d| d.message.contains("empty segment") || d.message.contains("invalid"))
832 );
833 }
834
835 #[test]
836 fn test_just_prefix_diagnostic() {
837 let schema = Schema::from_shape(ArgsWithConfig::SHAPE).unwrap();
838 let env = MockEnv::from_pairs([("REEF__", "x")]);
839 let config = env_config("REEF");
840
841 let output = parse_env(&schema, &config, &env);
842
843 assert!(!output.diagnostics.is_empty());
845 }
846
847 #[test]
848 fn test_wrong_prefix_ignored() {
849 let schema = Schema::from_shape(ArgsWithConfig::SHAPE).unwrap();
850 let env = MockEnv::from_pairs([("OTHER__PORT", "8080")]);
851 let config = env_config("REEF");
852
853 let output = parse_env(&schema, &config, &env);
854
855 assert!(output.diagnostics.is_empty());
857 assert!(output.unused_keys.is_empty());
858 }
860
861 #[test]
862 fn test_single_underscore_ignored() {
863 let schema = Schema::from_shape(ArgsWithConfig::SHAPE).unwrap();
864 let env = MockEnv::from_pairs([("REEF_PORT", "8080")]);
865 let config = env_config("REEF");
866
867 let output = parse_env(&schema, &config, &env);
868
869 assert!(output.diagnostics.is_empty());
871 assert!(output.unused_keys.is_empty());
872 }
873
874 #[test]
879 fn test_unknown_field_unused_key() {
880 let schema = Schema::from_shape(ArgsWithConfig::SHAPE).unwrap();
881 let env = MockEnv::from_pairs([("REEF__PORTT", "8080")]);
883 let config = env_config("REEF");
884
885 let output = parse_env(&schema, &config, &env);
886
887 assert!(!output.unused_keys.is_empty());
889 assert!(output.unused_keys.iter().any(|k| {
890 k.key.iter().any(|s| s == "portt")
892 }));
893 }
894
895 #[test]
896 fn test_unknown_nested_field_unused_key() {
897 let schema = Schema::from_shape(ArgsWithNestedConfig::SHAPE).unwrap();
898 let env = MockEnv::from_pairs([("REEF__SMTP__HOSTT", "x")]);
900 let config = env_config("REEF");
901
902 let output = parse_env(&schema, &config, &env);
903
904 assert!(!output.unused_keys.is_empty());
905 }
906
907 #[test]
908 fn test_strict_mode_tracks_unknown_keys() {
909 let schema = Schema::from_shape(ArgsWithConfig::SHAPE).unwrap();
912 let env = MockEnv::from_pairs([("REEF__PORTT", "8080")]);
913 let config = env_config_strict("REEF");
914
915 let output = parse_env(&schema, &config, &env);
916
917 assert!(
919 !output.unused_keys.is_empty(),
920 "should track unknown key in unused_keys"
921 );
922 assert!(
923 output
924 .unused_keys
925 .iter()
926 .any(|uk| uk.key.join(".") == "portt"),
927 "unused_keys should contain 'portt': {:?}",
928 output.unused_keys
929 );
930
931 let errors: Vec<_> = output
933 .diagnostics
934 .iter()
935 .filter(|d| d.severity == Severity::Error)
936 .collect();
937 assert!(
938 errors.is_empty(),
939 "should not have error diagnostics at parse time, got: {:?}",
940 errors
941 );
942 }
943
944 #[test]
949 fn test_case_matching() {
950 let schema = Schema::from_shape(ArgsWithConfig::SHAPE).unwrap();
952 let env = MockEnv::from_pairs([("REEF__PORT", "8080")]);
953 let config = env_config("REEF");
954
955 let output = parse_env(&schema, &config, &env);
956
957 assert!(output.diagnostics.is_empty());
958 let value = output.value.expect("should have value");
959 assert!(get_nested(&value, &["config", "port"]).is_some());
961 }
962
963 #[test]
964 fn test_field_with_underscore() {
965 let schema = Schema::from_shape(ArgsWithNestedConfig::SHAPE).unwrap();
967 let env = MockEnv::from_pairs([("REEF__SMTP__CONNECTION_TIMEOUT", "30")]);
968 let config = env_config("REEF");
969
970 let output = parse_env(&schema, &config, &env);
971
972 assert!(output.diagnostics.is_empty());
973 let value = output.value.expect("should have value");
974 assert!(get_nested(&value, &["settings", "smtp", "connection_timeout"]).is_some());
975 }
976
977 #[test]
978 fn test_empty_value() {
979 let schema = Schema::from_shape(ArgsWithConfig::SHAPE).unwrap();
980 let env = MockEnv::from_pairs([("REEF__PORT", "")]);
981 let config = env_config("REEF");
982
983 let output = parse_env(&schema, &config, &env);
984
985 assert!(output.diagnostics.is_empty());
987 let value = output.value.expect("should have value");
988 let port = get_nested(&value, &["config", "port"]).expect("config.port");
989 assert_eq!(get_string(port), Some(""));
990 }
991
992 #[derive(Facet)]
997 struct ArgsWithoutConfig {
998 #[facet(args::named)]
999 verbose: bool,
1000 }
1001
1002 #[test]
1003 fn test_no_config_field_in_schema() {
1004 let schema = Schema::from_shape(ArgsWithoutConfig::SHAPE).unwrap();
1007 let env = MockEnv::from_pairs([("REEF__PORT", "8080")]);
1008 let config = env_config("REEF");
1009
1010 let output = parse_env(&schema, &config, &env);
1011
1012 assert!(!output.unused_keys.is_empty());
1014 }
1015
1016 #[derive(Facet)]
1021 struct CommonConfig {
1022 log_level: String,
1023 debug: bool,
1024 }
1025
1026 #[derive(Facet)]
1027 struct ServerConfigWithFlatten {
1028 port: u16,
1029 #[facet(flatten)]
1030 common: CommonConfig,
1031 }
1032
1033 #[derive(Facet)]
1034 struct ArgsWithFlattenConfig {
1035 #[facet(args::named)]
1036 verbose: bool,
1037
1038 #[facet(args::config)]
1039 config: ServerConfigWithFlatten,
1040 }
1041
1042 #[test]
1043 fn test_flatten_config_parses_flattened_field() {
1044 let schema = Schema::from_shape(ArgsWithFlattenConfig::SHAPE).unwrap();
1047 let env = MockEnv::from_pairs([("REEF__LOG_LEVEL", "debug")]);
1048 let config = env_config("REEF");
1049
1050 let output = parse_env(&schema, &config, &env);
1051
1052 assert!(
1053 output.diagnostics.is_empty(),
1054 "diagnostics: {:?}",
1055 output.diagnostics
1056 );
1057 assert!(
1058 output.unused_keys.is_empty(),
1059 "unused keys: {:?}",
1060 output.unused_keys
1061 );
1062
1063 let value = output.value.expect("should have value");
1064 let log_level = get_nested(&value, &["config", "log_level"]).expect("config.log_level");
1066 assert_eq!(get_string(log_level), Some("debug"));
1067 }
1068
1069 #[test]
1070 fn test_flatten_config_top_level_and_flattened() {
1071 let schema = Schema::from_shape(ArgsWithFlattenConfig::SHAPE).unwrap();
1073 let env = MockEnv::from_pairs([("REEF__PORT", "8080"), ("REEF__DEBUG", "true")]);
1075 let config = env_config("REEF");
1076
1077 let output = parse_env(&schema, &config, &env);
1078
1079 assert!(
1080 output.diagnostics.is_empty(),
1081 "diagnostics: {:?}",
1082 output.diagnostics
1083 );
1084 assert!(
1085 output.unused_keys.is_empty(),
1086 "unused keys: {:?}",
1087 output.unused_keys
1088 );
1089
1090 let value = output.value.expect("should have value");
1091 let port = get_nested(&value, &["config", "port"]).expect("config.port");
1092 assert_eq!(get_string(port), Some("8080"));
1093
1094 let debug = get_nested(&value, &["config", "debug"]).expect("config.debug");
1096 assert_eq!(get_string(debug), Some("true"));
1097 }
1098
1099 #[derive(Facet)]
1104 struct DeepConfig {
1105 trace: bool,
1106 }
1107
1108 #[derive(Facet)]
1109 struct MiddleConfig {
1110 #[facet(flatten)]
1111 deep: DeepConfig,
1112 verbose: bool,
1113 }
1114
1115 #[derive(Facet)]
1116 struct OuterConfigWithDeepFlatten {
1117 name: String,
1118 #[facet(flatten)]
1119 middle: MiddleConfig,
1120 }
1121
1122 #[derive(Facet)]
1123 struct ArgsWithDeepFlattenConfig {
1124 #[facet(args::config)]
1125 config: OuterConfigWithDeepFlatten,
1126 }
1127
1128 #[test]
1129 fn test_two_level_flatten_config() {
1130 let schema = Schema::from_shape(ArgsWithDeepFlattenConfig::SHAPE).unwrap();
1133 let env = MockEnv::from_pairs([
1134 ("REEF__NAME", "myapp"),
1135 ("REEF__VERBOSE", "true"),
1136 ("REEF__TRACE", "true"),
1137 ]);
1138 let config = env_config("REEF");
1139
1140 let output = parse_env(&schema, &config, &env);
1141
1142 assert!(
1143 output.diagnostics.is_empty(),
1144 "diagnostics: {:?}",
1145 output.diagnostics
1146 );
1147 assert!(
1148 output.unused_keys.is_empty(),
1149 "unused keys: {:?}",
1150 output.unused_keys
1151 );
1152
1153 let value = output.value.expect("should have value");
1154
1155 let name = get_nested(&value, &["config", "name"]).expect("config.name");
1157 assert_eq!(get_string(name), Some("myapp"));
1158
1159 let verbose = get_nested(&value, &["config", "verbose"]).expect("config.verbose");
1160 assert_eq!(get_string(verbose), Some("true"));
1161
1162 let trace = get_nested(&value, &["config", "trace"]).expect("config.trace");
1163 assert_eq!(get_string(trace), Some("true"));
1164 }
1165
1166 #[test]
1167 fn test_nested_path_rejected_for_flattened_field() {
1168 let schema = Schema::from_shape(ArgsWithFlattenConfig::SHAPE).unwrap();
1171 let env = MockEnv::from_pairs([("REEF__COMMON__LOG_LEVEL", "debug")]);
1172 let config = env_config("REEF");
1173
1174 let output = parse_env(&schema, &config, &env);
1175
1176 assert!(
1178 !output.unused_keys.is_empty(),
1179 "should reject nested path for flattened field"
1180 );
1181 assert!(
1182 output
1183 .unused_keys
1184 .iter()
1185 .any(|k| k.key.contains(&"common".to_string())),
1186 "unused key should contain 'common': {:?}",
1187 output.unused_keys
1188 );
1189 }
1190
1191 #[derive(Facet)]
1196 struct ConfigWithAlias {
1197 #[facet(args::env_alias = "DATABASE_URL")]
1199 database_url: String,
1200
1201 port: u16,
1203 }
1204
1205 #[derive(Facet)]
1206 struct ArgsWithAliasConfig {
1207 #[facet(args::config)]
1208 config: ConfigWithAlias,
1209 }
1210
1211 #[test]
1212 fn test_env_alias_basic() {
1213 let schema = Schema::from_shape(ArgsWithAliasConfig::SHAPE).unwrap();
1215 let env = MockEnv::from_pairs([("DATABASE_URL", "postgres://localhost/mydb")]);
1216 let config = env_config("REEF");
1217
1218 let output = parse_env(&schema, &config, &env);
1219
1220 assert!(
1221 output.diagnostics.is_empty(),
1222 "diagnostics: {:?}",
1223 output.diagnostics
1224 );
1225 let value = output.value.expect("should have value");
1226
1227 let db_url = get_nested(&value, &["config", "database_url"]).expect("config.database_url");
1228 assert_eq!(get_string(db_url), Some("postgres://localhost/mydb"));
1229 }
1230
1231 #[test]
1232 fn test_env_alias_prefixed_wins() {
1233 let schema = Schema::from_shape(ArgsWithAliasConfig::SHAPE).unwrap();
1235 let env = MockEnv::from_pairs([
1236 ("DATABASE_URL", "alias_value"),
1237 ("REEF__DATABASE_URL", "prefixed_value"),
1238 ]);
1239 let config = env_config("REEF");
1240
1241 let output = parse_env(&schema, &config, &env);
1242
1243 assert!(
1244 output.diagnostics.is_empty(),
1245 "diagnostics: {:?}",
1246 output.diagnostics
1247 );
1248 let value = output.value.expect("should have value");
1249
1250 let db_url = get_nested(&value, &["config", "database_url"]).expect("config.database_url");
1251 assert_eq!(get_string(db_url), Some("prefixed_value"));
1253 }
1254
1255 #[test]
1256 fn test_env_alias_only_alias_set() {
1257 let schema = Schema::from_shape(ArgsWithAliasConfig::SHAPE).unwrap();
1259 let env = MockEnv::from_pairs([("DATABASE_URL", "alias_value"), ("REEF__PORT", "8080")]);
1260 let config = env_config("REEF");
1261
1262 let output = parse_env(&schema, &config, &env);
1263
1264 assert!(
1265 output.diagnostics.is_empty(),
1266 "diagnostics: {:?}",
1267 output.diagnostics
1268 );
1269 let value = output.value.expect("should have value");
1270
1271 let db_url = get_nested(&value, &["config", "database_url"]).expect("config.database_url");
1272 assert_eq!(get_string(db_url), Some("alias_value"));
1273
1274 let port = get_nested(&value, &["config", "port"]).expect("config.port");
1275 assert_eq!(get_string(port), Some("8080"));
1276 }
1277
1278 #[derive(Facet)]
1279 struct ConfigWithMultipleAliases {
1280 #[facet(args::env_alias = "DATABASE_URL", args::env_alias = "DB_URL")]
1282 database_url: String,
1283 }
1284
1285 #[derive(Facet)]
1286 struct ArgsWithMultipleAliasConfig {
1287 #[facet(args::config)]
1288 config: ConfigWithMultipleAliases,
1289 }
1290
1291 #[test]
1292 fn test_env_alias_multiple_aliases_first_wins() {
1293 let schema = Schema::from_shape(ArgsWithMultipleAliasConfig::SHAPE).unwrap();
1295 let env = MockEnv::from_pairs([("DB_URL", "second_alias_value")]);
1297 let config = env_config("REEF");
1298
1299 let output = parse_env(&schema, &config, &env);
1300
1301 assert!(
1302 output.diagnostics.is_empty(),
1303 "diagnostics: {:?}",
1304 output.diagnostics
1305 );
1306 let value = output.value.expect("should have value");
1307
1308 let db_url = get_nested(&value, &["config", "database_url"]).expect("config.database_url");
1309 assert_eq!(get_string(db_url), Some("second_alias_value"));
1310 }
1311
1312 #[test]
1313 fn test_env_alias_provenance() {
1314 use crate::provenance::Provenance;
1316
1317 let schema = Schema::from_shape(ArgsWithAliasConfig::SHAPE).unwrap();
1318 let env = MockEnv::from_pairs([("DATABASE_URL", "postgres://localhost/mydb")]);
1319 let config = env_config("REEF");
1320
1321 let output = parse_env(&schema, &config, &env);
1322
1323 let value = output.value.expect("should have value");
1324 let db_url = get_nested(&value, &["config", "database_url"]).expect("config.database_url");
1325
1326 if let ConfigValue::String(s) = db_url {
1327 let prov = s.provenance.as_ref().expect("should have provenance");
1328 if let Provenance::Env { var, value } = prov {
1329 assert_eq!(var, "DATABASE_URL");
1331 assert_eq!(value, "postgres://localhost/mydb");
1332 } else {
1333 panic!("expected Env provenance");
1334 }
1335 } else {
1336 panic!("expected string");
1337 }
1338 }
1339
1340 #[derive(Facet)]
1341 struct NestedConfigWithAlias {
1342 db: DbConfig,
1343 }
1344
1345 #[derive(Facet)]
1346 struct DbConfig {
1347 #[facet(args::env_alias = "DATABASE_URL")]
1348 url: String,
1349 }
1350
1351 #[derive(Facet)]
1352 struct ArgsWithNestedAliasConfig {
1353 #[facet(args::config)]
1354 config: NestedConfigWithAlias,
1355 }
1356
1357 #[test]
1358 fn test_env_alias_in_nested_struct() {
1359 let schema = Schema::from_shape(ArgsWithNestedAliasConfig::SHAPE).unwrap();
1361 let env = MockEnv::from_pairs([("DATABASE_URL", "postgres://localhost/mydb")]);
1362 let config = env_config("REEF");
1363
1364 let output = parse_env(&schema, &config, &env);
1365
1366 assert!(
1367 output.diagnostics.is_empty(),
1368 "diagnostics: {:?}",
1369 output.diagnostics
1370 );
1371 let value = output.value.expect("should have value");
1372
1373 let url = get_nested(&value, &["config", "db", "url"]).expect("config.db.url");
1374 assert_eq!(get_string(url), Some("postgres://localhost/mydb"));
1375 }
1376
1377 #[test]
1378 fn test_env_alias_nested_prefixed_wins() {
1379 let schema = Schema::from_shape(ArgsWithNestedAliasConfig::SHAPE).unwrap();
1381 let env = MockEnv::from_pairs([
1382 ("DATABASE_URL", "alias_value"),
1383 ("REEF__DB__URL", "prefixed_value"),
1384 ]);
1385 let config = env_config("REEF");
1386
1387 let output = parse_env(&schema, &config, &env);
1388
1389 let value = output.value.expect("should have value");
1390 let url = get_nested(&value, &["config", "db", "url"]).expect("config.db.url");
1391 assert_eq!(get_string(url), Some("prefixed_value"));
1392 }
1393
1394 #[derive(Facet)]
1400 #[repr(u8)]
1401 #[allow(dead_code)]
1402 enum LogLevel {
1403 Debug,
1404 Info,
1405 Warn,
1406 Error,
1407 }
1408
1409 #[derive(Facet)]
1410 struct ConfigWithEnum {
1411 log_level: LogLevel,
1412 port: u16,
1413 }
1414
1415 #[derive(Facet)]
1416 struct ArgsWithEnumConfig {
1417 #[facet(args::config)]
1418 config: ConfigWithEnum,
1419 }
1420
1421 #[test]
1422 fn test_enum_valid_variant_no_warning() {
1423 let schema = Schema::from_shape(ArgsWithEnumConfig::SHAPE).unwrap();
1425 let env = MockEnv::from_pairs([("REEF__LOG_LEVEL", "Debug")]);
1426 let config = env_config("REEF");
1427
1428 let output = parse_env(&schema, &config, &env);
1429
1430 assert!(
1431 output.diagnostics.is_empty(),
1432 "valid enum variant should not produce warnings: {:?}",
1433 output.diagnostics
1434 );
1435
1436 let value = output.value.expect("should have value");
1437 let log_level = get_nested(&value, &["config", "log_level"]).expect("config.log_level");
1438 assert_eq!(get_string(log_level), Some("Debug"));
1439 }
1440
1441 #[test]
1442 fn test_enum_invalid_variant_produces_warning() {
1443 let schema = Schema::from_shape(ArgsWithEnumConfig::SHAPE).unwrap();
1445 let env = MockEnv::from_pairs([("REEF__LOG_LEVEL", "Debugg")]); let config = env_config("REEF");
1447
1448 let output = parse_env(&schema, &config, &env);
1449
1450 assert!(
1452 !output.diagnostics.is_empty(),
1453 "invalid enum variant should produce a warning"
1454 );
1455
1456 let warning = &output.diagnostics[0];
1458 assert!(
1459 warning.message.contains("Debugg"),
1460 "warning should mention the invalid value: {}",
1461 warning.message
1462 );
1463 assert!(
1464 warning.message.contains("Debug")
1465 && warning.message.contains("Info")
1466 && warning.message.contains("Warn")
1467 && warning.message.contains("Error"),
1468 "warning should list valid variants: {}",
1469 warning.message
1470 );
1471 assert!(
1473 warning.message.contains("Did you mean 'Debug'?"),
1474 "warning should suggest similar variant: {}",
1475 warning.message
1476 );
1477
1478 let value = output.value.expect("should have value");
1480 let log_level = get_nested(&value, &["config", "log_level"]).expect("config.log_level");
1481 assert_eq!(get_string(log_level), Some("Debugg"));
1482 }
1483
1484 #[derive(Facet)]
1485 struct ConfigWithOptionalEnum {
1486 log_level: Option<LogLevel>,
1487 }
1488
1489 #[derive(Facet)]
1490 struct ArgsWithOptionalEnumConfig {
1491 #[facet(args::config)]
1492 config: ConfigWithOptionalEnum,
1493 }
1494
1495 #[test]
1496 fn test_optional_enum_validation() {
1497 let schema = Schema::from_shape(ArgsWithOptionalEnumConfig::SHAPE).unwrap();
1499 let env = MockEnv::from_pairs([("REEF__LOG_LEVEL", "invalid")]);
1500 let config = env_config("REEF");
1501
1502 let output = parse_env(&schema, &config, &env);
1503
1504 assert!(
1506 !output.diagnostics.is_empty(),
1507 "invalid optional enum variant should produce a warning"
1508 );
1509 }
1510
1511 #[derive(Facet)]
1512 struct NestedConfigWithEnum {
1513 logging: LoggingConfig,
1514 }
1515
1516 #[derive(Facet)]
1517 struct LoggingConfig {
1518 level: LogLevel,
1519 }
1520
1521 #[derive(Facet)]
1522 struct ArgsWithNestedEnumConfig {
1523 #[facet(args::config)]
1524 config: NestedConfigWithEnum,
1525 }
1526
1527 #[test]
1528 fn test_nested_enum_validation() {
1529 let schema = Schema::from_shape(ArgsWithNestedEnumConfig::SHAPE).unwrap();
1531 let env = MockEnv::from_pairs([("REEF__LOGGING__LEVEL", "unknown")]);
1532 let config = env_config("REEF");
1533
1534 let output = parse_env(&schema, &config, &env);
1535
1536 assert!(
1538 !output.diagnostics.is_empty(),
1539 "invalid nested enum variant should produce a warning"
1540 );
1541 }
1542
1543 #[derive(Facet)]
1549 #[repr(u8)]
1550 #[allow(dead_code)]
1551 enum Storage {
1552 S3 { bucket: String, region: String },
1553 Gcp { project: String, zone: String },
1554 Local { path: String },
1555 }
1556
1557 #[derive(Facet)]
1558 struct ConfigWithEnumVariants {
1559 storage: Storage,
1560 port: u16,
1561 }
1562
1563 #[derive(Facet)]
1564 struct ArgsWithEnumVariantConfig {
1565 #[facet(args::config)]
1566 config: ConfigWithEnumVariants,
1567 }
1568
1569 #[test]
1570 fn test_enum_variant_field_single() {
1571 let schema = Schema::from_shape(ArgsWithEnumVariantConfig::SHAPE).unwrap();
1573 let env = MockEnv::from_pairs([("REEF__STORAGE__S3__BUCKET", "my-bucket")]);
1574 let config = env_config("REEF");
1575
1576 let output = parse_env(&schema, &config, &env);
1577
1578 assert!(
1579 output.diagnostics.is_empty(),
1580 "should not have diagnostics: {:?}",
1581 output.diagnostics
1582 );
1583 assert!(
1584 output.unused_keys.is_empty(),
1585 "should not have unused keys: {:?}",
1586 output.unused_keys
1587 );
1588
1589 let value = output.value.expect("should have value");
1590 let bucket = get_nested(&value, &["config", "storage", "S3", "bucket"])
1592 .expect("should have config.storage.S3.bucket");
1593 assert_eq!(get_string(bucket), Some("my-bucket"));
1594 }
1595
1596 #[test]
1597 fn test_enum_variant_field_multiple() {
1598 let schema = Schema::from_shape(ArgsWithEnumVariantConfig::SHAPE).unwrap();
1600 let env = MockEnv::from_pairs([
1601 ("REEF__STORAGE__S3__BUCKET", "my-bucket"),
1602 ("REEF__STORAGE__S3__REGION", "us-east-1"),
1603 ]);
1604 let config = env_config("REEF");
1605
1606 let output = parse_env(&schema, &config, &env);
1607
1608 assert!(
1609 output.diagnostics.is_empty(),
1610 "should not have diagnostics: {:?}",
1611 output.diagnostics
1612 );
1613 assert!(
1614 output.unused_keys.is_empty(),
1615 "should not have unused keys: {:?}",
1616 output.unused_keys
1617 );
1618
1619 let value = output.value.expect("should have value");
1620 let bucket = get_nested(&value, &["config", "storage", "S3", "bucket"])
1621 .expect("should have config.storage.S3.bucket");
1622 assert_eq!(get_string(bucket), Some("my-bucket"));
1623
1624 let region = get_nested(&value, &["config", "storage", "S3", "region"])
1625 .expect("should have config.storage.S3.region");
1626 assert_eq!(get_string(region), Some("us-east-1"));
1627 }
1628
1629 #[test]
1630 fn test_enum_variant_field_different_variant() {
1631 let schema = Schema::from_shape(ArgsWithEnumVariantConfig::SHAPE).unwrap();
1633 let env = MockEnv::from_pairs([("REEF__STORAGE__GCP__PROJECT", "my-project")]);
1634 let config = env_config("REEF");
1635
1636 let output = parse_env(&schema, &config, &env);
1637
1638 assert!(
1639 output.diagnostics.is_empty(),
1640 "should not have diagnostics: {:?}",
1641 output.diagnostics
1642 );
1643 assert!(
1644 output.unused_keys.is_empty(),
1645 "should not have unused keys: {:?}",
1646 output.unused_keys
1647 );
1648
1649 let value = output.value.expect("should have value");
1650 let project = get_nested(&value, &["config", "storage", "Gcp", "project"])
1651 .expect("should have config.storage.Gcp.project");
1652 assert_eq!(get_string(project), Some("my-project"));
1653 }
1654
1655 #[test]
1656 fn test_enum_variant_field_with_regular_field() {
1657 let schema = Schema::from_shape(ArgsWithEnumVariantConfig::SHAPE).unwrap();
1659 let env = MockEnv::from_pairs([
1660 ("REEF__STORAGE__S3__BUCKET", "my-bucket"),
1661 ("REEF__PORT", "8080"),
1662 ]);
1663 let config = env_config("REEF");
1664
1665 let output = parse_env(&schema, &config, &env);
1666
1667 assert!(
1668 output.diagnostics.is_empty(),
1669 "should not have diagnostics: {:?}",
1670 output.diagnostics
1671 );
1672 assert!(
1673 output.unused_keys.is_empty(),
1674 "should not have unused keys: {:?}",
1675 output.unused_keys
1676 );
1677
1678 let value = output.value.expect("should have value");
1679 let bucket = get_nested(&value, &["config", "storage", "S3", "bucket"])
1680 .expect("should have config.storage.S3.bucket");
1681 assert_eq!(get_string(bucket), Some("my-bucket"));
1682
1683 let port = get_nested(&value, &["config", "port"]).expect("should have config.port");
1684 assert_eq!(get_string(port), Some("8080"));
1685 }
1686
1687 #[test]
1688 fn test_enum_variant_unknown_variant_rejected() {
1689 let schema = Schema::from_shape(ArgsWithEnumVariantConfig::SHAPE).unwrap();
1691 let env = MockEnv::from_pairs([("REEF__STORAGE__AZURE__CONTAINER", "my-container")]);
1692 let config = env_config("REEF");
1693
1694 let output = parse_env(&schema, &config, &env);
1695
1696 assert!(
1698 !output.unused_keys.is_empty(),
1699 "unknown variant should produce unused key"
1700 );
1701 assert!(
1702 output
1703 .unused_keys
1704 .iter()
1705 .any(|k| k.key.iter().any(|s| s == "azure")),
1706 "unused key should mention azure: {:?}",
1707 output.unused_keys
1708 );
1709 }
1710
1711 #[test]
1712 fn test_enum_variant_unknown_field_rejected() {
1713 let schema = Schema::from_shape(ArgsWithEnumVariantConfig::SHAPE).unwrap();
1715 let env = MockEnv::from_pairs([("REEF__STORAGE__S3__UNKNOWN_FIELD", "value")]);
1716 let config = env_config("REEF");
1717
1718 let output = parse_env(&schema, &config, &env);
1719
1720 assert!(
1722 !output.unused_keys.is_empty(),
1723 "unknown field in variant should produce unused key"
1724 );
1725 }
1726
1727 #[derive(Facet)]
1728 struct ConfigWithOptionalEnumVariants {
1729 storage: Option<Storage>,
1730 }
1731
1732 #[derive(Facet)]
1733 struct ArgsWithOptionalEnumVariantConfig {
1734 #[facet(args::config)]
1735 config: ConfigWithOptionalEnumVariants,
1736 }
1737
1738 #[test]
1739 fn test_optional_enum_variant_field() {
1740 let schema = Schema::from_shape(ArgsWithOptionalEnumVariantConfig::SHAPE).unwrap();
1742 let env = MockEnv::from_pairs([("REEF__STORAGE__LOCAL__PATH", "/data")]);
1743 let config = env_config("REEF");
1744
1745 let output = parse_env(&schema, &config, &env);
1746
1747 assert!(
1748 output.diagnostics.is_empty(),
1749 "should not have diagnostics: {:?}",
1750 output.diagnostics
1751 );
1752 assert!(
1753 output.unused_keys.is_empty(),
1754 "should not have unused keys: {:?}",
1755 output.unused_keys
1756 );
1757
1758 let value = output.value.expect("should have value");
1759 let path = get_nested(&value, &["config", "storage", "Local", "path"])
1760 .expect("should have config.storage.Local.path");
1761 assert_eq!(get_string(path), Some("/data"));
1762 }
1763
1764 #[test]
1769 fn test_env_spans_are_assigned() {
1770 let schema = Schema::from_shape(ArgsWithConfig::SHAPE).unwrap();
1772 let env = MockEnv::from_pairs([("REEF__PORT", "8080"), ("REEF__HOST", "localhost")]);
1773 let config = env_config("REEF");
1774
1775 let output = parse_env(&schema, &config, &env);
1776
1777 assert!(
1779 output.source_text.is_some(),
1780 "source_text should be set when env vars are parsed"
1781 );
1782
1783 let source_text = output.source_text.as_ref().unwrap();
1784
1785 assert!(
1787 source_text.contains("REEF__PORT=\"8080\""),
1788 "source_text should contain REEF__PORT: {}",
1789 source_text
1790 );
1791 assert!(
1792 source_text.contains("REEF__HOST=\"localhost\""),
1793 "source_text should contain REEF__HOST: {}",
1794 source_text
1795 );
1796
1797 let value = output.value.expect("should have value");
1799 let port = get_nested(&value, &["config", "port"]).expect("config.port");
1800
1801 assert!(port.span().is_some(), "port should have a span");
1803
1804 let span = port.span().unwrap();
1806 let offset = span.offset as usize;
1807 let len = span.len as usize;
1808 let pointed_text = &source_text[offset..offset + len];
1809 assert_eq!(
1810 pointed_text, "8080",
1811 "span should point to the value in source_text"
1812 );
1813 }
1814
1815 #[test]
1816 fn test_env_spans_with_alias() {
1817 let schema = Schema::from_shape(ArgsWithAliasConfig::SHAPE).unwrap();
1819 let env = MockEnv::from_pairs([("DATABASE_URL", "postgres://localhost/db")]);
1820 let config = env_config("REEF");
1821
1822 let output = parse_env(&schema, &config, &env);
1823
1824 assert!(
1825 output.source_text.is_some(),
1826 "source_text should be set for aliased env vars"
1827 );
1828
1829 let source_text = output.source_text.as_ref().unwrap();
1830 assert!(
1831 source_text.contains("DATABASE_URL=\"postgres://localhost/db\""),
1832 "source_text should contain DATABASE_URL: {}",
1833 source_text
1834 );
1835
1836 let value = output.value.expect("should have value");
1837 let db_url = get_nested(&value, &["config", "database_url"]).expect("config.database_url");
1838
1839 let span = db_url.span().expect("db_url should have a span");
1840 let offset = span.offset as usize;
1841 let len = span.len as usize;
1842 let pointed_text = &source_text[offset..offset + len];
1843 assert_eq!(
1844 pointed_text, "postgres://localhost/db",
1845 "span should point to the value in source_text"
1846 );
1847 }
1848
1849 #[test]
1850 fn test_env_spans_no_env_vars_no_source_text() {
1851 let schema = Schema::from_shape(ArgsWithConfig::SHAPE).unwrap();
1853 let env = MockEnv::new();
1854 let config = env_config("REEF");
1855
1856 let output = parse_env(&schema, &config, &env);
1857
1858 assert!(
1860 output.source_text.is_none(),
1861 "source_text should be None when no env vars are parsed"
1862 );
1863 }
1864}