loam-cli 0.14.4

Loam CLI for building smart contracts
Documentation
use indexmap::IndexMap;
use serde::Deserialize;
use std::collections::BTreeMap as Map;
use std::path::Path;
use toml::value::Table;

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 LOAM_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 Environment {
    pub accounts: Option<Vec<Account>>,
    pub network: Network,
    pub contracts: Option<IndexMap<Box<str>, Contract>>,
}

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>,
        }

        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()?;

        Ok(Environment {
            accounts: helper.accounts,
            network: helper.network,
            contracts,
        })
    }
}

#[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, Default)]
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 init: Option<String>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,
}

fn default_client() -> bool {
    true
}

impl Environment {
    pub fn get(workspace_root: &Path, loam_env: &str) -> 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(loam_env);
        if current_env.is_none() {
            return Err(Error::NoSettingsForCurrentEnv(loam_env.to_string()));
        };
        Ok(current_env)
    }
}