foundry_config/providers/
ext.rs

1use crate::{utils, Config};
2use figment::{
3    providers::{Env, Format, Toml},
4    value::{Dict, Map, Value},
5    Error, Figment, Metadata, Profile, Provider,
6};
7use foundry_compilers::ProjectPathsConfig;
8use inflector::Inflector;
9use std::path::{Path, PathBuf};
10
11pub(crate) trait ProviderExt: Provider + Sized {
12    fn rename(
13        self,
14        from: impl Into<Profile>,
15        to: impl Into<Profile>,
16    ) -> RenameProfileProvider<Self> {
17        RenameProfileProvider::new(self, from, to)
18    }
19
20    fn wrap(
21        self,
22        wrapping_key: impl Into<Profile>,
23        profile: impl Into<Profile>,
24    ) -> WrapProfileProvider<Self> {
25        WrapProfileProvider::new(self, wrapping_key, profile)
26    }
27
28    fn strict_select(
29        self,
30        profiles: impl IntoIterator<Item = impl Into<Profile>>,
31    ) -> OptionalStrictProfileProvider<Self> {
32        OptionalStrictProfileProvider::new(self, profiles)
33    }
34
35    fn fallback(
36        self,
37        profile: impl Into<Profile>,
38        fallback: impl Into<Profile>,
39    ) -> FallbackProfileProvider<Self> {
40        FallbackProfileProvider::new(self, profile, fallback)
41    }
42}
43
44impl<P: Provider> ProviderExt for P {}
45
46/// A convenience provider to retrieve a toml file.
47/// This will return an error if the env var is set but the file does not exist
48pub(crate) struct TomlFileProvider {
49    pub env_var: Option<&'static str>,
50    pub default: PathBuf,
51    pub cache: Option<Result<Map<Profile, Dict>, Error>>,
52}
53
54impl TomlFileProvider {
55    pub(crate) fn new(env_var: Option<&'static str>, default: impl Into<PathBuf>) -> Self {
56        Self { env_var, default: default.into(), cache: None }
57    }
58
59    fn env_val(&self) -> Option<String> {
60        self.env_var.and_then(Env::var)
61    }
62
63    fn file(&self) -> PathBuf {
64        self.env_val().map(PathBuf::from).unwrap_or_else(|| self.default.clone())
65    }
66
67    fn is_missing(&self) -> bool {
68        if let Some(file) = self.env_val() {
69            let path = Path::new(&file);
70            if !path.exists() {
71                return true;
72            }
73        }
74        false
75    }
76
77    pub(crate) fn cached(mut self) -> Self {
78        self.cache = Some(self.read());
79        self
80    }
81
82    fn read(&self) -> Result<Map<Profile, Dict>, Error> {
83        use serde::de::Error as _;
84        if let Some(file) = self.env_val() {
85            let path = Path::new(&file);
86            if !path.exists() {
87                return Err(Error::custom(format!(
88                    "Config file `{}` set in env var `{}` does not exist",
89                    file,
90                    self.env_var.unwrap()
91                )));
92            }
93            Toml::file(file)
94        } else {
95            Toml::file(&self.default)
96        }
97        .nested()
98        .data()
99    }
100}
101
102impl Provider for TomlFileProvider {
103    fn metadata(&self) -> Metadata {
104        if self.is_missing() {
105            Metadata::named("TOML file provider")
106        } else {
107            Toml::file(self.file()).nested().metadata()
108        }
109    }
110
111    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
112        if let Some(cache) = self.cache.as_ref() {
113            cache.clone()
114        } else {
115            self.read()
116        }
117    }
118}
119
120/// A Provider that ensures all keys are snake case if they're not standalone sections, See
121/// `Config::STANDALONE_SECTIONS`
122pub(crate) struct ForcedSnakeCaseData<P>(pub(crate) P);
123
124impl<P: Provider> Provider for ForcedSnakeCaseData<P> {
125    fn metadata(&self) -> Metadata {
126        self.0.metadata()
127    }
128
129    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
130        let mut map = Map::new();
131        for (profile, dict) in self.0.data()? {
132            if Config::STANDALONE_SECTIONS.contains(&profile.as_ref()) {
133                // don't force snake case for keys in standalone sections
134                map.insert(profile, dict);
135                continue;
136            }
137            map.insert(profile, dict.into_iter().map(|(k, v)| (k.to_snake_case(), v)).collect());
138        }
139        Ok(map)
140    }
141}
142
143/// A Provider that handles breaking changes in toml files
144pub(crate) struct BackwardsCompatTomlProvider<P>(pub(crate) P);
145
146impl<P: Provider> Provider for BackwardsCompatTomlProvider<P> {
147    fn metadata(&self) -> Metadata {
148        self.0.metadata()
149    }
150
151    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
152        let mut map = Map::new();
153        let solc_env = std::env::var("FOUNDRY_SOLC_VERSION")
154            .or_else(|_| std::env::var("DAPP_SOLC_VERSION"))
155            .map(Value::from)
156            .ok();
157        for (profile, mut dict) in self.0.data()? {
158            if let Some(v) = solc_env.clone() {
159                // ENV var takes precedence over config file
160                dict.insert("solc".to_string(), v);
161            } else if let Some(v) = dict.remove("solc_version") {
162                // only insert older variant if not already included
163                if !dict.contains_key("solc") {
164                    dict.insert("solc".to_string(), v);
165                }
166            }
167
168            if let Some(v) = dict.remove("odyssey") {
169                dict.insert("odyssey".to_string(), v);
170            }
171            map.insert(profile, dict);
172        }
173        Ok(map)
174    }
175}
176
177/// A provider that sets the `src` and `output` path depending on their existence.
178pub(crate) struct DappHardhatDirProvider<'a>(pub(crate) &'a Path);
179
180impl Provider for DappHardhatDirProvider<'_> {
181    fn metadata(&self) -> Metadata {
182        Metadata::named("Dapp Hardhat dir compat")
183    }
184
185    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
186        let mut dict = Dict::new();
187        dict.insert(
188            "src".to_string(),
189            ProjectPathsConfig::find_source_dir(self.0)
190                .file_name()
191                .unwrap()
192                .to_string_lossy()
193                .to_string()
194                .into(),
195        );
196        dict.insert(
197            "out".to_string(),
198            ProjectPathsConfig::find_artifacts_dir(self.0)
199                .file_name()
200                .unwrap()
201                .to_string_lossy()
202                .to_string()
203                .into(),
204        );
205
206        // detect libs folders:
207        //   if `lib` _and_ `node_modules` exists: include both
208        //   if only `node_modules` exists: include `node_modules`
209        //   include `lib` otherwise
210        let mut libs = vec![];
211        let node_modules = self.0.join("node_modules");
212        let lib = self.0.join("lib");
213        if node_modules.exists() {
214            if lib.exists() {
215                libs.push(lib.file_name().unwrap().to_string_lossy().to_string());
216            }
217            libs.push(node_modules.file_name().unwrap().to_string_lossy().to_string());
218        } else {
219            libs.push(lib.file_name().unwrap().to_string_lossy().to_string());
220        }
221
222        dict.insert("libs".to_string(), libs.into());
223
224        Ok(Map::from([(Config::selected_profile(), dict)]))
225    }
226}
227
228/// A provider that checks for DAPP_ env vars that are named differently than FOUNDRY_
229pub(crate) struct DappEnvCompatProvider;
230
231impl Provider for DappEnvCompatProvider {
232    fn metadata(&self) -> Metadata {
233        Metadata::named("Dapp env compat")
234    }
235
236    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
237        use serde::de::Error as _;
238        use std::env;
239
240        let mut dict = Dict::new();
241        if let Ok(val) = env::var("DAPP_TEST_NUMBER") {
242            dict.insert(
243                "block_number".to_string(),
244                val.parse::<u64>().map_err(figment::Error::custom)?.into(),
245            );
246        }
247        if let Ok(val) = env::var("DAPP_TEST_ADDRESS") {
248            dict.insert("sender".to_string(), val.into());
249        }
250        if let Ok(val) = env::var("DAPP_FORK_BLOCK") {
251            dict.insert(
252                "fork_block_number".to_string(),
253                val.parse::<u64>().map_err(figment::Error::custom)?.into(),
254            );
255        } else if let Ok(val) = env::var("DAPP_TEST_NUMBER") {
256            dict.insert(
257                "fork_block_number".to_string(),
258                val.parse::<u64>().map_err(figment::Error::custom)?.into(),
259            );
260        }
261        if let Ok(val) = env::var("DAPP_TEST_TIMESTAMP") {
262            dict.insert(
263                "block_timestamp".to_string(),
264                val.parse::<u64>().map_err(figment::Error::custom)?.into(),
265            );
266        }
267        if let Ok(val) = env::var("DAPP_BUILD_OPTIMIZE_RUNS") {
268            dict.insert(
269                "optimizer_runs".to_string(),
270                val.parse::<u64>().map_err(figment::Error::custom)?.into(),
271            );
272        }
273        if let Ok(val) = env::var("DAPP_BUILD_OPTIMIZE") {
274            // Activate Solidity optimizer (0 or 1)
275            let val = val.parse::<u8>().map_err(figment::Error::custom)?;
276            if val > 1 {
277                return Err(
278                    format!("Invalid $DAPP_BUILD_OPTIMIZE value `{val}`, expected 0 or 1").into()
279                );
280            }
281            dict.insert("optimizer".to_string(), (val == 1).into());
282        }
283
284        // libraries in env vars either as `[..]` or single string separated by comma
285        if let Ok(val) = env::var("DAPP_LIBRARIES").or_else(|_| env::var("FOUNDRY_LIBRARIES")) {
286            dict.insert("libraries".to_string(), utils::to_array_value(&val)?);
287        }
288
289        let mut fuzz_dict = Dict::new();
290        if let Ok(val) = env::var("DAPP_TEST_FUZZ_RUNS") {
291            fuzz_dict.insert(
292                "runs".to_string(),
293                val.parse::<u32>().map_err(figment::Error::custom)?.into(),
294            );
295        }
296        dict.insert("fuzz".to_string(), fuzz_dict.into());
297
298        let mut invariant_dict = Dict::new();
299        if let Ok(val) = env::var("DAPP_TEST_DEPTH") {
300            invariant_dict.insert(
301                "depth".to_string(),
302                val.parse::<u32>().map_err(figment::Error::custom)?.into(),
303            );
304        }
305        dict.insert("invariant".to_string(), invariant_dict.into());
306
307        Ok(Map::from([(Config::selected_profile(), dict)]))
308    }
309}
310
311/// Renames a profile from `from` to `to`.
312///
313/// For example given:
314///
315/// ```toml
316/// [from]
317/// key = "value"
318/// ```
319///
320/// RenameProfileProvider will output
321///
322/// ```toml
323/// [to]
324/// key = "value"
325/// ```
326pub(crate) struct RenameProfileProvider<P> {
327    provider: P,
328    from: Profile,
329    to: Profile,
330}
331
332impl<P> RenameProfileProvider<P> {
333    pub(crate) fn new(provider: P, from: impl Into<Profile>, to: impl Into<Profile>) -> Self {
334        Self { provider, from: from.into(), to: to.into() }
335    }
336}
337
338impl<P: Provider> Provider for RenameProfileProvider<P> {
339    fn metadata(&self) -> Metadata {
340        self.provider.metadata()
341    }
342    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
343        let mut data = self.provider.data()?;
344        if let Some(data) = data.remove(&self.from) {
345            return Ok(Map::from([(self.to.clone(), data)]));
346        }
347        Ok(Default::default())
348    }
349    fn profile(&self) -> Option<Profile> {
350        Some(self.to.clone())
351    }
352}
353
354/// Unwraps a profile reducing the key depth
355///
356/// For example given:
357///
358/// ```toml
359/// [wrapping_key.profile]
360/// key = "value"
361/// ```
362///
363/// UnwrapProfileProvider will output:
364///
365/// ```toml
366/// [profile]
367/// key = "value"
368/// ```
369struct UnwrapProfileProvider<P> {
370    provider: P,
371    wrapping_key: Profile,
372    profile: Profile,
373}
374
375impl<P> UnwrapProfileProvider<P> {
376    pub fn new(provider: P, wrapping_key: impl Into<Profile>, profile: impl Into<Profile>) -> Self {
377        Self { provider, wrapping_key: wrapping_key.into(), profile: profile.into() }
378    }
379}
380
381impl<P: Provider> Provider for UnwrapProfileProvider<P> {
382    fn metadata(&self) -> Metadata {
383        self.provider.metadata()
384    }
385    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
386        self.provider.data().and_then(|mut data| {
387            if let Some(profiles) = data.remove(&self.wrapping_key) {
388                for (profile_str, profile_val) in profiles {
389                    let profile = Profile::new(&profile_str);
390                    if profile != self.profile {
391                        continue;
392                    }
393                    match profile_val {
394                        Value::Dict(_, dict) => return Ok(profile.collect(dict)),
395                        bad_val => {
396                            let mut err = Error::from(figment::error::Kind::InvalidType(
397                                bad_val.to_actual(),
398                                "dict".into(),
399                            ));
400                            err.metadata = Some(self.provider.metadata());
401                            err.profile = Some(self.profile.clone());
402                            return Err(err);
403                        }
404                    }
405                }
406            }
407            Ok(Default::default())
408        })
409    }
410    fn profile(&self) -> Option<Profile> {
411        Some(self.profile.clone())
412    }
413}
414
415/// Wraps a profile in another profile
416///
417/// For example given:
418///
419/// ```toml
420/// [profile]
421/// key = "value"
422/// ```
423///
424/// WrapProfileProvider will output:
425///
426/// ```toml
427/// [wrapping_key.profile]
428/// key = "value"
429/// ```
430pub(crate) struct WrapProfileProvider<P> {
431    provider: P,
432    wrapping_key: Profile,
433    profile: Profile,
434}
435
436impl<P> WrapProfileProvider<P> {
437    pub fn new(provider: P, wrapping_key: impl Into<Profile>, profile: impl Into<Profile>) -> Self {
438        Self { provider, wrapping_key: wrapping_key.into(), profile: profile.into() }
439    }
440}
441
442impl<P: Provider> Provider for WrapProfileProvider<P> {
443    fn metadata(&self) -> Metadata {
444        self.provider.metadata()
445    }
446    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
447        if let Some(inner) = self.provider.data()?.remove(&self.profile) {
448            let value = Value::from(inner);
449            let dict = [(self.profile.to_string().to_snake_case(), value)].into_iter().collect();
450            Ok(self.wrapping_key.collect(dict))
451        } else {
452            Ok(Default::default())
453        }
454    }
455    fn profile(&self) -> Option<Profile> {
456        Some(self.profile.clone())
457    }
458}
459
460/// Extracts the profile from the `profile` key and using the original key as backup, merging
461/// values where necessary
462///
463/// For example given:
464///
465/// ```toml
466/// [profile.cool]
467/// key = "value"
468///
469/// [cool]
470/// key2 = "value2"
471/// ```
472///
473/// OptionalStrictProfileProvider will output:
474///
475/// ```toml
476/// [cool]
477/// key = "value"
478/// key2 = "value2"
479/// ```
480///
481/// And emit a deprecation warning
482pub(crate) struct OptionalStrictProfileProvider<P> {
483    provider: P,
484    profiles: Vec<Profile>,
485}
486
487impl<P> OptionalStrictProfileProvider<P> {
488    pub const PROFILE_PROFILE: Profile = Profile::const_new("profile");
489
490    pub fn new(provider: P, profiles: impl IntoIterator<Item = impl Into<Profile>>) -> Self {
491        Self { provider, profiles: profiles.into_iter().map(|profile| profile.into()).collect() }
492    }
493}
494
495impl<P: Provider> Provider for OptionalStrictProfileProvider<P> {
496    fn metadata(&self) -> Metadata {
497        self.provider.metadata()
498    }
499    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
500        let mut figment = Figment::from(&self.provider);
501        for profile in &self.profiles {
502            figment = figment.merge(UnwrapProfileProvider::new(
503                &self.provider,
504                Self::PROFILE_PROFILE,
505                profile.clone(),
506            ));
507        }
508        figment.data().map_err(|err| {
509            // figment does tag metadata and tries to map metadata to an error, since we use a new
510            // figment in this provider this new figment does not know about the metadata of the
511            // provider and can't map the metadata to the error. Therefore we return the root error
512            // if this error originated in the provider's data.
513            if let Err(root_err) = self.provider.data() {
514                return root_err;
515            }
516            err
517        })
518    }
519    fn profile(&self) -> Option<Profile> {
520        self.profiles.last().cloned()
521    }
522}
523
524/// Extracts the profile from the `profile` key and sets unset values according to the fallback
525/// provider
526pub struct FallbackProfileProvider<P> {
527    provider: P,
528    profile: Profile,
529    fallback: Profile,
530}
531
532impl<P> FallbackProfileProvider<P> {
533    /// Creates a new fallback profile provider.
534    pub fn new(provider: P, profile: impl Into<Profile>, fallback: impl Into<Profile>) -> Self {
535        Self { provider, profile: profile.into(), fallback: fallback.into() }
536    }
537}
538
539impl<P: Provider> Provider for FallbackProfileProvider<P> {
540    fn metadata(&self) -> Metadata {
541        self.provider.metadata()
542    }
543
544    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
545        let data = self.provider.data()?;
546        if let Some(fallback) = data.get(&self.fallback) {
547            let mut inner = data.get(&self.profile).cloned().unwrap_or_default();
548            for (k, v) in fallback {
549                if !inner.contains_key(k) {
550                    inner.insert(k.to_owned(), v.clone());
551                }
552            }
553            Ok(self.profile.collect(inner))
554        } else {
555            Ok(data)
556        }
557    }
558
559    fn profile(&self) -> Option<Profile> {
560        Some(self.profile.clone())
561    }
562}