stellar_scaffold_cli/commands/build/
env_toml.rs1use indexmap::IndexMap;
2use serde::Deserialize;
3use std::collections::BTreeMap as Map;
4use std::path::Path;
5use toml::value::Table;
6
7use crate::commands::build::clients::ScaffoldEnv;
8
9pub const ENV_FILE: &str = "environments.toml";
10
11#[derive(thiserror::Error, Debug)]
12pub enum Error {
13 #[error("⛔ ️parsing environments.toml: {0}")]
14 ParsingToml(#[from] toml::de::Error),
15 #[error("⛔ ️no settings for current STELLAR_SCAFFOLD_ENV ({0:?}) found in environments.toml")]
16 NoSettingsForCurrentEnv(String),
17 #[error("⛔ ️reading environments.toml as a string: {0}")]
18 ParsingString(#[from] std::io::Error),
19}
20
21type Environments = Map<Box<str>, Environment>;
22
23#[derive(Debug, Clone)]
44pub struct ExtensionEntry {
45 pub name: String,
48 pub config: Option<serde_json::Value>,
52}
53
54#[derive(Debug, Clone)]
55pub struct Environment {
56 pub accounts: Option<Vec<Account>>,
57 pub network: Network,
58 pub contracts: Option<IndexMap<Box<str>, Contract>>,
59 pub extensions: Vec<ExtensionEntry>,
61}
62
63fn deserialize_accounts<'de, D>(deserializer: D) -> Result<Option<Vec<Account>>, D::Error>
64where
65 D: serde::Deserializer<'de>,
66{
67 let opt: Option<Vec<AccountRepresentation>> = Option::deserialize(deserializer)?;
68 Ok(opt.map(|vec| vec.into_iter().map(Account::from).collect()))
69}
70
71impl<'de> Deserialize<'de> for Environment {
72 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
73 where
74 D: serde::Deserializer<'de>,
75 {
76 #[derive(Deserialize)]
77 struct EnvironmentHelper {
78 #[serde(default, deserialize_with = "deserialize_accounts")]
79 accounts: Option<Vec<Account>>,
80 network: Network,
81 contracts: Option<Table>,
82 #[serde(default)]
84 extensions: Vec<String>,
85 ext: Option<Table>,
87 }
88
89 let helper = EnvironmentHelper::deserialize(deserializer)?;
90
91 let contracts = helper
92 .contracts
93 .map(|contracts_table| {
94 contracts_table
95 .into_iter()
96 .map(|(key, value)| {
97 let contract: Contract =
98 Contract::deserialize(value).map_err(serde::de::Error::custom)?;
99 Ok((key.into_boxed_str(), contract))
100 })
101 .collect::<Result<IndexMap<_, _>, D::Error>>()
102 })
103 .transpose()?;
104
105 let extensions = parse_extensions(helper.extensions, helper.ext);
106
107 Ok(Environment {
108 accounts: helper.accounts,
109 network: helper.network,
110 contracts,
111 extensions,
112 })
113 }
114}
115
116fn parse_extensions(names: Vec<String>, ext: Option<Table>) -> Vec<ExtensionEntry> {
123 let mut configs = ext.unwrap_or_default();
124 names
125 .into_iter()
126 .map(|name| {
127 let config = configs.remove(&name).and_then(|val| {
128 match &val {
130 toml::Value::Table(t) if t.is_empty() => None,
131 _ => serde_json::to_value(&val).ok(),
132 }
133 });
134 ExtensionEntry { name, config }
135 })
136 .collect()
137}
138
139#[derive(Debug, serde::Deserialize, Clone)]
140#[serde(rename_all = "kebab-case")]
141pub struct Network {
142 pub name: Option<String>,
143 pub rpc_url: Option<String>,
144 pub network_passphrase: Option<String>,
145 pub rpc_headers: Option<Vec<(String, String)>>,
146 #[serde(skip_serializing_if = "is_false", default)]
147 pub run_locally: bool,
148}
149
150#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
151#[serde(untagged)]
152pub enum AccountRepresentation {
153 Simple(String),
154 Detailed(Account),
155}
156
157#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
158pub struct Account {
159 pub name: String,
160 #[serde(default)]
161 pub default: bool,
162}
163
164impl From<AccountRepresentation> for Account {
165 fn from(rep: AccountRepresentation) -> Self {
166 match rep {
167 AccountRepresentation::Simple(name) => Account {
168 name,
169 default: false,
170 },
171 AccountRepresentation::Detailed(account) => account,
172 }
173 }
174}
175
176#[derive(Debug, Deserialize, Clone)]
177pub struct Contract {
178 #[serde(default = "default_client", skip_serializing_if = "std::ops::Not::not")]
179 pub client: bool,
180
181 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub after_deploy: Option<String>,
183
184 #[serde(default, skip_serializing_if = "Option::is_none")]
185 pub id: Option<String>,
186
187 #[serde(default, skip_serializing_if = "Option::is_none")]
188 pub constructor_args: Option<String>,
189}
190
191impl Default for Contract {
192 fn default() -> Self {
193 Self {
194 client: default_client(),
195 after_deploy: None,
196 id: None,
197 constructor_args: None,
198 }
199 }
200}
201
202fn default_client() -> bool {
203 true
204}
205
206impl Environment {
207 pub fn get(
208 workspace_root: &Path,
209 scaffold_env: &ScaffoldEnv,
210 ) -> Result<Option<Environment>, Error> {
211 let env_toml = workspace_root.join(ENV_FILE);
212
213 if !env_toml.exists() {
214 return Ok(None);
215 }
216
217 let toml_str = std::fs::read_to_string(env_toml)?;
218 let mut parsed_toml: Environments = toml::from_str(&toml_str)?;
219 let current_env = parsed_toml.remove(scaffold_env.to_string().as_str());
220 if current_env.is_none() {
221 return Err(Error::NoSettingsForCurrentEnv(scaffold_env.to_string()));
222 }
223 Ok(current_env)
224 }
225}
226
227impl From<&Network> for stellar_cli::config::network::Args {
228 fn from(network: &Network) -> Self {
229 stellar_cli::config::network::Args {
230 network: network.name.clone(),
231 rpc_url: network.rpc_url.clone(),
232 network_passphrase: network.network_passphrase.clone(),
233 rpc_headers: network.rpc_headers.clone().unwrap_or_default(),
234 }
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use serde_json::json;
242
243 fn parse_dev(toml: &str) -> Environment {
246 let mut envs: Environments = toml::from_str(toml).expect("invalid TOML");
247 envs.remove("development").expect("missing development key")
248 }
249
250 const NETWORK_STUB: &str = r#"[development.network]
252name = "testnet"
253"#;
254
255 #[test]
256 fn extensions_with_config() {
257 let toml = format!(
258 r#"{NETWORK_STUB}
259[development]
260extensions = ["reporter", "indexer"]
261
262[development.ext.reporter]
263warn_size_kb = 128
264
265[development.ext.indexer]
266storage = "sqlite"
267events = ["transfer", "mint"]
268"#
269 );
270
271 let env = parse_dev(&toml);
272 assert_eq!(env.extensions.len(), 2);
273
274 let reporter = &env.extensions[0];
275 assert_eq!(reporter.name, "reporter");
276 assert_eq!(reporter.config, Some(json!({ "warn_size_kb": 128 })));
277
278 let indexer = &env.extensions[1];
279 assert_eq!(indexer.name, "indexer");
280 assert_eq!(
281 indexer.config,
282 Some(json!({ "storage": "sqlite", "events": ["transfer", "mint"] }))
283 );
284 }
285
286 #[test]
287 fn extensions_without_config() {
288 let toml = format!(
289 r#"{NETWORK_STUB}
290[development]
291extensions = ["reporter", "indexer"]
292"#
293 );
294
295 let env = parse_dev(&toml);
296 assert_eq!(env.extensions.len(), 2);
297
298 assert_eq!(env.extensions[0].name, "reporter");
299 assert!(env.extensions[0].config.is_none());
300
301 assert_eq!(env.extensions[1].name, "indexer");
302 assert!(env.extensions[1].config.is_none());
303 }
304
305 #[test]
306 fn extensions_empty_array() {
307 let toml = format!(
308 r"{NETWORK_STUB}
309[development]
310extensions = []
311"
312 );
313
314 let env = parse_dev(&toml);
315 assert!(env.extensions.is_empty());
316 }
317
318 #[test]
319 fn extensions_absent() {
320 let env = parse_dev(NETWORK_STUB);
321 assert!(env.extensions.is_empty());
322 }
323
324 #[test]
325 fn extensions_empty_ext_table() {
326 let toml = format!(
328 r#"{NETWORK_STUB}
329[development]
330extensions = ["linter"]
331
332[development.ext.linter]
333"#
334 );
335
336 let env = parse_dev(&toml);
337 assert_eq!(env.extensions.len(), 1);
338 assert_eq!(env.extensions[0].name, "linter");
339 assert!(env.extensions[0].config.is_none());
340 }
341
342 #[test]
343 fn extensions_ext_table_without_listing_is_ignored() {
344 let toml = format!(
346 r#"{NETWORK_STUB}
347[development]
348extensions = ["reporter"]
349
350[development.ext.reporter]
351warn_size_kb = 128
352
353[development.ext.unlisted]
354some_key = "value"
355"#
356 );
357
358 let env = parse_dev(&toml);
359 assert_eq!(env.extensions.len(), 1);
360 assert_eq!(env.extensions[0].name, "reporter");
361 }
362}