Skip to main content

foundry_config/providers/
ext.rs

1use crate::{Config, extend, 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::{
10    cell::OnceCell,
11    path::{Path, PathBuf},
12};
13
14pub(crate) trait ProviderExt: Provider + Sized {
15    fn rename(
16        self,
17        from: impl Into<Profile>,
18        to: impl Into<Profile>,
19    ) -> RenameProfileProvider<Self> {
20        RenameProfileProvider::new(self, from, to)
21    }
22
23    fn wrap(
24        self,
25        wrapping_key: impl Into<Profile>,
26        profile: impl Into<Profile>,
27    ) -> WrapProfileProvider<Self> {
28        WrapProfileProvider::new(self, wrapping_key, profile)
29    }
30
31    fn strict_select(
32        self,
33        profiles: impl IntoIterator<Item = impl Into<Profile>>,
34    ) -> OptionalStrictProfileProvider<Self> {
35        OptionalStrictProfileProvider::new(self, profiles)
36    }
37
38    fn fallback(
39        self,
40        profile: impl Into<Profile>,
41        fallback: impl Into<Profile>,
42    ) -> FallbackProfileProvider<Self> {
43        FallbackProfileProvider::new(self, profile, fallback)
44    }
45
46    fn legacy_labels(self) -> LegacyLabelsProvider<Self> {
47        LegacyLabelsProvider(self)
48    }
49}
50
51impl<P: Provider> ProviderExt for P {}
52
53/// A convenience provider to retrieve a toml file.
54/// This will return an error if the env var is set but the file does not exist
55pub(crate) struct TomlFileProvider {
56    env_var: Option<&'static str>,
57    env_val: OnceCell<Option<String>>,
58    default: PathBuf,
59    cache: OnceCell<Result<Map<Profile, Dict>, Error>>,
60}
61
62impl TomlFileProvider {
63    pub(crate) const fn new(env_var: Option<&'static str>, default: PathBuf) -> Self {
64        Self { env_var, env_val: OnceCell::new(), default, cache: OnceCell::new() }
65    }
66
67    fn env_val(&self) -> Option<&str> {
68        self.env_val.get_or_init(|| self.env_var.and_then(Env::var)).as_deref()
69    }
70
71    pub(crate) fn file(&self) -> PathBuf {
72        self.env_val().map(PathBuf::from).unwrap_or_else(|| self.default.clone())
73    }
74
75    fn is_missing(&self) -> bool {
76        if let Some(file) = self.env_val() {
77            let path = Path::new(&file);
78            if !path.exists() {
79                return true;
80            }
81        }
82        false
83    }
84
85    /// Reads and processes the TOML configuration file, handling inheritance if configured.
86    fn read(&self) -> Result<Map<Profile, Dict>, Error> {
87        use serde::de::Error as _;
88
89        // Get the config file path and validate it exists
90        let local_path = self.file();
91        if !local_path.exists() {
92            if let Some(file) = self.env_val() {
93                return Err(Error::custom(format!(
94                    "Config file `{}` set in env var `{}` does not exist",
95                    file,
96                    self.env_var.unwrap()
97                )));
98            }
99            return Ok(Map::new());
100        }
101
102        // Create a provider for the local config file
103        let local_provider = Toml::file(local_path.clone()).nested();
104
105        // Parse the local config to check for extends field
106        let local_path_str = local_path.to_string_lossy();
107        let local_content = std::fs::read_to_string(&local_path)
108            .map_err(|e| Error::custom(e.to_string()).with_path(&local_path_str))?;
109        let partial_config: extend::ExtendsPartialConfig = toml::from_str(&local_content)
110            .map_err(|e| Error::custom(e.to_string()).with_path(&local_path_str))?;
111
112        // Check if the currently active profile has an 'extends' field
113        let selected_profile = Config::selected_profile();
114        let extends_config = partial_config.profile.as_ref().and_then(|profiles| {
115            let profile_str = selected_profile.to_string();
116            profiles.get(&profile_str).and_then(|cfg| cfg.extends.as_ref())
117        });
118
119        // If inheritance is configured, load and merge the base config
120        if let Some(extends_config) = extends_config {
121            let extends_path = extends_config.path();
122            let extends_strategy = extends_config.strategy();
123            let relative_base_path = PathBuf::from(extends_path);
124            let local_dir = local_path.parent().ok_or_else(|| {
125                Error::custom(format!(
126                    "Could not determine parent directory of config file: {}",
127                    local_path.display()
128                ))
129            })?;
130
131            // `extends` takes a file path, but a profile name is an easy mistake to make.
132            let names_profile = partial_config
133                .profile
134                .as_ref()
135                .is_some_and(|profiles| profiles.contains_key(extends_path));
136            let base_path =
137                foundry_compilers::utils::canonicalize(local_dir.join(&relative_base_path))
138                    .map_err(|e| {
139                        let hint = if names_profile {
140                            "; `extends` takes the path of another config file, and every \
141                             profile already inherits from `[profile.default]`"
142                        } else {
143                            ""
144                        };
145                        Error::custom(format!(
146                            "Failed to resolve inherited config path: {}: {e}{hint}",
147                            relative_base_path.display()
148                        ))
149                    })?;
150
151            // Validate the base config file exists
152            if !base_path.is_file() {
153                return Err(Error::custom(format!(
154                    "Inherited config file does not exist or is not a file: {}",
155                    base_path.display()
156                )));
157            }
158
159            // Prevent self-inheritance which would cause infinite recursion
160            if foundry_compilers::utils::canonicalize(&local_path).ok().as_ref() == Some(&base_path)
161            {
162                return Err(Error::custom(format!(
163                    "Config file {} cannot inherit from itself.",
164                    local_path.display()
165                )));
166            }
167
168            // Parse the base config to check for nested inheritance
169            let base_path_str = base_path.to_string_lossy();
170            let base_content = std::fs::read_to_string(&base_path)
171                .map_err(|e| Error::custom(e.to_string()).with_path(&base_path_str))?;
172            let base_partial: extend::ExtendsPartialConfig = toml::from_str(&base_content)
173                .map_err(|e| Error::custom(e.to_string()).with_path(&base_path_str))?;
174
175            // Check if the base file's same profile also has extends (nested inheritance)
176            let base_extends = base_partial
177                .profile
178                .as_ref()
179                .and_then(|profiles| {
180                    let profile_str = selected_profile.to_string();
181                    profiles.get(&profile_str)
182                })
183                .and_then(|profile| profile.extends.as_ref());
184
185            // Prevent nested inheritance to avoid complexity and potential cycles
186            if base_extends.is_some() {
187                return Err(Error::custom(format!(
188                    "Nested inheritance is not allowed. Base file '{}' cannot have an 'extends' field in profile '{selected_profile}'.",
189                    base_path.display()
190                )));
191            }
192
193            // Normalize standalone sections before merging so equivalent profile-qualified values
194            // have the same shape across inherited files.
195            let base_provider = NormalizeSymbolicProvider::new(
196                NormalizeTracingProvider::new(
197                    Toml::file(base_path).nested().legacy_labels(),
198                    selected_profile.clone(),
199                ),
200                selected_profile.clone(),
201            );
202            let local_provider = NormalizeSymbolicProvider::new(
203                NormalizeTracingProvider::new(
204                    local_provider.legacy_labels(),
205                    selected_profile.clone(),
206                ),
207                selected_profile.clone(),
208            );
209
210            // Apply the selected merge strategy
211            match extends_strategy {
212                extend::ExtendStrategy::ExtendArrays => {
213                    // Using 'admerge' strategy:
214                    // - Arrays are concatenated (base elements + local elements)
215                    // - Other values are replaced (local values override base values)
216                    // - The extends field is preserved in the final configuration
217                    Figment::new().merge(base_provider).admerge(local_provider).data()
218                }
219                extend::ExtendStrategy::ReplaceArrays => {
220                    // Using 'merge' strategy:
221                    // - Arrays are replaced entirely (local arrays replace base arrays)
222                    // - Other values are replaced (local values override base values)
223                    Figment::new().merge(base_provider).merge(local_provider).data()
224                }
225                extend::ExtendStrategy::NoCollision => {
226                    // Check for key collisions between base and local configs
227                    let base_data = base_provider.data()?;
228                    let local_data = local_provider.data()?;
229
230                    let profile_key = Profile::new("profile");
231                    if let (Some(local_profiles), Some(base_profiles)) =
232                        (local_data.get(&profile_key), base_data.get(&profile_key))
233                    {
234                        // Extract dicts for the selected profile
235                        let profile_str = selected_profile.to_string();
236                        let base_dict = base_profiles.get(&profile_str).and_then(|v| v.as_dict());
237                        let local_dict = local_profiles.get(&profile_str).and_then(|v| v.as_dict());
238
239                        // Find colliding keys
240                        if let (Some(local_dict), Some(base_dict)) = (local_dict, base_dict) {
241                            let collisions: Vec<&String> = local_dict
242                                .keys()
243                                .filter(|key| {
244                                    // Ignore the "extends" key as it's expected
245                                    *key != "extends" && base_dict.contains_key(*key)
246                                })
247                                .collect();
248
249                            if !collisions.is_empty() {
250                                return Err(Error::custom(format!(
251                                    "Key collision detected in profile '{profile_str}' when extending '{extends_path}'. \
252                                    Conflicting keys: {collisions:?}. Use 'extends.strategy' or 'extends_strategy' to specify how to handle conflicts."
253                                )));
254                            }
255                        }
256                    }
257
258                    // Safe to merge the configs without collisions
259                    Figment::new().merge(base_provider).merge(local_provider).data()
260                }
261            }
262        } else {
263            // No inheritance - return the local config as-is
264            local_provider.data()
265        }
266    }
267}
268
269struct NormalizeSymbolicProvider<P> {
270    provider: P,
271    selected_profile: Profile,
272}
273
274impl<P> NormalizeSymbolicProvider<P> {
275    const fn new(provider: P, selected_profile: Profile) -> Self {
276        Self { provider, selected_profile }
277    }
278}
279
280struct NormalizeTracingProvider<P> {
281    provider: P,
282    selected_profile: Profile,
283}
284
285impl<P> NormalizeTracingProvider<P> {
286    const fn new(provider: P, selected_profile: Profile) -> Self {
287        Self { provider, selected_profile }
288    }
289}
290
291impl<P: Provider> Provider for NormalizeTracingProvider<P> {
292    fn metadata(&self) -> Metadata {
293        self.provider.metadata()
294    }
295
296    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
297        let mut data = self.provider.data()?;
298        normalize_tracing_section(&mut data, &self.selected_profile);
299        Ok(data)
300    }
301
302    fn profile(&self) -> Option<Profile> {
303        self.provider.profile()
304    }
305}
306
307/// Moves the standalone tracing section into the selected profile before inherited configs are
308/// merged. The deprecated standalone labels section remains in place for warning generation and
309/// is normalized again after inheritance is resolved.
310fn normalize_tracing_section(data: &mut Map<Profile, Dict>, selected_profile: &Profile) {
311    let Some(tracing) = data.remove(&Profile::new("tracing")) else { return };
312
313    let profiles = data.entry(Profile::new(Config::PROFILE_SECTION)).or_default();
314    let profile =
315        profiles.entry(selected_profile.to_string()).or_insert_with(|| Value::from(Dict::new()));
316    let Value::Dict(_, profile) = profile else { return };
317
318    match (profile.get_mut("tracing"), tracing) {
319        (Some(Value::Dict(_, profile_tracing)), tracing) => {
320            merge_missing(profile_tracing, tracing);
321        }
322        (None, tracing) => {
323            profile.insert("tracing".to_string(), Value::from(tracing));
324        }
325        _ => {}
326    }
327}
328
329impl<P: Provider> Provider for NormalizeSymbolicProvider<P> {
330    fn metadata(&self) -> Metadata {
331        self.provider.metadata()
332    }
333
334    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
335        let mut data = self.provider.data()?;
336        normalize_symbolic_section(&mut data, &self.selected_profile);
337        Ok(data)
338    }
339
340    fn profile(&self) -> Option<Profile> {
341        self.provider.profile()
342    }
343}
344
345/// Moves the standalone symbolic section into the selected profile before inherited configs are
346/// merged.
347///
348/// This gives equivalent standalone and profile-qualified keys the same shape, so source
349/// precedence and collision detection apply consistently across inherited files.
350fn normalize_symbolic_section(data: &mut Map<Profile, Dict>, selected_profile: &Profile) {
351    let Some(symbolic) = data.remove(&Profile::new("symbolic")) else { return };
352
353    let profiles = data.entry(Profile::new(Config::PROFILE_SECTION)).or_default();
354    let profile =
355        profiles.entry(selected_profile.to_string()).or_insert_with(|| Value::from(Dict::new()));
356    let Value::Dict(_, profile) = profile else { return };
357
358    match (profile.get_mut("symbolic"), symbolic) {
359        (Some(Value::Dict(_, profile_symbolic)), symbolic) => {
360            merge_missing(profile_symbolic, symbolic);
361        }
362        (None, symbolic) => {
363            profile.insert("symbolic".to_string(), Value::from(symbolic));
364        }
365        _ => {}
366    }
367}
368
369/// Recursively fills missing values while preserving values from the higher-precedence source.
370fn merge_missing(target: &mut Dict, fallback: Dict) {
371    for (key, value) in fallback {
372        match (target.get_mut(&key), value) {
373            (Some(Value::Dict(_, target)), Value::Dict(_, fallback)) => {
374                merge_missing(target, fallback);
375            }
376            (None, value) => {
377                target.insert(key, value);
378            }
379            _ => {}
380        }
381    }
382}
383
384impl Provider for TomlFileProvider {
385    fn metadata(&self) -> Metadata {
386        if self.is_missing() {
387            Metadata::named("TOML file provider")
388        } else {
389            Toml::file(self.file()).nested().metadata()
390        }
391    }
392
393    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
394        self.cache.get_or_init(|| self.read()).clone()
395    }
396}
397
398/// A Provider that ensures all keys are snake case if they're not standalone sections. See
399/// `Config::STANDALONE_SECTIONS`
400///
401/// For the `[profile]` section, profile names (like `ci-venom`) are preserved as-is,
402/// but the top-level config keys within each profile are still converted to snake_case.
403pub(crate) struct ForcedSnakeCaseData<P>(pub(crate) P);
404
405impl<P: Provider> Provider for ForcedSnakeCaseData<P> {
406    fn metadata(&self) -> Metadata {
407        self.0.metadata()
408    }
409
410    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
411        let mut map = self.0.data()?;
412        for (profile, dict) in &mut map {
413            if Config::STANDALONE_SECTIONS.contains(&profile.as_ref()) {
414                // don't force snake case for keys in standalone sections
415                continue;
416            }
417
418            if profile.as_str().as_str() == Config::PROFILE_SECTION {
419                // For the `[profile]` section, we need to preserve profile names (the keys)
420                // but snake_case the top-level config keys within each profile's dict.
421                let dict2 = std::mem::take(dict);
422                *dict = dict2
423                    .into_iter()
424                    .map(|(profile_name, v)| {
425                        // Keep the profile name exactly as-is (e.g., "ci-venom" stays "ci-venom")
426                        let v = snake_case_profile_keys(v);
427                        (profile_name, v)
428                    })
429                    .collect();
430                continue;
431            }
432
433            let dict2 = std::mem::take(dict);
434            *dict = dict2.into_iter().map(|(k, v)| (k.to_snake_case(), v)).collect();
435        }
436        Ok(map)
437    }
438
439    fn profile(&self) -> Option<Profile> {
440        self.0.profile()
441    }
442}
443
444/// Converts the top-level config keys in a profile value to snake_case.
445fn snake_case_profile_keys(value: Value) -> Value {
446    match value {
447        Value::Dict(tag, dict) => {
448            let new_dict = dict.into_iter().map(|(k, v)| (k.to_snake_case(), v)).collect();
449            Value::Dict(tag, new_dict)
450        }
451        other => other,
452    }
453}
454
455/// A Provider that handles breaking changes in toml files
456pub(crate) struct BackwardsCompatTomlProvider<P>(pub(crate) P);
457
458impl<P: Provider> Provider for BackwardsCompatTomlProvider<P> {
459    fn metadata(&self) -> Metadata {
460        self.0.metadata()
461    }
462
463    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
464        let mut map = self.0.data()?;
465        let solc_env = std::env::var("FOUNDRY_SOLC_VERSION")
466            .or_else(|_| std::env::var("DAPP_SOLC_VERSION"))
467            .map(Value::from)
468            .ok();
469        for (profile, dict) in &mut map {
470            if profile.as_str().as_str() == Config::PROFILE_SECTION {
471                for value in dict.values_mut() {
472                    if let Value::Dict(_, dict) = value {
473                        rewrite_legacy_profile_keys(dict, solc_env.as_ref());
474                    }
475                }
476            } else if !Config::STANDALONE_SECTIONS.contains(&profile.as_ref()) {
477                rewrite_legacy_profile_keys(dict, solc_env.as_ref());
478            }
479        }
480        normalize_legacy_labels(&mut map);
481        Ok(map)
482    }
483
484    fn profile(&self) -> Option<Profile> {
485        self.0.profile()
486    }
487}
488
489/// Applies compiler overrides and legacy key rewrites to a profile dictionary.
490fn rewrite_legacy_profile_keys(dict: &mut Dict, solc_env: Option<&Value>) {
491    if let Some(v) = solc_env {
492        // ENV var takes precedence over config file.
493        dict.insert("solc".to_string(), v.clone());
494    } else if let Some(v) = dict.remove("solc_version") {
495        // Only insert the older variant if not already included.
496        dict.entry("solc".to_string()).or_insert(v);
497    }
498    if let Some(v) = dict.remove("deny_warnings") {
499        dict.entry("deny".to_string()).or_insert(v);
500    }
501}
502
503/// Drops the top-level `root` key from config file profiles.
504///
505/// The project root is selected by the caller, not by config files.
506pub(crate) struct IgnoreRootProvider<P>(pub(crate) P);
507
508impl<P: Provider> Provider for IgnoreRootProvider<P> {
509    fn metadata(&self) -> Metadata {
510        self.0.metadata()
511    }
512
513    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
514        let mut map = self.0.data()?;
515        for (profile, dict) in &mut map {
516            // `[profile]` keys are profile names, so `[profile.root]` must be kept.
517            if profile.as_str().as_str() != Config::PROFILE_SECTION {
518                dict.remove("root");
519            }
520        }
521        Ok(map)
522    }
523
524    fn profile(&self) -> Option<Profile> {
525        self.0.profile()
526    }
527}
528
529/// Adapts deprecated labels from arbitrary external providers.
530pub(crate) struct LegacyLabelsProvider<P>(pub(crate) P);
531
532impl<P: Provider> Provider for LegacyLabelsProvider<P> {
533    fn metadata(&self) -> Metadata {
534        self.0.metadata()
535    }
536
537    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
538        let mut map = self.0.data()?;
539        normalize_legacy_labels(&mut map);
540        Ok(map)
541    }
542
543    fn profile(&self) -> Option<Profile> {
544        self.0.profile()
545    }
546}
547
548pub(crate) fn normalize_legacy_labels(map: &mut Map<Profile, Dict>) {
549    if let Some(labels) = map.get(&Profile::new("labels")).cloned() {
550        merge_tracing_labels(&labels, map.entry(Profile::new("tracing")).or_default());
551    }
552
553    for (profile, dict) in map {
554        if profile.as_str().as_str() == Config::PROFILE_SECTION {
555            for value in dict.values_mut() {
556                if let Value::Dict(_, profile) = value {
557                    normalize_legacy_labels_in_profile(profile);
558                }
559            }
560        } else if !Config::STANDALONE_SECTIONS.contains(&profile.as_ref()) {
561            normalize_legacy_labels_in_profile(dict);
562        }
563    }
564}
565
566pub(crate) fn normalize_legacy_labels_in_profile(dict: &mut Dict) {
567    let Some(Value::Dict(_, labels)) = dict.get("labels").cloned() else { return };
568    let tracing = dict.entry("tracing".to_string()).or_insert_with(|| Dict::new().into());
569    if let Value::Dict(_, tracing) = tracing {
570        merge_tracing_labels(&labels, tracing);
571    }
572}
573
574fn merge_tracing_labels(legacy: &Dict, tracing: &mut Dict) {
575    match tracing.get("labels") {
576        Some(Value::Dict(_, configured)) => {
577            let mut labels = legacy.clone();
578            labels.extend(configured.clone());
579            tracing.insert("labels".to_string(), labels.into());
580        }
581        Some(_) => {}
582        None => {
583            tracing.insert("labels".to_string(), legacy.clone().into());
584        }
585    }
586}
587
588/// A provider that sets the `src` and `output` path depending on their existence.
589pub(crate) struct DappHardhatDirProvider<'a> {
590    pub(crate) root: &'a Path,
591    pub(crate) detect_src: bool,
592}
593
594impl Provider for DappHardhatDirProvider<'_> {
595    fn metadata(&self) -> Metadata {
596        Metadata::named("Dapp Hardhat dir compat")
597    }
598
599    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
600        let mut dict = Dict::new();
601        if self.detect_src {
602            dict.insert(
603                "src".to_string(),
604                ProjectPathsConfig::find_source_dir(self.root)
605                    .file_name()
606                    .unwrap()
607                    .to_string_lossy()
608                    .to_string()
609                    .into(),
610            );
611        }
612        dict.insert(
613            "out".to_string(),
614            ProjectPathsConfig::find_artifacts_dir(self.root)
615                .file_name()
616                .unwrap()
617                .to_string_lossy()
618                .to_string()
619                .into(),
620        );
621
622        // detect libs folders:
623        //   if `lib` _and_ `node_modules` exists: include both
624        //   if only `node_modules` exists: include `node_modules`
625        //   include `lib` otherwise
626        let mut libs = vec![];
627        let node_modules = self.root.join("node_modules");
628        let lib = self.root.join("lib");
629        if node_modules.exists() {
630            if lib.exists() {
631                libs.push(lib.file_name().unwrap().to_string_lossy().to_string());
632            }
633            libs.push(node_modules.file_name().unwrap().to_string_lossy().to_string());
634        } else {
635            libs.push(lib.file_name().unwrap().to_string_lossy().to_string());
636        }
637
638        dict.insert("libs".to_string(), libs.into());
639
640        Ok(Map::from([(Config::selected_profile(), dict)]))
641    }
642}
643
644/// A provider that checks for DAPP_ env vars that are named differently than FOUNDRY_
645pub(crate) struct DappEnvCompatProvider;
646
647impl Provider for DappEnvCompatProvider {
648    fn metadata(&self) -> Metadata {
649        Metadata::named("Dapp env compat")
650    }
651
652    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
653        use serde::de::Error as _;
654        use std::env;
655
656        let mut dict = Dict::new();
657        if let Ok(val) = env::var("DAPP_TEST_NUMBER") {
658            dict.insert(
659                "block_number".to_string(),
660                val.parse::<u64>().map_err(figment::Error::custom)?.into(),
661            );
662        }
663        if let Ok(val) = env::var("DAPP_TEST_ADDRESS") {
664            dict.insert("sender".to_string(), val.into());
665        }
666        if let Ok(val) = env::var("DAPP_FORK_BLOCK") {
667            dict.insert(
668                "fork_block_number".to_string(),
669                val.parse::<u64>().map_err(figment::Error::custom)?.into(),
670            );
671        } else if let Ok(val) = env::var("DAPP_TEST_NUMBER") {
672            dict.insert(
673                "fork_block_number".to_string(),
674                val.parse::<u64>().map_err(figment::Error::custom)?.into(),
675            );
676        }
677        if let Ok(val) = env::var("DAPP_TEST_TIMESTAMP") {
678            dict.insert(
679                "block_timestamp".to_string(),
680                val.parse::<u64>().map_err(figment::Error::custom)?.into(),
681            );
682        }
683        if let Ok(val) = env::var("DAPP_BUILD_OPTIMIZE_RUNS") {
684            dict.insert(
685                "optimizer_runs".to_string(),
686                val.parse::<u64>().map_err(figment::Error::custom)?.into(),
687            );
688        }
689        if let Ok(val) = env::var("DAPP_BUILD_OPTIMIZE") {
690            // Activate Solidity optimizer (0 or 1)
691            let val = val.parse::<u8>().map_err(figment::Error::custom)?;
692            if val > 1 {
693                return Err(
694                    format!("Invalid $DAPP_BUILD_OPTIMIZE value `{val}`, expected 0 or 1").into()
695                );
696            }
697            dict.insert("optimizer".to_string(), (val == 1).into());
698        }
699
700        // libraries in env vars either as `[..]` or single string separated by comma
701        if let Ok(val) = env::var("DAPP_LIBRARIES").or_else(|_| env::var("FOUNDRY_LIBRARIES")) {
702            dict.insert("libraries".to_string(), utils::to_array_value(&val)?);
703        }
704
705        let mut fuzz_dict = Dict::new();
706        if let Ok(val) = env::var("DAPP_TEST_FUZZ_RUNS") {
707            fuzz_dict.insert(
708                "runs".to_string(),
709                val.parse::<u32>().map_err(figment::Error::custom)?.into(),
710            );
711        }
712        dict.insert("fuzz".to_string(), fuzz_dict.into());
713
714        let mut invariant_dict = Dict::new();
715        if let Ok(val) = env::var("DAPP_TEST_DEPTH") {
716            invariant_dict.insert(
717                "depth".to_string(),
718                val.parse::<u32>().map_err(figment::Error::custom)?.into(),
719            );
720        }
721        dict.insert("invariant".to_string(), invariant_dict.into());
722
723        Ok(Map::from([(Config::selected_profile(), dict)]))
724    }
725}
726
727/// Renames a profile from `from` to `to`.
728///
729/// For example given:
730///
731/// ```toml
732/// [from]
733/// key = "value"
734/// ```
735///
736/// RenameProfileProvider will output
737///
738/// ```toml
739/// [to]
740/// key = "value"
741/// ```
742pub(crate) struct RenameProfileProvider<P> {
743    provider: P,
744    from: Profile,
745    to: Profile,
746}
747
748impl<P> RenameProfileProvider<P> {
749    pub(crate) fn new(provider: P, from: impl Into<Profile>, to: impl Into<Profile>) -> Self {
750        Self { provider, from: from.into(), to: to.into() }
751    }
752}
753
754impl<P: Provider> Provider for RenameProfileProvider<P> {
755    fn metadata(&self) -> Metadata {
756        self.provider.metadata()
757    }
758
759    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
760        let mut data = self.provider.data()?;
761        if let Some(data) = data.remove(&self.from) {
762            return Ok(Map::from([(self.to.clone(), data)]));
763        }
764        Ok(Default::default())
765    }
766
767    fn profile(&self) -> Option<Profile> {
768        Some(self.to.clone())
769    }
770}
771
772/// Unwraps a profile reducing the key depth
773///
774/// For example given:
775///
776/// ```toml
777/// [wrapping_key.profile]
778/// key = "value"
779/// ```
780///
781/// UnwrapProfileProvider will output:
782///
783/// ```toml
784/// [profile]
785/// key = "value"
786/// ```
787struct UnwrapProfileProvider<P> {
788    provider: P,
789    wrapping_key: Profile,
790    profile: Profile,
791}
792
793impl<P> UnwrapProfileProvider<P> {
794    pub fn new(provider: P, wrapping_key: impl Into<Profile>, profile: impl Into<Profile>) -> Self {
795        Self { provider, wrapping_key: wrapping_key.into(), profile: profile.into() }
796    }
797}
798
799impl<P: Provider> Provider for UnwrapProfileProvider<P> {
800    fn metadata(&self) -> Metadata {
801        self.provider.metadata()
802    }
803
804    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
805        let mut data = self.provider.data()?;
806        if let Some(profiles) = data.remove(&self.wrapping_key) {
807            for (profile_str, profile_val) in profiles {
808                let profile = Profile::new(&profile_str);
809                if profile != self.profile {
810                    continue;
811                }
812                match profile_val {
813                    Value::Dict(_, dict) => return Ok(profile.collect(dict)),
814                    bad_val => {
815                        let mut err = Error::from(figment::error::Kind::InvalidType(
816                            bad_val.to_actual(),
817                            "dict".into(),
818                        ));
819                        err.metadata = Some(self.provider.metadata());
820                        err.profile = Some(self.profile.clone());
821                        return Err(err);
822                    }
823                }
824            }
825        }
826        Ok(Default::default())
827    }
828
829    fn profile(&self) -> Option<Profile> {
830        Some(self.profile.clone())
831    }
832}
833
834/// Wraps a profile in another profile
835///
836/// For example given:
837///
838/// ```toml
839/// [profile]
840/// key = "value"
841/// ```
842///
843/// WrapProfileProvider will output:
844///
845/// ```toml
846/// [wrapping_key.profile]
847/// key = "value"
848/// ```
849pub(crate) struct WrapProfileProvider<P> {
850    provider: P,
851    wrapping_key: Profile,
852    profile: Profile,
853}
854
855impl<P> WrapProfileProvider<P> {
856    pub fn new(provider: P, wrapping_key: impl Into<Profile>, profile: impl Into<Profile>) -> Self {
857        Self { provider, wrapping_key: wrapping_key.into(), profile: profile.into() }
858    }
859}
860
861impl<P: Provider> Provider for WrapProfileProvider<P> {
862    fn metadata(&self) -> Metadata {
863        self.provider.metadata()
864    }
865
866    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
867        if let Some(inner) = self.provider.data()?.remove(&self.profile) {
868            let value = Value::from(inner);
869            let mut dict = Dict::new();
870            dict.insert(self.profile.as_str().as_str().to_snake_case(), value);
871            Ok(self.wrapping_key.collect(dict))
872        } else {
873            Ok(Default::default())
874        }
875    }
876
877    fn profile(&self) -> Option<Profile> {
878        Some(self.profile.clone())
879    }
880}
881
882/// Extracts the profile from the `profile` key and using the original key as backup, merging
883/// values where necessary
884///
885/// For example given:
886///
887/// ```toml
888/// [profile.cool]
889/// key = "value"
890///
891/// [cool]
892/// key2 = "value2"
893/// ```
894///
895/// OptionalStrictProfileProvider will output:
896///
897/// ```toml
898/// [cool]
899/// key = "value"
900/// key2 = "value2"
901/// ```
902///
903/// And emit a deprecation warning
904pub(crate) struct OptionalStrictProfileProvider<P> {
905    provider: P,
906    profiles: Vec<Profile>,
907}
908
909impl<P> OptionalStrictProfileProvider<P> {
910    pub const PROFILE_PROFILE: Profile = Profile::const_new("profile");
911
912    pub fn new(provider: P, profiles: impl IntoIterator<Item = impl Into<Profile>>) -> Self {
913        Self { provider, profiles: profiles.into_iter().map(|profile| profile.into()).collect() }
914    }
915}
916
917impl<P: Provider> Provider for OptionalStrictProfileProvider<P> {
918    fn metadata(&self) -> Metadata {
919        self.provider.metadata()
920    }
921
922    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
923        let mut figment = Figment::from(&self.provider);
924        for profile in &self.profiles {
925            figment = figment.merge(UnwrapProfileProvider::new(
926                &self.provider,
927                Self::PROFILE_PROFILE,
928                profile.clone(),
929            ));
930        }
931        figment.data().map_err(|err| {
932            // figment does tag metadata and tries to map metadata to an error, since we use a new
933            // figment in this provider this new figment does not know about the metadata of the
934            // provider and can't map the metadata to the error. Therefore we return the root error
935            // if this error originated in the provider's data.
936            if let Err(root_err) = self.provider.data() {
937                return root_err;
938            }
939            err
940        })
941    }
942
943    fn profile(&self) -> Option<Profile> {
944        self.profiles.last().cloned()
945    }
946}
947
948/// Extracts the profile from the `profile` key and sets unset values according to the fallback
949/// provider
950pub struct FallbackProfileProvider<P> {
951    provider: P,
952    profile: Profile,
953    fallback: Profile,
954}
955
956impl<P> FallbackProfileProvider<P> {
957    /// Creates a new fallback profile provider.
958    pub fn new(provider: P, profile: impl Into<Profile>, fallback: impl Into<Profile>) -> Self {
959        Self { provider, profile: profile.into(), fallback: fallback.into() }
960    }
961}
962
963impl<P: Provider> Provider for FallbackProfileProvider<P> {
964    fn metadata(&self) -> Metadata {
965        self.provider.metadata()
966    }
967
968    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
969        let mut data = self.provider.data()?;
970        let invariant_corpus_random_sequence_weight_configured = self.profile == "invariant"
971            && data
972                .get(&self.profile)
973                .is_some_and(|inner| inner.contains_key("corpus_random_sequence_weight"));
974        let mark_invariant_corpus_random_sequence_weight_configured =
975            |inner: &mut Dict| -> Result<(), Error> {
976                if invariant_corpus_random_sequence_weight_configured {
977                    inner.insert(
978                        "corpus_random_sequence_weight_configured".to_string(),
979                        Value::serialize(true)?,
980                    );
981                }
982                Ok(())
983            };
984
985        if let Some(fallback) = data.remove(&self.fallback) {
986            let mut inner = data.remove(&self.profile).unwrap_or_default();
987            for (k, v) in fallback {
988                inner.entry(k).or_insert(v);
989            }
990            mark_invariant_corpus_random_sequence_weight_configured(&mut inner)?;
991            Ok(self.profile.collect(inner))
992        } else {
993            if let Some(inner) = data.get_mut(&self.profile) {
994                mark_invariant_corpus_random_sequence_weight_configured(inner)?;
995            }
996            Ok(data)
997        }
998    }
999
1000    fn profile(&self) -> Option<Profile> {
1001        Some(self.profile.clone())
1002    }
1003}