foundry_config/inline/
mod.rs1use std::collections::BTreeSet;
2
3use crate::Config;
4use alloy_primitives::map::HashMap;
5use figment::{
6 Figment, Profile, Provider,
7 value::{Dict, Map, Value},
8};
9use foundry_compilers::ProjectCompileOutput;
10use foundry_evm_networks::NetworkVariant;
11use itertools::Itertools;
12
13mod natspec;
14pub use natspec::*;
15
16const INLINE_CONFIG_PREFIX: &str = "forge-config:";
17
18type DataMap = Map<Profile, Dict>;
19
20#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
22pub enum InlineConfigErrorKind {
23 #[error(transparent)]
25 Parse(#[from] toml::de::Error),
26 #[error("invalid profile `{0}`; valid profiles: {1}")]
28 InvalidProfile(String, String),
29}
30
31#[derive(Debug, thiserror::Error)]
34#[error("Inline config error at {location}: {kind}")]
35pub struct InlineConfigError {
36 pub location: String,
39 pub kind: InlineConfigErrorKind,
41}
42
43#[derive(Clone, Debug, Default)]
47pub struct InlineConfig {
48 contract_level: HashMap<String, DataMap>,
50 fn_level: HashMap<(String, String), DataMap>,
52}
53
54impl InlineConfig {
55 pub fn new() -> Self {
57 Self::default()
58 }
59
60 pub fn new_parsed(output: &ProjectCompileOutput, config: &Config) -> eyre::Result<Self> {
63 let natspecs: Vec<NatSpec> = NatSpec::parse(output, &config.root);
64 let profiles = &config.profiles;
65 let mut inline = Self::new();
66 for natspec in &natspecs {
67 inline.insert(natspec)?;
68 natspec.validate_profiles(profiles)?;
70 }
71 Ok(inline)
72 }
73
74 pub fn insert(&mut self, natspec: &NatSpec) -> Result<(), InlineConfigError> {
76 let map = if let Some(function) = &natspec.function {
77 self.fn_level.entry((natspec.contract.clone(), function.clone())).or_default()
78 } else {
79 self.contract_level.entry(natspec.contract.clone()).or_default()
80 };
81 let joined = natspec
82 .config_values()
83 .map(|s| {
84 if let Some(idx) = s.find('=') {
86 s[..idx].replace('-', "_") + &s[idx..]
87 } else {
88 s.to_string()
89 }
90 })
91 .format("\n")
92 .to_string();
93 let data = toml::from_str::<DataMap>(&joined).map_err(|e| InlineConfigError {
94 location: natspec.location_string(),
95 kind: InlineConfigErrorKind::Parse(e),
96 })?;
97 extend_data_map(map, &data);
98 Ok(())
99 }
100
101 pub const fn provide<'a>(
104 &'a self,
105 contract: &'a str,
106 function: &'a str,
107 ) -> InlineConfigProvider<'a> {
108 InlineConfigProvider { inline: self, contract, function }
109 }
110
111 pub fn merge(&self, contract: &str, function: &str, base: &Config) -> Figment {
114 Figment::from(base).merge(self.provide(contract, function))
115 }
116
117 pub fn contains_contract(&self, contract: &str) -> bool {
119 self.get_contract(contract).is_some_and(|map| !map.is_empty())
120 }
121
122 pub fn contains_function(&self, contract: &str, function: &str) -> bool {
126 self.get_function(contract, function).is_some_and(|map| !map.is_empty())
127 }
128
129 pub fn network_for(
132 &self,
133 profile: &Profile,
134 contract: &str,
135 function: &str,
136 ) -> Option<NetworkVariant> {
137 let data = self.provide(contract, function).data().ok()?;
138 let dict = data.get(profile).or_else(|| data.get(&Profile::Default))?;
139 if let Some(Value::Dict(_, networks)) = dict.get("networks")
140 && let Some(Value::String(_, s)) = networks.get("network")
141 {
142 return s.parse().ok();
143 }
144 None
145 }
146
147 pub fn referenced_override_networks(&self, profile: &Profile) -> Vec<NetworkVariant> {
151 let mut seen = BTreeSet::new();
152 for (contract, function) in self.fn_level.keys() {
153 if let Some(v) = self.network_for(profile, contract, function) {
154 seen.insert(v);
155 }
156 }
157 for contract in self.contract_level.keys() {
158 if let Some(v) = self.network_for(profile, contract, "") {
159 seen.insert(v);
160 }
161 }
162 seen.into_iter().collect()
163 }
164
165 fn get_contract(&self, contract: &str) -> Option<&DataMap> {
166 self.contract_level.get(contract)
167 }
168
169 fn get_function(&self, contract: &str, function: &str) -> Option<&DataMap> {
170 let key = (contract.to_string(), function.to_string());
171 self.fn_level.get(&key)
172 }
173}
174
175#[derive(Clone, Debug)]
179pub struct InlineConfigProvider<'a> {
180 inline: &'a InlineConfig,
181 contract: &'a str,
182 function: &'a str,
183}
184
185impl Provider for InlineConfigProvider<'_> {
186 fn metadata(&self) -> figment::Metadata {
187 figment::Metadata::named("inline config")
188 }
189
190 fn data(&self) -> figment::Result<DataMap> {
191 let mut map = DataMap::new();
192 if let Some(new) = self.inline.get_contract(self.contract) {
193 extend_data_map(&mut map, new);
194 }
195 if let Some(new) = self.inline.get_function(self.contract, self.function) {
196 extend_data_map(&mut map, new);
197 }
198 Ok(map)
199 }
200}
201
202fn extend_data_map(map: &mut DataMap, new: &DataMap) {
203 for (profile, data) in new {
204 extend_dict(map.entry(profile.clone()).or_default(), data);
205 }
206}
207
208fn extend_dict(dict: &mut Dict, new: &Dict) {
209 for (k, v) in new {
210 match dict.entry(k.clone()) {
211 std::collections::btree_map::Entry::Vacant(entry) => {
212 entry.insert(v.clone());
213 }
214 std::collections::btree_map::Entry::Occupied(entry) => {
215 extend_value(entry.into_mut(), v);
216 }
217 }
218 }
219}
220
221fn extend_value(value: &mut Value, new: &Value) {
222 match (value, new) {
223 (Value::Dict(tag, dict), Value::Dict(new_tag, new_dict)) => {
224 *tag = *new_tag;
225 extend_dict(dict, new_dict);
226 }
227 (value, new) => *value = new.clone(),
228 }
229}