Skip to content

Commit

Permalink
feat(cli): build contracts in specified order (#160)
Browse files Browse the repository at this point in the history
  • Loading branch information
BlaineHeffron authored Sep 24, 2024
1 parent b3d3ef1 commit 157c70b
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 12 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion crates/loam-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ sha2 = "0.10.7"
hex = "0.4.3"
shlex = "1.1.0"
symlink = "0.1.0"
toml = { version = "0.8.12", features = ["parse"] }
toml = { version = "0.8.12", features = ["parse", "preserve_order"] }
rand = "0.8.5"
wasm-gen = { version = "0.1.4" }
notify = "6.1.1"
Expand All @@ -65,6 +65,7 @@ stellar-xdr = "21.0.0"
rust-embed = { version = "8.2.0", features = ["debug-embed"] }
regex = "1.10.5"
toml_edit = "0.22.16"
indexmap = { version = "1.9", features = ["serde"] }

[dev-dependencies]
assert_cmd = "2.0.4"
Expand Down
39 changes: 35 additions & 4 deletions crates/loam-cli/src/commands/build/clients.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
#![allow(clippy::struct_excessive_bools)]
use crate::commands::build::env_toml;
use indexmap::IndexMap;
use regex::Regex;
use serde_json;
use shlex::split;
use soroban_cli::commands::NetworkRunnable;
use soroban_cli::utils::contract_hash;
use soroban_cli::{commands as cli, CommandParser};
use std::collections::BTreeMap as Map;
use std::fmt::Debug;
use std::hash::Hash;
use std::process::Command;
Expand Down Expand Up @@ -328,10 +328,39 @@ export default new Client.Client({{
Ok(())
}

fn reorder_package_names(
package_names: &[String],
contracts: Option<&IndexMap<Box<str>, env_toml::Contract>>,
) -> Vec<String> {
contracts.map_or_else(
|| package_names.to_vec(),
|contracts| {
let mut reordered: Vec<String> = contracts
.keys()
.filter_map(|contract_name| {
package_names
.iter()
.find(|&name| name == contract_name.as_ref())
.cloned()
})
.collect();

reordered.extend(
package_names
.iter()
.filter(|name| !contracts.contains_key(name.as_str()))
.cloned(),
);

reordered
},
)
}

async fn handle_production_contracts(
&self,
workspace_root: &std::path::Path,
contracts: &Map<Box<str>, env_toml::Contract>,
contracts: &IndexMap<Box<str>, env_toml::Contract>,
) -> Result<(), Error> {
for (name, contract) in contracts.iter().filter(|(_, settings)| settings.client) {
if let Some(id) = &contract.id {
Expand All @@ -350,7 +379,7 @@ export default new Client.Client({{
async fn handle_contracts(
self,
workspace_root: &std::path::Path,
contracts: Option<&Map<Box<str>, env_toml::Contract>>,
contracts: Option<&IndexMap<Box<str>, env_toml::Contract>>,
package_names: Vec<String>,
network: &Network,
) -> Result<(), Error> {
Expand All @@ -375,7 +404,9 @@ export default new Client.Client({{
}
}
}
for name in package_names {
// Reorder package_names based on contracts order
let reordered_names = Self::reorder_package_names(&package_names, contracts);
for name in reordered_names {
let settings = match contracts {
Some(contracts) => contracts.get(&name as &str),
None => None,
Expand Down
53 changes: 46 additions & 7 deletions crates/loam-cli/src/commands/build/env_toml.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
use indexmap::IndexMap;
use serde::Deserialize;
use std::collections::BTreeMap as Map;
use std::io;
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(io::Error),
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, serde::Deserialize, Clone)]
#[derive(Debug, Clone)]
pub struct Environment {
#[serde(default, deserialize_with = "deserialize_accounts")]
pub accounts: Option<Vec<Account>>,
pub network: Network,
pub contracts: Option<Map<Box<str>, Contract>>,
pub contracts: Option<IndexMap<Box<str>, Contract>>,
}

fn deserialize_accounts<'de, D>(deserializer: D) -> Result<Option<Vec<Account>>, D::Error>
Expand All @@ -31,6 +33,43 @@ where
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 {
Expand Down Expand Up @@ -90,8 +129,8 @@ impl Environment {
return Ok(None);
}

let toml_str = std::fs::read_to_string(env_toml).map_err(Error::ParsingToml)?;
let mut parsed_toml: Environments = toml::from_str(&toml_str).unwrap();
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()));
Expand Down
114 changes: 114 additions & 0 deletions crates/loam-cli/tests/it/build_clients/init_script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,117 @@ fn init_handles_quotations_and_subcommands_in_script() {
));
})
}

#[test]
fn init_scripts_run_in_specified_order() {
TestEnv::from("soroban-init-boilerplate", |env| {
let binary_path = env
.find_binary("stellar")
.expect("Stellar binary not found. Test cannot proceed.");
let binary_path_str = binary_path.to_string_lossy();
// First configuration: custom_types then token
env.set_environments_toml(format!(
r#"
development.accounts = [
{{ name = "alice" }},
{{ name = "bob" }},
]
[development.network]
rpc-url = "http://localhost:8000/rpc"
network-passphrase = "Standalone Network ; February 2017"
[development.contracts]
hello_world.client = false
soroban_increment_contract.client = false
soroban_auth_contract.client = false
[development.contracts.soroban_custom_types_contract]
client = true
init = """
test_init --resolution 300000 --assets '[{{"Stellar": "$({} contract id asset --asset native)"}} ]' --decimals 14 --base '{{"Stellar":"$({} contract id asset --asset native)"}}'
"""
[development.contracts.soroban_token_contract]
client = true
init = """
STELLAR_ACCOUNT=bob initialize --symbol ABND --decimal 7 --name abundance --admin bob
STELLAR_ACCOUNT=bob mint --amount 2000000 --to bob
"""
"#,
binary_path_str, binary_path_str
));

let output = env
.loam_env("development", true)
.output()
.expect("Failed to execute command");

let stderr = String::from_utf8_lossy(&output.stderr);
assert!(output.status.success());

// Check order of initialization
let custom_types_index = stderr
.find("Running initialization script for \"soroban_custom")
.expect("Custom types init not found");
let token_index = stderr
.find("Running initialization script for \"soroban_token")
.expect("Token init not found");
assert!(
custom_types_index < token_index,
"Custom types should be initialized before token"
);

// Second configuration: token then custom_types
env.set_environments_toml(format!(
r#"
development.accounts = [
{{ name = "alice" }},
{{ name = "bob" }},
]
[development.network]
rpc-url = "http://localhost:8000/rpc"
network-passphrase = "Standalone Network ; February 2017"
[development.contracts]
hello_world.client = false
soroban_increment_contract.client = false
soroban_auth_contract.client = false
[development.contracts.soroban_token_contract]
client = true
init = """
STELLAR_ACCOUNT=bob initialize --symbol ABND --decimal 7 --name abundance --admin bob
STELLAR_ACCOUNT=bob mint --amount 2000000 --to bob
"""
[development.contracts.soroban_custom_types_contract]
client = true
init = """
test_init --resolution 300000 --assets '[{{"Stellar": "$({} contract id asset --asset native)"}} ]' --decimals 14 --base '{{"Stellar":"$({} contract id asset --asset native)"}}'
"""
"#,
binary_path_str, binary_path_str));

let output = env
.loam_env("development", true)
.output()
.expect("Failed to execute command");

let stderr = String::from_utf8_lossy(&output.stderr);
assert!(output.status.success());

// Check order of initialization
let token_index = stderr
.find("Running initialization script for \"soroban_token")
.expect("Token init not found");
let custom_types_index = stderr
.find("Running initialization script for \"soroban_custom")
.expect("Custom types init not found");
assert!(
token_index < custom_types_index,
"Token should be initialized before custom types"
);
})
}

0 comments on commit 157c70b

Please sign in to comment.