Skip to main content

foundry_config/providers/
ext.rs

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