use indexmap::IndexMap;
use serde::Deserialize;
use std::collections::BTreeMap as Map;
use std::path::Path;
use toml::value::Table;
use crate::commands::build::clients::ScaffoldEnv;
pub const ENV_FILE: &str = "environments.toml";
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("⛔ ️parsing environments.toml: {0}")]
ParsingToml(#[from] toml::de::Error),
#[error("⛔ ️no settings for current STELLAR_SCAFFOLD_ENV ({0:?}) found in environments.toml")]
NoSettingsForCurrentEnv(String),
#[error("⛔ ️reading environments.toml as a string: {0}")]
ParsingString(#[from] std::io::Error),
}
type Environments = Map<Box<str>, Environment>;
#[derive(Debug, Clone)]
pub struct ExtensionEntry {
pub name: String,
pub config: Option<serde_json::Value>,
}
#[derive(Debug, Clone)]
pub struct Environment {
pub accounts: Option<Vec<Account>>,
pub network: Network,
pub contracts: Option<IndexMap<Box<str>, Contract>>,
pub extensions: Vec<ExtensionEntry>,
}
fn deserialize_accounts<'de, D>(deserializer: D) -> Result<Option<Vec<Account>>, D::Error>
where
D: serde::Deserializer<'de>,
{
let opt: Option<Vec<AccountRepresentation>> = Option::deserialize(deserializer)?;
Ok(opt.map(|vec| vec.into_iter().map(Account::from).collect()))
}
impl<'de> Deserialize<'de> for Environment {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct EnvironmentHelper {
#[serde(default, deserialize_with = "deserialize_accounts")]
accounts: Option<Vec<Account>>,
network: Network,
contracts: Option<Table>,
#[serde(default)]
extensions: Vec<String>,
ext: Option<Table>,
}
let helper = EnvironmentHelper::deserialize(deserializer)?;
let contracts = helper
.contracts
.map(|contracts_table| {
contracts_table
.into_iter()
.map(|(key, value)| {
let contract: Contract =
Contract::deserialize(value).map_err(serde::de::Error::custom)?;
Ok((key.into_boxed_str(), contract))
})
.collect::<Result<IndexMap<_, _>, D::Error>>()
})
.transpose()?;
let extensions = parse_extensions(helper.extensions, helper.ext);
Ok(Environment {
accounts: helper.accounts,
network: helper.network,
contracts,
extensions,
})
}
}
fn parse_extensions(names: Vec<String>, ext: Option<Table>) -> Vec<ExtensionEntry> {
let mut configs = ext.unwrap_or_default();
names
.into_iter()
.map(|name| {
let config = configs.remove(&name).and_then(|val| {
match &val {
toml::Value::Table(t) if t.is_empty() => None,
_ => serde_json::to_value(&val).ok(),
}
});
ExtensionEntry { name, config }
})
.collect()
}
#[derive(Debug, serde::Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Network {
pub name: Option<String>,
pub rpc_url: Option<String>,
pub network_passphrase: Option<String>,
pub rpc_headers: Option<Vec<(String, String)>>,
#[serde(skip_serializing_if = "is_false", default)]
pub run_locally: bool,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum AccountRepresentation {
Simple(String),
Detailed(Account),
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Account {
pub name: String,
#[serde(default)]
pub default: bool,
}
impl From<AccountRepresentation> for Account {
fn from(rep: AccountRepresentation) -> Self {
match rep {
AccountRepresentation::Simple(name) => Account {
name,
default: false,
},
AccountRepresentation::Detailed(account) => account,
}
}
}
#[derive(Debug, Deserialize, Clone)]
pub struct Contract {
#[serde(default = "default_client", skip_serializing_if = "std::ops::Not::not")]
pub client: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub after_deploy: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub constructor_args: Option<String>,
}
impl Default for Contract {
fn default() -> Self {
Self {
client: default_client(),
after_deploy: None,
id: None,
constructor_args: None,
}
}
}
fn default_client() -> bool {
true
}
impl Environment {
pub fn get(
workspace_root: &Path,
scaffold_env: &ScaffoldEnv,
) -> Result<Option<Environment>, Error> {
let env_toml = workspace_root.join(ENV_FILE);
if !env_toml.exists() {
return Ok(None);
}
let toml_str = std::fs::read_to_string(env_toml)?;
let mut parsed_toml: Environments = toml::from_str(&toml_str)?;
let current_env = parsed_toml.remove(scaffold_env.to_string().as_str());
if current_env.is_none() {
return Err(Error::NoSettingsForCurrentEnv(scaffold_env.to_string()));
}
Ok(current_env)
}
}
impl From<&Network> for stellar_cli::config::network::Args {
fn from(network: &Network) -> Self {
stellar_cli::config::network::Args {
network: network.name.clone(),
rpc_url: network.rpc_url.clone(),
network_passphrase: network.network_passphrase.clone(),
rpc_headers: network.rpc_headers.clone().unwrap_or_default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn parse_dev(toml: &str) -> Environment {
let mut envs: Environments = toml::from_str(toml).expect("invalid TOML");
envs.remove("development").expect("missing development key")
}
const NETWORK_STUB: &str = r#"[development.network]
name = "testnet"
"#;
#[test]
fn extensions_with_config() {
let toml = format!(
r#"{NETWORK_STUB}
[development]
extensions = ["reporter", "indexer"]
[development.ext.reporter]
warn_size_kb = 128
[development.ext.indexer]
storage = "sqlite"
events = ["transfer", "mint"]
"#
);
let env = parse_dev(&toml);
assert_eq!(env.extensions.len(), 2);
let reporter = &env.extensions[0];
assert_eq!(reporter.name, "reporter");
assert_eq!(reporter.config, Some(json!({ "warn_size_kb": 128 })));
let indexer = &env.extensions[1];
assert_eq!(indexer.name, "indexer");
assert_eq!(
indexer.config,
Some(json!({ "storage": "sqlite", "events": ["transfer", "mint"] }))
);
}
#[test]
fn extensions_without_config() {
let toml = format!(
r#"{NETWORK_STUB}
[development]
extensions = ["reporter", "indexer"]
"#
);
let env = parse_dev(&toml);
assert_eq!(env.extensions.len(), 2);
assert_eq!(env.extensions[0].name, "reporter");
assert!(env.extensions[0].config.is_none());
assert_eq!(env.extensions[1].name, "indexer");
assert!(env.extensions[1].config.is_none());
}
#[test]
fn extensions_empty_array() {
let toml = format!(
r"{NETWORK_STUB}
[development]
extensions = []
"
);
let env = parse_dev(&toml);
assert!(env.extensions.is_empty());
}
#[test]
fn extensions_absent() {
let env = parse_dev(NETWORK_STUB);
assert!(env.extensions.is_empty());
}
#[test]
fn extensions_empty_ext_table() {
let toml = format!(
r#"{NETWORK_STUB}
[development]
extensions = ["linter"]
[development.ext.linter]
"#
);
let env = parse_dev(&toml);
assert_eq!(env.extensions.len(), 1);
assert_eq!(env.extensions[0].name, "linter");
assert!(env.extensions[0].config.is_none());
}
#[test]
fn extensions_ext_table_without_listing_is_ignored() {
let toml = format!(
r#"{NETWORK_STUB}
[development]
extensions = ["reporter"]
[development.ext.reporter]
warn_size_kb = 128
[development.ext.unlisted]
some_key = "value"
"#
);
let env = parse_dev(&toml);
assert_eq!(env.extensions.len(), 1);
assert_eq!(env.extensions[0].name, "reporter");
}
}