1use std::collections::BTreeMap;
10
11#[cfg(feature = "load")]
12use adotenv::load::LoadedEnvironment;
13use adotenv::Document;
14use figment::{
15 value::{Dict, Map, Value},
16 Metadata, Profile, Provider,
17};
18
19#[derive(Debug, Clone)]
21pub struct AdotenvProvider {
22 values: BTreeMap<String, String>,
23 source: &'static str,
24}
25
26impl Default for AdotenvProvider {
27 fn default() -> Self {
28 Self {
29 values: BTreeMap::new(),
30 source: "empty",
31 }
32 }
33}
34
35impl AdotenvProvider {
36 #[must_use]
38 pub fn from_values(values: BTreeMap<String, String>) -> Self {
39 Self {
40 values,
41 source: "explicit values",
42 }
43 }
44
45 #[cfg(feature = "load")]
49 #[must_use]
50 pub fn from_environment(environment: &LoadedEnvironment) -> Self {
51 Self {
52 values: environment.values().clone(),
53 source: "loaded environment",
54 }
55 }
56
57 #[must_use]
63 pub fn from_document(document: &Document<'_>) -> Self {
64 let values = document
65 .effective_entries()
66 .map(|entry| (entry.key().to_string(), entry.value().to_string()))
67 .collect();
68 Self {
69 values,
70 source: "literal document",
71 }
72 }
73
74 #[must_use]
76 pub fn values(&self) -> &BTreeMap<String, String> {
77 &self.values
78 }
79}
80
81impl Provider for AdotenvProvider {
82 fn metadata(&self) -> Metadata {
83 Metadata::from("adotenv", self.source)
84 }
85
86 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
87 let mut dict = Dict::new();
88 for (key, value) in &self.values {
89 dict.insert(key.clone(), Value::from(value.as_str()));
90 }
91 let mut profiles = Map::new();
92 profiles.insert(Profile::Default, dict);
93 Ok(profiles)
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use adotenv::{parse_with, BareKeys, Dialect, Duplicates};
101 use figment::Figment;
102 use serde::Deserialize;
103 use std::collections::HashMap;
104
105 #[derive(Debug, Deserialize, PartialEq)]
106 struct Config {
107 host: String,
108 port: String,
109 }
110
111 #[test]
112 fn figment_provider_exposes_flat_values() {
113 let figment = Figment::new().merge(AdotenvProvider::from_values(BTreeMap::from([
114 ("host".to_string(), "localhost".to_string()),
115 ("port".to_string(), "8080".to_string()),
116 ])));
117 let config: Config = figment.extract().unwrap();
118 assert_eq!(
119 config,
120 Config {
121 host: "localhost".to_string(),
122 port: "8080".to_string(),
123 }
124 );
125 }
126
127 #[test]
128 fn figment_preserves_literal_key_spelling() {
129 let values: HashMap<String, String> = Figment::new()
130 .merge(AdotenvProvider::from_values(BTreeMap::from([
131 ("A.B".to_string(), "dotted".to_string()),
132 ("UPPER".to_string(), "upper".to_string()),
133 ])))
134 .extract()
135 .unwrap();
136 assert_eq!(values.get("A.B").map(String::as_str), Some("dotted"));
137 assert_eq!(values.get("UPPER").map(String::as_str), Some("upper"));
138 }
139
140 #[test]
141 fn document_provider_honors_duplicate_and_quote_rules() {
142 let (document, _) = parse_with(
143 "A='first'\nA=second\nB=\"two words\"\nC='$HOME'\nD=\"line\\nbreak\"\n",
144 Dialect::PORTABLE,
145 );
146 let provider = AdotenvProvider::from_document(&document);
147 assert_eq!(
148 provider.values().get("A").map(String::as_str),
149 Some("second")
150 );
151 assert_eq!(
152 provider.values().get("B").map(String::as_str),
153 Some("two words")
154 );
155 assert_eq!(
156 provider.values().get("C").map(String::as_str),
157 Some("$HOME")
158 );
159 assert_eq!(
160 provider.values().get("D").map(String::as_str),
161 Some("line\nbreak")
162 );
163
164 let first = Dialect::PORTABLE.with_duplicates(Duplicates::First);
165 let (document, _) = parse_with("A='first'\nA=second\n", first);
166 let provider = AdotenvProvider::from_document(&document);
167 assert_eq!(
168 provider.values().get("A").map(String::as_str),
169 Some("first")
170 );
171 }
172
173 #[test]
174 fn document_provider_honors_bare_and_nonparticipating_rules() {
175 let empty = Dialect::PORTABLE.with_bare_keys(BareKeys::Empty);
176 let (document, _) = parse_with("A=first\nA\nB\n", empty);
177 let provider = AdotenvProvider::from_document(&document);
178 assert_eq!(provider.values().get("A").map(String::as_str), Some(""));
179 assert_eq!(provider.values().get("B").map(String::as_str), Some(""));
180
181 let ignore = Dialect::PORTABLE.with_bare_keys(BareKeys::Ignore);
182 let (document, _) = parse_with("A=first\nA\nB\n", ignore);
183 let provider = AdotenvProvider::from_document(&document);
184 assert_eq!(
185 provider.values().get("A").map(String::as_str),
186 Some("first")
187 );
188 assert!(!provider.values().contains_key("B"));
189 }
190
191 #[cfg(feature = "load")]
192 #[test]
193 fn environment_provider_copies_final_values() {
194 let path = std::env::temp_dir().join(format!(
195 "adotenv-figment-environment-{}.env",
196 std::process::id()
197 ));
198 std::fs::write(&path, "A=from-file\nA=final\nB=two\n").unwrap();
199 let environment = adotenv::load::load_files([&path], Dialect::PORTABLE).unwrap();
200 let provider = AdotenvProvider::from_environment(&environment);
201 assert_eq!(
202 provider.values().get("A").map(String::as_str),
203 Some("final")
204 );
205 assert_eq!(provider.values().get("B").map(String::as_str), Some("two"));
206 let _ = std::fs::remove_file(path);
207 }
208}
209
210#[cfg(test)]
211mod typed_tests {
212 use super::*;
213 use figment::Figment;
214 use serde::Deserialize;
215
216 #[derive(Debug, Deserialize)]
217 #[allow(dead_code)]
218 struct NumericConfig {
219 port: u16,
220 }
221
222 #[derive(Debug, Deserialize, PartialEq)]
223 struct StringConfig {
224 port: String,
225 }
226
227 #[test]
228 fn numeric_values_require_explicit_conversion() {
229 let figment = Figment::new().merge(AdotenvProvider::from_values(BTreeMap::from([(
230 "port".to_string(),
231 "8080".to_string(),
232 )])));
233
234 let direct: Result<NumericConfig, _> = figment.extract();
235 assert!(direct.is_err());
236
237 let raw: StringConfig = figment.extract().unwrap();
238 assert_eq!(raw.port.parse::<u16>().unwrap(), 8080);
239 }
240}