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