use crate::{util, Abigen, Context, ContractBindings, ContractFilter, ExpandedContract};
use eyre::Result;
use inflector::Inflector;
use proc_macro2::TokenStream;
use quote::quote;
use std::{
collections::{BTreeMap, BTreeSet},
fs,
io::Write,
path::{Path, PathBuf},
};
use toml::Value;
const DEFAULT_ETHERS_DEP: &str =
"ethers = { version = \"2\", default-features = false, features = [\"abigen\"] }";
#[derive(Debug, Clone)]
pub struct MultiAbigen {
abigens: Vec<Abigen>,
}
impl std::ops::Deref for MultiAbigen {
type Target = Vec<Abigen>;
fn deref(&self) -> &Self::Target {
&self.abigens
}
}
impl std::ops::DerefMut for MultiAbigen {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.abigens
}
}
impl From<Vec<Abigen>> for MultiAbigen {
fn from(abigens: Vec<Abigen>) -> Self {
Self { abigens }
}
}
impl std::iter::FromIterator<Abigen> for MultiAbigen {
fn from_iter<I: IntoIterator<Item = Abigen>>(iter: I) -> Self {
iter.into_iter().collect::<Vec<_>>().into()
}
}
impl MultiAbigen {
pub fn new<I, Name, Source>(abis: I) -> Result<Self>
where
I: IntoIterator<Item = (Name, Source)>,
Name: AsRef<str>,
Source: AsRef<str>,
{
let abis = abis
.into_iter()
.map(|(contract_name, abi_source)| Abigen::new(contract_name.as_ref(), abi_source))
.collect::<Result<Vec<_>>>()?;
Ok(Self::from_abigens(abis))
}
pub fn from_abigens(abis: impl IntoIterator<Item = Abigen>) -> Self {
abis.into_iter().collect()
}
pub fn from_json_files(root: impl AsRef<Path>) -> Result<Self> {
let root = root.as_ref();
let files = util::json_files(root);
eyre::ensure!(!files.is_empty(), "No json files found in directory: {}", root.display());
files.into_iter().map(Abigen::from_file).collect()
}
#[must_use]
pub fn with_filter(mut self, filter: impl Into<ContractFilter>) -> Self {
self.apply_filter(&filter.into());
self
}
pub fn apply_filter(&mut self, filter: &ContractFilter) {
self.abigens.retain(|abi| filter.is_match(abi.contract_name.to_string()))
}
pub fn push(&mut self, abigen: Abigen) {
self.abigens.push(abigen)
}
pub fn build(self) -> Result<MultiBindings> {
let format = self.abigens.iter().any(|gen| gen.format);
Ok(MultiBindings {
expansion: MultiExpansion::from_abigen(self.abigens)?.expand(),
format,
dependencies: vec![],
})
}
}
pub struct MultiExpansion {
contracts: Vec<(ExpandedContract, Context)>,
}
impl MultiExpansion {
pub fn new(contracts: Vec<(ExpandedContract, Context)>) -> Self {
Self { contracts }
}
pub fn from_abigen(abigens: impl IntoIterator<Item = Abigen>) -> Result<Self> {
let contracts = abigens.into_iter().map(|abigen| abigen.expand()).collect::<Result<_>>()?;
Ok(Self::new(contracts))
}
pub fn expand_inplace(self) -> TokenStream {
self.expand().expand_inplace()
}
pub fn expand(self) -> MultiExpansionResult {
let mut expansions = self.contracts;
let mut shared_types = Vec::new();
let mut dirty_contracts = BTreeSet::new();
if expansions.len() > 1 {
let mut conflicts: BTreeMap<String, Vec<usize>> = BTreeMap::new();
for (idx, (_, ctx)) in expansions.iter().enumerate() {
for type_identifier in ctx.internal_structs().rust_type_names().keys() {
conflicts
.entry(type_identifier.clone())
.or_insert_with(|| Vec::with_capacity(1))
.push(idx);
}
}
for (id, contracts) in conflicts.iter().filter(|(_, c)| c.len() > 1) {
shared_types.push(
expansions[contracts[0]]
.1
.struct_definition(id)
.expect("struct def succeeded previously"),
);
for contract in contracts.iter().copied() {
expansions[contract].1.remove_struct(id);
dirty_contracts.insert(contract);
}
}
for contract in dirty_contracts.iter().copied() {
let (expanded, ctx) = &mut expansions[contract];
expanded.abi_structs = ctx.abi_structs().expect("struct def succeeded previously");
}
}
MultiExpansionResult { root: None, contracts: expansions, dirty_contracts, shared_types }
}
}
pub struct MultiExpansionResult {
root: Option<PathBuf>,
contracts: Vec<(ExpandedContract, Context)>,
dirty_contracts: BTreeSet<usize>,
shared_types: Vec<TokenStream>,
}
impl MultiExpansionResult {
pub fn expand_inplace(mut self) -> TokenStream {
let mut tokens = TokenStream::new();
let shared_types_module = quote! {__shared_types};
let shared_path = quote!(
pub use super::#shared_types_module::*;
);
self.add_shared_import_path(shared_path);
let Self { contracts, shared_types, .. } = self;
if !shared_types.is_empty() {
tokens.extend(quote! {
pub mod #shared_types_module {
#( #shared_types )*
}
});
}
tokens.extend(contracts.into_iter().map(|(exp, _)| exp.into_tokens()));
tokens
}
pub fn set_root(&mut self, root: impl Into<PathBuf>) {
self.root = Some(root.into());
}
fn set_shared_import_path(&mut self, single_file: bool) {
let shared_path = if single_file {
quote!(
pub use super::__shared_types::*;
)
} else {
quote!(
pub use super::super::shared_types::*;
)
};
self.add_shared_import_path(shared_path);
}
fn add_shared_import_path(&mut self, shared: TokenStream) {
for contract in self.dirty_contracts.iter().copied() {
let (expanded, ..) = &mut self.contracts[contract];
expanded.imports.extend(shared.clone());
}
}
fn into_bindings(
mut self,
single_file: bool,
format: bool,
dependencies: Vec<String>,
) -> MultiBindingsInner {
self.set_shared_import_path(single_file);
let Self { contracts, shared_types, root, .. } = self;
let bindings = contracts
.into_iter()
.map(|(expanded, ctx)| ContractBindings {
tokens: expanded.into_tokens(),
format,
name: ctx.contract_name().to_string(),
})
.map(|v| (v.name.clone(), v))
.collect();
let shared_types = if !shared_types.is_empty() {
let shared_types = if single_file {
quote! {
pub mod __shared_types {
#( #shared_types )*
}
}
} else {
quote! {
#( #shared_types )*
}
};
Some(ContractBindings {
tokens: shared_types,
format,
name: "shared_types".to_string(),
})
} else {
None
};
MultiBindingsInner { root, bindings, shared_types, dependencies }
}
}
pub struct MultiBindings {
expansion: MultiExpansionResult,
format: bool,
dependencies: Vec<String>,
}
impl MultiBindings {
pub fn len(&self) -> usize {
self.expansion.contracts.len()
}
pub fn is_empty(&self) -> bool {
self.expansion.contracts.is_empty()
}
#[must_use]
#[deprecated = "Use format instead"]
#[doc(hidden)]
pub fn rustfmt(mut self, rustfmt: bool) -> Self {
self.format = rustfmt;
self
}
pub fn format(mut self, format: bool) -> Self {
self.format = format;
self
}
pub fn dependencies(
mut self,
dependencies: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
self.dependencies = dependencies.into_iter().map(|dep| dep.into()).collect();
self
}
fn into_inner(self, single_file: bool) -> MultiBindingsInner {
self.expansion.into_bindings(single_file, self.format, self.dependencies)
}
pub fn write_to_module(self, module: impl AsRef<Path>, single_file: bool) -> Result<()> {
self.into_inner(single_file).write_to_module(module, single_file)
}
pub fn write_to_crate(
self,
name: impl AsRef<str>,
version: impl AsRef<str>,
lib: impl AsRef<Path>,
single_file: bool,
) -> Result<()> {
self.into_inner(single_file).write_to_crate(name, version, lib, single_file)
}
pub fn ensure_consistent_crate(
self,
name: impl AsRef<str>,
version: impl AsRef<str>,
crate_path: impl AsRef<Path>,
single_file: bool,
check_cargo_toml: bool,
) -> Result<()> {
self.into_inner(single_file).ensure_consistent_crate(
name,
version,
crate_path,
single_file,
check_cargo_toml,
)
}
pub fn ensure_consistent_module(
self,
module: impl AsRef<Path>,
single_file: bool,
) -> Result<()> {
self.into_inner(single_file).ensure_consistent_module(module, single_file)
}
}
struct MultiBindingsInner {
root: Option<PathBuf>,
bindings: BTreeMap<String, ContractBindings>,
shared_types: Option<ContractBindings>,
dependencies: Vec<String>,
}
impl std::ops::Deref for MultiBindingsInner {
type Target = BTreeMap<String, ContractBindings>;
fn deref(&self) -> &Self::Target {
&self.bindings
}
}
impl MultiBindingsInner {
fn generate_cargo_toml(
&self,
name: impl AsRef<str>,
version: impl AsRef<str>,
crate_version: String,
) -> Result<Vec<u8>> {
let mut toml = vec![];
writeln!(toml, "[package]")?;
writeln!(toml, r#"name = "{}""#, name.as_ref())?;
writeln!(toml, r#"version = "{}""#, version.as_ref())?;
writeln!(toml, r#"edition = "2021""#)?;
writeln!(toml)?;
writeln!(toml, "# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html")?;
writeln!(toml)?;
writeln!(toml, "[dependencies]")?;
writeln!(toml, r#"{crate_version}"#)?;
for dependency in self.dependencies.clone() {
writeln!(toml, "{}", dependency)?;
}
Ok(toml)
}
fn crate_version(&self) -> String {
self.try_find_crate_version().unwrap_or_else(|_| DEFAULT_ETHERS_DEP.to_string())
}
fn try_find_crate_version(&self) -> Result<String> {
let cargo_toml =
if let Some(root) = self.root.clone() { root } else { std::env::current_dir()? }
.join("Cargo.toml");
if !cargo_toml.exists() {
return Ok(DEFAULT_ETHERS_DEP.to_string())
}
let data = fs::read_to_string(cargo_toml)?;
let toml = data.parse::<Value>()?;
let ethers = toml
.get("dependencies")
.and_then(|v| v.get("ethers").or_else(|| v.get("ethers-contract")))
.ok_or_else(|| eyre::eyre!("couldn't find ethers or ethers-contract dependency"))?;
if let Some(rev) = ethers.get("rev") {
Ok(format!("ethers = {{ git = \"https://github.com/gakonst/ethers-rs\", rev = {rev}, default-features = false, features = [\"abigen\"] }}"))
} else if let Some(version) = ethers.get("version") {
Ok(format!(
"ethers = {{ version = {version}, default-features = false, features = [\"abigen\"] }}"
))
} else {
Ok(DEFAULT_ETHERS_DEP.to_string())
}
}
fn write_cargo_toml(
&self,
lib: &Path,
name: impl AsRef<str>,
version: impl AsRef<str>,
) -> Result<()> {
let crate_version = self.crate_version();
let contents = self.generate_cargo_toml(name, version, crate_version)?;
let mut file = fs::OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(lib.join("Cargo.toml"))?;
file.write_all(&contents)?;
Ok(())
}
fn append_module_names(&self, mut buf: impl Write) -> Result<()> {
let mut mod_names: BTreeSet<_> =
self.bindings.keys().map(|name| util::safe_module_name(name)).collect();
if let Some(ref shared) = self.shared_types {
mod_names.insert(shared.name.to_snake_case());
}
for module in mod_names.into_iter().map(|name| format!("pub mod {name};")) {
writeln!(buf, "{module}")?;
}
Ok(())
}
fn generate_super_contents(&self, is_crate: bool, single_file: bool) -> Result<Vec<u8>> {
let mut contents = vec![];
generate_prefix(&mut contents, is_crate, single_file)?;
if single_file {
if let Some(ref shared) = self.shared_types {
shared.write(&mut contents)?;
}
for binding in self.bindings.values() {
binding.write(&mut contents)?;
}
} else {
self.append_module_names(&mut contents)?;
}
Ok(contents)
}
fn write_super_file(&self, path: &Path, is_crate: bool, single_file: bool) -> Result<()> {
let filename = if is_crate { "lib.rs" } else { "mod.rs" };
let contents = self.generate_super_contents(is_crate, single_file)?;
fs::write(path.join(filename), contents)?;
Ok(())
}
fn write_bindings(&self, path: &Path) -> Result<()> {
if let Some(ref shared) = self.shared_types {
shared.write_module_in_dir(path)?;
}
for binding in self.bindings.values() {
binding.write_module_in_dir(path)?;
}
Ok(())
}
fn write_to_module(self, module: impl AsRef<Path>, single_file: bool) -> Result<()> {
let module = module.as_ref();
fs::create_dir_all(module)?;
self.write_super_file(module, false, single_file)?;
if !single_file {
self.write_bindings(module)?;
}
Ok(())
}
fn write_to_crate(
self,
name: impl AsRef<str>,
version: impl AsRef<str>,
lib: impl AsRef<Path>,
single_file: bool,
) -> Result<()> {
let lib = lib.as_ref();
let src = lib.join("src");
fs::create_dir_all(&src)?;
self.write_cargo_toml(lib, name, version)?;
self.write_super_file(&src, true, single_file)?;
if !single_file {
self.write_bindings(&src)?;
}
Ok(())
}
fn ensure_consistent_bindings(
self,
dir: impl AsRef<Path>,
is_crate: bool,
single_file: bool,
) -> Result<()> {
let dir = dir.as_ref();
let super_name = if is_crate { "lib.rs" } else { "mod.rs" };
let super_contents = self.generate_super_contents(is_crate, single_file)?;
check_file_in_dir(dir, super_name, &super_contents)?;
if !single_file {
for binding in self.bindings.values() {
check_binding_in_dir(dir, binding)?;
}
}
Ok(())
}
fn ensure_consistent_crate(
self,
name: impl AsRef<str>,
version: impl AsRef<str>,
crate_path: impl AsRef<Path>,
single_file: bool,
check_cargo_toml: bool,
) -> Result<()> {
let crate_path = crate_path.as_ref();
if check_cargo_toml {
let crate_version = self.crate_version();
let cargo_contents = self.generate_cargo_toml(name, version, crate_version)?;
check_file_in_dir(crate_path, "Cargo.toml", &cargo_contents)?;
}
self.ensure_consistent_bindings(crate_path.join("src"), true, single_file)?;
Ok(())
}
fn ensure_consistent_module(self, module: impl AsRef<Path>, single_file: bool) -> Result<()> {
self.ensure_consistent_bindings(module, false, single_file)?;
Ok(())
}
}
fn generate_prefix(mut buf: impl Write, is_crate: bool, single_file: bool) -> Result<()> {
writeln!(buf, "#![allow(clippy::all)]")?;
writeln!(
buf,
"//! This {} contains abigen! generated bindings for solidity contracts.",
if is_crate { "lib" } else { "module" }
)?;
writeln!(buf, "//! This is autogenerated code.")?;
writeln!(buf, "//! Do not manually edit these files.")?;
writeln!(
buf,
"//! {} may be overwritten by the codegen system at any time.",
if single_file && !is_crate { "This file" } else { "These files" }
)?;
Ok(())
}
fn check_file_in_dir(dir: &Path, file_name: &str, expected_contents: &[u8]) -> Result<()> {
eyre::ensure!(dir.is_dir(), "Not a directory: {}", dir.display());
let file_path = dir.join(file_name);
eyre::ensure!(file_path.is_file(), "Not a file: {}", file_path.display());
let contents = fs::read(&file_path).expect("Unable to read file");
eyre::ensure!(contents == expected_contents, format!("The contents of `{}` do not match the expected output of the newest `ethers::Abigen` version.\
This indicates that the existing bindings are outdated and need to be generated again.", file_path.display()));
Ok(())
}
fn check_binding_in_dir(dir: &Path, binding: &ContractBindings) -> Result<()> {
let name = binding.module_filename();
let contents = binding.to_vec();
check_file_in_dir(dir, &name, &contents)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ExcludeContracts, SelectContracts};
use std::env;
struct Context {
multi_gen: MultiAbigen,
mod_root: PathBuf,
}
fn run_test<T>(test: T)
where
T: FnOnce(Context),
{
let crate_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let console = Abigen::new(
"Console",
crate_root.join("../tests/solidity-contracts/console.json").display().to_string(),
)
.unwrap();
let simple_storage = Abigen::new(
"SimpleStorage",
crate_root.join("../tests/solidity-contracts/SimpleStorage.json").display().to_string(),
)
.unwrap();
let human_readable = Abigen::new(
"HrContract",
r"[
struct Foo { uint256 x; }
function foo(Foo memory x)
function bar(uint256 x, uint256 y, address addr)
yeet(uint256,uint256,address)
]",
)
.unwrap();
let multi_gen = MultiAbigen::from_abigens([console, simple_storage, human_readable]);
let tmp = tempfile::tempdir().unwrap();
let mod_root = tmp.path().join("contracts");
let context = Context { multi_gen, mod_root };
test(context)
}
#[test]
fn can_generate_multi_file_module() {
run_test(|context| {
let Context { multi_gen, mod_root } = context;
let single_file = false;
multi_gen.clone().build().unwrap().write_to_module(&mod_root, single_file).unwrap();
multi_gen
.build()
.unwrap()
.ensure_consistent_module(mod_root, single_file)
.expect("Inconsistent bindings");
})
}
#[test]
fn can_find_ethers_dep() {
run_test(|context| {
let Context { multi_gen, mod_root } = context;
let single_file = true;
let mut inner = multi_gen.build().unwrap().into_inner(single_file);
inner.root = Some(PathBuf::from("this does not exist"));
inner.write_to_module(mod_root, single_file).unwrap();
})
}
#[test]
fn can_generate_single_file_module() {
run_test(|context| {
let Context { multi_gen, mod_root } = context;
let single_file = true;
multi_gen.clone().build().unwrap().write_to_module(&mod_root, single_file).unwrap();
multi_gen
.build()
.unwrap()
.ensure_consistent_module(mod_root, single_file)
.expect("Inconsistent bindings");
})
}
#[test]
fn can_generate_multi_file_crate() {
run_test(|context| {
let Context { multi_gen, mod_root } = context;
let single_file = false;
let name = "a-name";
let version = "290.3782.3";
multi_gen
.clone()
.build()
.unwrap()
.write_to_crate(name, version, &mod_root, single_file)
.unwrap();
multi_gen
.build()
.unwrap()
.ensure_consistent_crate(name, version, mod_root, single_file, true)
.expect("Inconsistent bindings");
})
}
#[test]
fn can_generate_single_file_crate() {
run_test(|context| {
let Context { multi_gen, mod_root } = context;
let single_file = true;
let name = "a-name";
let version = "290.3782.3";
multi_gen
.clone()
.build()
.unwrap()
.write_to_crate(name, version, &mod_root, single_file)
.unwrap();
multi_gen
.build()
.unwrap()
.ensure_consistent_crate(name, version, mod_root, single_file, true)
.expect("Inconsistent bindings");
})
}
#[test]
fn can_detect_incosistent_multi_file_module() {
run_test(|context| {
let Context { mut multi_gen, mod_root } = context;
let single_file = false;
multi_gen.clone().build().unwrap().write_to_module(&mod_root, single_file).unwrap();
multi_gen.push(
Abigen::new(
"AdditionalContract",
r"[
getValue() (uint256)
]",
)
.unwrap(),
);
let result =
multi_gen.build().unwrap().ensure_consistent_module(mod_root, single_file).is_err();
assert!(result, "Inconsistent bindings wrongly approved");
})
}
#[test]
fn can_detect_incosistent_single_file_module() {
run_test(|context| {
let Context { mut multi_gen, mod_root } = context;
let single_file = true;
multi_gen.clone().build().unwrap().write_to_module(&mod_root, single_file).unwrap();
multi_gen.push(
Abigen::new(
"AdditionalContract",
r"[
getValue() (uint256)
]",
)
.unwrap(),
);
let result =
multi_gen.build().unwrap().ensure_consistent_module(mod_root, single_file).is_err();
assert!(result, "Inconsistent bindings wrongly approved");
})
}
#[test]
fn can_detect_incosistent_multi_file_crate() {
run_test(|context| {
let Context { mut multi_gen, mod_root } = context;
let single_file = false;
let name = "a-name";
let version = "290.3782.3";
multi_gen
.clone()
.build()
.unwrap()
.write_to_crate(name, version, &mod_root, single_file)
.unwrap();
multi_gen.push(
Abigen::new(
"AdditionalContract",
r"[
getValue() (uint256)
]",
)
.unwrap(),
);
let result = multi_gen
.build()
.unwrap()
.ensure_consistent_crate(name, version, mod_root, single_file, true)
.is_err();
assert!(result, "Inconsistent bindings wrongly approved");
})
}
#[test]
fn can_detect_inconsistent_single_file_crate() {
run_test(|context| {
let Context { mut multi_gen, mod_root } = context;
let single_file = true;
let name = "a-name";
let version = "290.3782.3";
multi_gen
.clone()
.build()
.unwrap()
.write_to_crate(name, version, &mod_root, single_file)
.unwrap();
multi_gen.push(
Abigen::new(
"AdditionalContract",
r"[
getValue() (uint256)
]",
)
.unwrap(),
);
let result = multi_gen
.build()
.unwrap()
.ensure_consistent_crate(name, version, mod_root, single_file, true)
.is_err();
assert!(result, "Inconsistent bindings wrongly approved");
})
}
#[test]
fn does_not_generate_shared_types_if_empty() {
let gen = Abigen::new(
"Greeter",
r"[
struct Inner {bool a;}
greet1() (uint256)
greet2(Inner inner) (string)
]",
)
.unwrap();
let tokens = MultiExpansion::new(vec![gen.expand().unwrap()]).expand_inplace().to_string();
assert!(!tokens.contains("mod __shared_types"));
}
#[test]
fn can_filter_abigen() {
let abi = Abigen::new(
"MyGreeter",
r"[
greet() (string)
]",
)
.unwrap();
let mut gen = MultiAbigen::from_abigens(vec![abi]).with_filter(ContractFilter::All);
assert_eq!(gen.abigens.len(), 1);
gen.apply_filter(&SelectContracts::default().add_name("MyGreeter").into());
assert_eq!(gen.abigens.len(), 1);
gen.apply_filter(&ExcludeContracts::default().add_name("MyGreeter2").into());
assert_eq!(gen.abigens.len(), 1);
let filtered = gen.clone().with_filter(SelectContracts::default().add_name("MyGreeter2"));
assert!(filtered.abigens.is_empty());
let filtered = gen.clone().with_filter(ExcludeContracts::default().add_name("MyGreeter"));
assert!(filtered.abigens.is_empty());
let filtered =
gen.clone().with_filter(SelectContracts::default().add_pattern("MyGreeter2"));
assert!(filtered.abigens.is_empty());
let filtered =
gen.clone().with_filter(ExcludeContracts::default().add_pattern("MyGreeter"));
assert!(filtered.abigens.is_empty());
gen.push(
Abigen::new(
"MyGreeterTest",
r"[
greet() (string)
]",
)
.unwrap(),
);
let filtered = gen.clone().with_filter(SelectContracts::default().add_pattern(".*Test"));
assert_eq!(filtered.abigens.len(), 1);
assert_eq!(filtered.abigens[0].contract_name, "MyGreeterTest");
let filtered = gen.clone().with_filter(ExcludeContracts::default().add_pattern(".*Test"));
assert_eq!(filtered.abigens.len(), 1);
assert_eq!(filtered.abigens[0].contract_name, "MyGreeter");
}
#[test]
fn can_deduplicate_types() {
let root = tempfile::tempdir().unwrap();
let json_files = "../tests/solidity-contracts/greeter";
let gen = MultiAbigen::from_json_files(json_files).unwrap();
let bindings = gen.clone().build().unwrap();
let single_file_dir = root.path().join("single_bindings");
bindings.write_to_module(&single_file_dir, true).unwrap();
let single_file_mod = single_file_dir.join("mod.rs");
assert!(single_file_mod.exists());
let content = fs::read_to_string(&single_file_mod).unwrap();
assert!(content.contains("mod __shared_types"));
assert!(content.contains("pub struct Inner"));
assert!(content.contains("pub struct Stuff"));
let bindings = gen.build().unwrap();
let multi_file_dir = root.path().join("multi_bindings");
bindings.write_to_module(&multi_file_dir, false).unwrap();
let multi_file_mod = multi_file_dir.join("mod.rs");
assert!(multi_file_mod.exists());
let content = fs::read_to_string(&multi_file_mod).unwrap();
assert!(content.contains("pub mod shared_types"));
let greeter1 = multi_file_dir.join("greeter_1.rs");
assert!(greeter1.exists());
let content = fs::read_to_string(&greeter1).unwrap();
assert!(!content.contains("pub struct Inner"));
assert!(!content.contains("pub struct Stuff"));
let greeter2 = multi_file_dir.join("greeter_2.rs");
assert!(greeter2.exists());
let content = fs::read_to_string(&greeter2).unwrap();
assert!(!content.contains("pub struct Inner"));
assert!(!content.contains("pub struct Stuff"));
let shared_types = multi_file_dir.join("shared_types.rs");
assert!(shared_types.exists());
let content = fs::read_to_string(&shared_types).unwrap();
assert!(content.contains("pub struct Inner"));
assert!(content.contains("pub struct Stuff"));
}
#[test]
fn can_sanitize_reserved_words() {
let root = tempfile::tempdir().unwrap();
let json_files = "../tests/solidity-contracts/ReservedWords";
let gen = MultiAbigen::from_json_files(json_files).unwrap();
let bindings = gen.clone().build().unwrap();
let single_file_dir = root.path().join("single_bindings");
bindings.write_to_module(&single_file_dir, true).unwrap();
let single_file_mod = single_file_dir.join("mod.rs");
assert!(single_file_mod.exists());
let content = fs::read_to_string(&single_file_mod).unwrap();
assert!(content.contains("pub mod mod_ {"));
assert!(content.contains("pub mod enum_ {"));
let bindings = gen.build().unwrap();
let multi_file_dir = root.path().join("multi_bindings");
bindings.write_to_module(&multi_file_dir, false).unwrap();
let multi_file_mod = multi_file_dir.join("mod.rs");
assert!(multi_file_mod.exists());
let content = fs::read_to_string(&multi_file_mod).unwrap();
assert!(content.contains("pub mod enum_;"));
assert!(content.contains("pub mod mod_;"));
let enum_ = multi_file_dir.join("enum_.rs");
assert!(enum_.exists());
let content = fs::read_to_string(&enum_).unwrap();
assert!(content.contains("pub mod enum_ {"));
let mod_ = multi_file_dir.join("mod_.rs");
assert!(mod_.exists());
let content = fs::read_to_string(&mod_).unwrap();
assert!(content.contains("pub mod mod_ {"));
}
#[test]
fn parse_ethers_crate() {
run_test(|context| {
let Context { multi_gen, mod_root } = context;
let manifest = r#"
[package]
name = "ethers-contract"
version = "1.0.0"
edition = "2021"
rust-version = "1.64"
authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
license = "MIT OR Apache-2.0"
description = "Smart contract bindings for the ethers-rs crate"
homepage = "https://docs.rs/ethers"
repository = "https://github.com/gakonst/ethers-rs"
keywords = ["ethereum", "web3", "celo", "ethers"]
[dependencies]
ethers-providers = { version = "^1.0.0", path = "../ethers-providers", default-features = false }
"#;
let root = mod_root.parent().unwrap();
fs::write(root.join("../Cargo.toml"), manifest).unwrap();
env::set_var("CARGO_MANIFEST_DIR", root);
let single_file = false;
let name = "a-name";
let version = "290.3782.3";
multi_gen
.clone()
.build()
.unwrap()
.write_to_crate(name, version, &mod_root, single_file)
.unwrap();
multi_gen
.build()
.unwrap()
.ensure_consistent_crate(name, version, &mod_root, single_file, true)
.expect("Inconsistent bindings");
});
run_test(|context| {
let Context { multi_gen, mod_root } = context;
let manifest = r#"
[package]
name = "ethers-contract"
version = "1.0.0"
edition = "2021"
rust-version = "1.64"
authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
license = "MIT OR Apache-2.0"
description = "Smart contract bindings for the ethers-rs crate"
homepage = "https://docs.rs/ethers"
repository = "https://github.com/gakonst/ethers-rs"
keywords = ["ethereum", "web3", "celo", "ethers"]
[dependencies]
ethers-contracts = "0.4.0"
"#;
let root = mod_root.parent().unwrap();
fs::write(root.join("../Cargo.toml"), manifest).unwrap();
env::set_var("CARGO_MANIFEST_DIR", root);
let single_file = false;
let name = "a-name";
let version = "290.3782.3";
multi_gen
.clone()
.build()
.unwrap()
.write_to_crate(name, version, &mod_root, single_file)
.unwrap();
multi_gen
.build()
.unwrap()
.ensure_consistent_crate(name, version, mod_root, single_file, true)
.expect("Inconsistent bindings");
});
run_test(|context| {
let Context { multi_gen, mod_root } = context;
let manifest = r#"
[package]
name = "ethers-contract"
version = "1.0.0"
edition = "2021"
rust-version = "1.64"
authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
license = "MIT OR Apache-2.0"
description = "Smart contract bindings for the ethers-rs crate"
homepage = "https://docs.rs/ethers"
repository = "https://github.com/gakonst/ethers-rs"
keywords = ["ethereum", "web3", "celo", "ethers"]
[dependencies]
ethers = {git="https://github.com/gakonst/ethers-rs", rev = "fd8ebf5",features = ["ws", "rustls", "ipc"] }
"#;
let root = mod_root.parent().unwrap();
fs::write(root.join("../Cargo.toml"), manifest).unwrap();
env::set_var("CARGO_MANIFEST_DIR", root);
let single_file = false;
let name = "a-name";
let version = "290.3782.3";
multi_gen
.clone()
.build()
.unwrap()
.write_to_crate(name, version, &mod_root, single_file)
.unwrap();
multi_gen
.build()
.unwrap()
.ensure_consistent_crate(name, version, mod_root, single_file, true)
.expect("Inconsistent bindings");
});
run_test(|context| {
let Context { multi_gen, mod_root } = context;
let manifest = r#"
[package]
name = "ethers-contract"
version = "1.0.0"
edition = "2021"
rust-version = "1.64"
authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
license = "MIT OR Apache-2.0"
description = "Smart contract bindings for the ethers-rs crate"
homepage = "https://docs.rs/ethers"
repository = "https://github.com/gakonst/ethers-rs"
keywords = ["ethereum", "web3", "celo", "ethers"]
[dependencies]
ethers = {git = "https://github.com/gakonst/ethers-rs", features = ["ws", "rustls", "ipc"] }
"#;
let root = mod_root.parent().unwrap();
fs::write(root.join("../Cargo.toml"), manifest).unwrap();
env::set_var("CARGO_MANIFEST_DIR", root);
let single_file = false;
let name = "a-name";
let version = "290.3782.3";
multi_gen
.clone()
.build()
.unwrap()
.write_to_crate(name, version, &mod_root, single_file)
.unwrap();
multi_gen
.build()
.unwrap()
.ensure_consistent_crate(name, version, mod_root, single_file, true)
.expect("Inconsistent bindings");
});
}
}