From 5f68bf52d2ce559c772f8fbf2871b5124d9883b1 Mon Sep 17 00:00:00 2001 From: James Prestwich <10149425+prestwich@users.noreply.github.com> Date: Wed, 2 Feb 2022 05:57:31 -0800 Subject: [PATCH] refactor: MultiAbigen rework (#852) * refactor: MultiAbigen rework * docs: update ensure family docs * feature: FromIterator for MultiAbigen * refactor: cleaner instantiation of Abigen from files * docs: update examples to use build step * chore: add 854 to changelog * lint: clippy * Update ethers-contract/ethers-contract-abigen/src/lib.rs Co-authored-by: Georgios Konstantopoulos --- CHANGELOG.md | 4 + .../ethers-contract-abigen/src/lib.rs | 330 ++------ .../ethers-contract-abigen/src/multi.rs | 754 ++++++++++++++++++ 3 files changed, 805 insertions(+), 283 deletions(-) create mode 100644 ethers-contract/ethers-contract-abigen/src/multi.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index c2b6255e..1b0bc972 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,9 @@ - Move `fill_transaction` implementation to the provider, to allow middleware to properly override its behavior. - Add informational messages to solc installation and compilation. +- Significantly refactor `MultiAbigen` module generation. Now allows for lib + generation, and does not make unnecessary disk writes. + [#854](https://github.com/gakonst/ethers-rs/pull/852) ## ethers-contract-abigen @@ -105,6 +108,7 @@ [640](https://github.com/gakonst/ethers-rs/pull/640) ### Unreleased + - Add support for basic and bearer authentication in http and non-wasm websockets. [829](https://github.com/gakonst/ethers-rs/pull/829) diff --git a/ethers-contract/ethers-contract-abigen/src/lib.rs b/ethers-contract/ethers-contract-abigen/src/lib.rs index 5df72810..85a4abd4 100644 --- a/ethers-contract/ethers-contract-abigen/src/lib.rs +++ b/ethers-contract/ethers-contract-abigen/src/lib.rs @@ -19,6 +19,9 @@ mod rustfmt; mod source; mod util; +pub mod multi; +pub use multi::MultiAbigen; + pub use ethers_core::types::Address; pub use source::Source; pub use util::parse_address; @@ -26,12 +29,7 @@ pub use util::parse_address; use anyhow::Result; use inflector::Inflector; use proc_macro2::TokenStream; -use std::{ - collections::HashMap, - fs::{self, File}, - io::Write, - path::Path, -}; +use std::{collections::HashMap, fs::File, io::Write, path::Path}; /// Builder struct for generating type-safe bindings from a contract's ABI /// @@ -86,6 +84,19 @@ impl Abigen { }) } + /// Attemtps to load a new builder from an ABI JSON file at the specific + /// path. + pub fn from_file(path: impl AsRef) -> Result { + let name = path + .as_ref() + .file_stem() + .ok_or_else(|| anyhow::format_err!("Missing file stem in path"))? + .to_str() + .ok_or_else(|| anyhow::format_err!("Unable to convert file stem to string"))?; + + Self::new(name, std::fs::read_to_string(path.as_ref())?) + } + /// Manually adds a solidity event alias to specify what the event struct /// and function name will be in Rust. #[must_use] @@ -138,8 +149,9 @@ impl Abigen { /// Generates the contract bindings. pub fn generate(self) -> Result { let rustfmt = self.rustfmt; + let name = self.contract_name.clone(); let tokens = Context::from_abigen(self)?.expand()?.into_tokens(); - Ok(ContractBindings { tokens, rustfmt }) + Ok(ContractBindings { tokens, rustfmt, name }) } } @@ -150,6 +162,8 @@ pub struct ContractBindings { tokens: TokenStream, /// The output options used for serialization. rustfmt: bool, + /// The contract name + name: String, } impl ContractBindings { @@ -172,6 +186,13 @@ impl ContractBindings { Ok(()) } + /// Writes the bindings to a new Vec. Panics if unable to allocate + pub fn to_vec(&self) -> Vec { + let mut bindings = vec![]; + self.write(&mut bindings).expect("allocations don't fail"); + bindings + } + /// Writes the bindings to the specified file. pub fn write_to_file

(&self, path: P) -> Result<()> where @@ -181,289 +202,32 @@ impl ContractBindings { self.write(file) } + /// Writes the bindings to a `contract_name.rs` file in the specified + /// directory. The filename is the snake_case transformation of the contract + /// name. + pub fn write_module_in_dir

(&self, dir: P) -> Result<()> + where + P: AsRef, + { + let file = dir.as_ref().join(self.module_filename()); + self.write_to_file(file) + } + /// Converts the bindings into its underlying token stream. This allows it /// to be used within a procedural macro. pub fn into_tokens(self) -> TokenStream { self.tokens } -} -/// Generates bindings for a series of contracts -/// -/// This type can be used to generate multiple `ContractBindings` and put them all in a single rust -/// module, (eg. a `contracts` directory). -/// -/// This can be used to -/// 1) write all bindings directly into a new directory in the project's source directory, so that -/// it is included in the repository. 2) write all bindings to the value of cargo's `OUT_DIR` in a -/// build script and import the bindings as `include!(concat!(env!("OUT_DIR"), "/mod.rs"));`. -/// -/// However, the main purpose of this generator is to create bindings for option `1)` and write all -/// contracts to some `contracts` module in `src`, like `src/contracts/mod.rs` __once__ via a build -/// script or a test. After that it's recommend to remove the build script and replace it with an -/// integration test (See `MultiAbigen::ensure_consistent_bindings`) that fails if the generated -/// code is out of date. This has several advantages: -/// -/// * No need for downstream users to compile the build script -/// * No need for downstream users to run the whole `abigen!` generation steps -/// * The generated code is more usable in an IDE -/// * CI will fail if the generated code is out of date (if `abigen!` or the contract's ABI itself -/// changed) -/// -/// See `MultiAbigen::ensure_consistent_bindings` for the recommended way to set this up to generate -/// the bindings once via a test and then use the test to ensure consistency. -#[derive(Debug, Clone)] -pub struct MultiAbigen { - /// whether to write all contracts in a single file instead of separated modules - single_file: bool, - - abigens: Vec, -} - -impl MultiAbigen { - /// Create a new instance from a series of already resolved `Abigen` - pub fn from_abigen(abis: impl IntoIterator) -> Self { - Self { - single_file: false, - abigens: abis.into_iter().map(|abi| abi.rustfmt(true)).collect(), - } + /// Generate the default module name (snake case of the contract name) + pub fn module_name(&self) -> String { + self.name.to_snake_case() } - /// Create a new instance from a series (`contract name`, `abi_source`) - /// - /// See `Abigen::new` - pub fn new(abis: I) -> Result - where - I: IntoIterator, - Name: AsRef, - Source: AsRef, - { - let abis = abis - .into_iter() - .map(|(contract_name, abi_source)| Abigen::new(contract_name.as_ref(), abi_source)) - .collect::>>()?; - - Ok(Self::from_abigen(abis)) - } - - /// Reads all json files contained in the given `dir` and use the file name for the name of the - /// `ContractBindings`. - /// This is equivalent to calling `MultiAbigen::new` with all the json files and their filename. - /// - /// # Example - /// - /// ```text - /// abi - /// ├── ERC20.json - /// ├── Contract1.json - /// ├── Contract2.json - /// ... - /// ``` - /// - /// ```no_run - /// # use ethers_contract_abigen::MultiAbigen; - /// let gen = MultiAbigen::from_json_files("./abi").unwrap(); - /// ``` - pub fn from_json_files(dir: impl AsRef) -> Result { - let mut abis = Vec::new(); - for file in util::json_files(dir) { - if let Some(file_name) = file.file_stem().and_then(|s| s.to_str()) { - let content = fs::read_to_string(&file)?; - abis.push((file_name.to_string(), content)); - } - } - Self::new(abis) - } - - /// Write all bindings into a single rust file instead of separate modules - #[must_use] - pub fn single_file(mut self) -> Self { - self.single_file = true; - self - } - - /// Generates all the bindings and writes them to the given module - /// - /// # Example - /// - /// Read all json abi files from the `./abi` directory - /// ```text - /// abi - /// ├── ERC20.json - /// ├── Contract1.json - /// ├── Contract2.json - /// ... - /// ``` - /// - /// and write them to the `./src/contracts` location as - /// - /// ```text - /// src/contracts - /// ├── mod.rs - /// ├── er20.rs - /// ├── contract1.rs - /// ├── contract2.rs - /// ... - /// ``` - /// - /// ```no_run - /// # use ethers_contract_abigen::MultiAbigen; - /// let gen = MultiAbigen::from_json_files("./abi").unwrap(); - /// gen.write_to_module("./src/contracts").unwrap(); - /// ``` - pub fn write_to_module(self, module: impl AsRef) -> Result<()> { - let module = module.as_ref(); - fs::create_dir_all(module)?; - - let mut contracts_mod = - b"/// This module contains all the autogenerated abigen! contract bindings\n".to_vec(); - - let mut modules = Vec::new(); - for abi in self.abigens { - let name = abi.contract_name.to_snake_case(); - let bindings = abi.generate()?; - if self.single_file { - // append to the mod file - bindings.write(&mut contracts_mod)?; - } else { - // create a contract rust file - let output = module.join(format!("{}.rs", name)); - bindings.write_to_file(output)?; - modules.push(format!("pub mod {};", name)); - } - } - - if !modules.is_empty() { - modules.sort(); - write!(contracts_mod, "{}", modules.join("\n"))?; - } - - // write the mod file - fs::write(module.join("mod.rs"), contracts_mod)?; - - Ok(()) - } - - /// This ensures that the already generated contract bindings match the output of a fresh new - /// run. Run this in a rust test, to get notified in CI if the newly generated bindings - /// deviate from the already generated ones, and it's time to generate them again. This could - /// happen if the ABI of a contract or the output that `ethers` generates changed. - /// - /// So if this functions is run within a test during CI and fails, then it's time to update all - /// bindings. - /// - /// Returns `true` if the freshly generated bindings match with the existing bindings, `false` - /// otherwise - /// - /// # Example - /// - /// Check that the generated files are up to date - /// - /// ```no_run - /// # use ethers_contract_abigen::MultiAbigen; - /// #[test] - /// fn generated_bindings_are_fresh() { - /// let project_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR")); - /// let abi_dir = project_root.join("abi"); - /// let gen = MultiAbigen::from_json_files(&abi_dir).unwrap(); - /// assert!(gen.ensure_consistent_bindings(project_root.join("src/contracts"))); - /// } - /// - /// gen.write_to_module("./src/contracts").unwrap(); - /// ``` - #[cfg(test)] - pub fn ensure_consistent_bindings(self, module: impl AsRef) -> bool { - let module = module.as_ref(); - let dir = tempfile::tempdir().expect("Failed to create temp dir"); - let temp_module = dir.path().join("contracts"); - self.write_to_module(&temp_module).expect("Failed to generate bindings"); - - for file in fs::read_dir(&temp_module).unwrap() { - let fresh_file = file.unwrap(); - let fresh_file_path = fresh_file.path(); - let file_name = fresh_file_path.file_name().and_then(|p| p.to_str()).unwrap(); - assert!(file_name.ends_with(".rs"), "Expected rust file"); - - let existing_bindings_file = module.join(file_name); - - if !existing_bindings_file.is_file() { - // file does not already exist - return false - } - - // read the existing file - let existing_contract_bindings = fs::read_to_string(existing_bindings_file).unwrap(); - - let fresh_bindings = fs::read_to_string(fresh_file.path()).unwrap(); - - if existing_contract_bindings != fresh_bindings { - return false - } - } - true - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn can_generate_multi_abi() { - let crate_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR")); - - let tempdir = tempfile::tempdir().unwrap(); - let mod_root = tempdir.path().join("contracts"); - - 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_abi.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 mut multi_gen = MultiAbigen::from_abigen([console, simple_storage, human_readable]); - - multi_gen.clone().write_to_module(&mod_root).unwrap(); - assert!(multi_gen.clone().ensure_consistent_bindings(&mod_root)); - - // add another contract - multi_gen.abigens.push( - Abigen::new( - "AdditionalContract", - r#"[ - getValue() (uint256) - getValue(uint256 otherValue) (uint256) - getValue(uint256 otherValue, address addr) (uint256) - ]"#, - ) - .unwrap(), - ); - - // ensure inconsistent bindings are detected - assert!(!multi_gen.clone().ensure_consistent_bindings(&mod_root)); - - // update with new contract - multi_gen.clone().write_to_module(&mod_root).unwrap(); - assert!(multi_gen.clone().ensure_consistent_bindings(&mod_root)); + /// Generate the default filename of the module + pub fn module_filename(&self) -> String { + let mut name = self.module_name(); + name.extend([".rs"]); + name } } diff --git a/ethers-contract/ethers-contract-abigen/src/multi.rs b/ethers-contract/ethers-contract-abigen/src/multi.rs new file mode 100644 index 00000000..19446d61 --- /dev/null +++ b/ethers-contract/ethers-contract-abigen/src/multi.rs @@ -0,0 +1,754 @@ +//! TODO + +use anyhow::Result; +use inflector::Inflector; +use std::{collections::BTreeMap, fs, io::Write, path::Path}; + +use crate::{util, Abigen, ContractBindings}; + +/// Collects Abigen structs for a series of contracts, pending generation of +/// the contract bindings. +#[derive(Debug, Clone)] +pub struct MultiAbigen { + /// Abigen objects to be written + abigens: Vec, +} + +impl std::ops::Deref for MultiAbigen { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.abigens + } +} + +impl From> for MultiAbigen { + fn from(abigens: Vec) -> Self { + Self { abigens } + } +} + +impl std::iter::FromIterator for MultiAbigen { + fn from_iter>(iter: I) -> Self { + iter.into_iter().collect::>().into() + } +} + +impl MultiAbigen { + /// Create a new instance from a series (`contract name`, `abi_source`) + /// + /// See `Abigen::new` + pub fn new(abis: I) -> Result + where + I: IntoIterator, + Name: AsRef, + Source: AsRef, + { + let abis = abis + .into_iter() + .map(|(contract_name, abi_source)| Abigen::new(contract_name.as_ref(), abi_source)) + .collect::>>()?; + + Ok(Self::from_abigens(abis)) + } + + /// Create a new instance from a series of already resolved `Abigen` + pub fn from_abigens(abis: impl IntoIterator) -> Self { + abis.into_iter().collect() + } + + /// Reads all json files contained in the given `dir` and use the file name for the name of the + /// `ContractBindings`. + /// This is equivalent to calling `MultiAbigen::new` with all the json files and their filename. + /// + /// # Example + /// + /// ```text + /// abi + /// ├── ERC20.json + /// ├── Contract1.json + /// ├── Contract2.json + /// ... + /// ``` + /// + /// ```no_run + /// # use ethers_contract_abigen::MultiAbigen; + /// let gen = MultiAbigen::from_json_files("./abi").unwrap(); + /// ``` + pub fn from_json_files(root: impl AsRef) -> Result { + util::json_files(root.as_ref()).into_iter().map(Abigen::from_file).collect() + } + + /// Add another Abigen to the module or lib + pub fn push(&mut self, abigen: Abigen) { + self.abigens.push(abigen) + } + + /// Build the contract bindings and prepare for writing + pub fn build(self) -> Result { + let bindings = self + .abigens + .into_iter() + .map(|v| v.generate()) + .collect::>>()? + .into_iter() + .map(|v| (v.name.clone(), v)) + .collect(); + + Ok(MultiBindings { bindings }) + } +} + +/// Output of the [`MultiAbigen`] build process. `MultiBindings` wraps a group +/// of built contract bindings that have yet to be written to disk. +/// +/// `MultiBindings` enables the user to +/// 1. Write a collection of bindings to a rust module +/// 2. Write a collection of bindings to a rust lib +/// 3. Ensure that a collection of bindings matches an on-disk module or lib. +/// +/// Generally we recommend writing the bindings to a module folder within your +/// rust project. Users seeking to create "official" bindings for some project +/// may instead write an entire library to publish via crates.io. +/// +/// Rather than using `MultiAbigen` in a build script, we recommend committing +/// the generated files, and replacing the build script with an integration +/// test. To enable this, we have provided +/// `MultiBindings::ensure_consistent_bindings` and +/// `MultiBindings::ensure_consistent_crate`. These functions generate the +/// expected module or library in memory, and check that the on-disk files +/// match the expected files. We recommend running these inside CI. +/// +/// This has several advantages: +/// * No need for downstream users to compile the build script +/// * No need for downstream users to run the whole `abigen!` generation steps +/// * The generated code is more usable in an IDE +/// * CI will fail if the generated code is out of date (if `abigen!` or the contract's ABI itself +/// changed) +pub struct MultiBindings { + /// Abigen objects to be written + bindings: BTreeMap, +} + +// deref allows for inspection without modification +impl std::ops::Deref for MultiBindings { + type Target = BTreeMap; + + fn deref(&self) -> &Self::Target { + &self.bindings + } +} + +impl MultiBindings { + /// Generat the contents of the `Cargo.toml` file for a lib + fn generate_cargo_toml( + &self, + name: impl AsRef, + version: impl AsRef, + ) -> Result> { + 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#"ethers = {{ git = "https://github.com/gakonst/ethers-rs", default-features = false }}"# + )?; + Ok(toml) + } + + /// Write the contents of `Cargo.toml` to disk + fn write_cargo_toml( + &self, + lib: &Path, + name: impl AsRef, + version: impl AsRef, + ) -> Result<()> { + let contents = self.generate_cargo_toml(name, version)?; + + let mut file = fs::OpenOptions::new() + .read(true) + .write(true) + .create_new(true) + .open(lib.join("Cargo.toml"))?; + file.write_all(&contents)?; + + Ok(()) + } + + /// Generate the shared prefix of the `lib.rs` or `mod.rs` + fn generate_prefix( + &self, + 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(()) + } + + /// Append module declarations to the `lib.rs` or `mod.rs` + fn append_module_names(&self, mut buf: impl Write) -> Result<()> { + // sorting here not necessary, as btreemap keys are ordered + for module in self.bindings.keys().map(|name| format!("pub mod {};", name.to_snake_case())) + { + writeln!(buf, "{}", module)?; + } + + Ok(()) + } + + /// Generate the contents of `lib.rs` or `mod.rs` + fn generate_super_contents(&self, is_crate: bool, single_file: bool) -> Result> { + let mut contents = vec![]; + self.generate_prefix(&mut contents, is_crate, single_file)?; + + if !single_file { + self.append_module_names(&mut contents)?; + } else { + for binding in self.bindings.values() { + binding.write(&mut contents)?; + } + } + + Ok(contents) + } + + /// Write the `lib.rs` or `mod.rs` to disk + 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(()) + } + + /// Write all contract bindings to their respective files + fn write_bindings(&self, path: &Path) -> Result<()> { + for binding in self.bindings.values() { + binding.write_module_in_dir(path)?; + } + Ok(()) + } + + /// Generates all the bindings and writes them to the given module + /// + /// # Example + /// + /// Read all json abi files from the `./abi` directory + /// ```text + /// abi + /// ├── ERC20.json + /// ├── Contract1.json + /// ├── Contract2.json + /// ... + /// ``` + /// + /// and write them to the `./src/contracts` location as + /// + /// ```text + /// src/contracts + /// ├── mod.rs + /// ├── er20.rs + /// ├── contract1.rs + /// ├── contract2.rs + /// ... + /// ``` + /// + /// ```no_run + /// # use ethers_contract_abigen::MultiAbigen; + /// let gen = MultiAbigen::from_json_files("./abi").unwrap(); + /// let bindings = gen.build().unwrap(); + /// bindings.write_to_module("./src/contracts", false).unwrap(); + /// ``` + pub fn write_to_module(self, module: impl AsRef, 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(()) + } + + /// Generates all the bindings and writes a library crate containing them + /// to the provided path + /// + /// # Example + /// + /// Read all json abi files from the `./abi` directory + /// ```text + /// abi + /// ├── ERC20.json + /// ├── Contract1.json + /// ├── Contract2.json + /// ├── Contract3/ + /// ├── Contract3.json + /// ... + /// ``` + /// + /// and write them to the `./bindings` location as + /// + /// ```text + /// bindings + /// ├── Cargo.toml + /// ├── src/ + /// ├── lib.rs + /// ├── er20.rs + /// ├── contract1.rs + /// ├── contract2.rs + /// ... + /// ``` + /// + /// ```no_run + /// # use ethers_contract_abigen::MultiAbigen; + /// let gen = MultiAbigen::from_json_files("./abi").unwrap(); + /// let bindings = gen.build().unwrap(); + /// bindings.write_to_crate( + /// "my-crate", "0.0.5", "./bindings", false + /// ).unwrap(); + /// ``` + pub fn write_to_crate( + self, + name: impl AsRef, + version: impl AsRef, + lib: impl AsRef, + 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(()) + } + + /// Ensures the contents of the bindings directory are correct + /// + /// Does this by first generating the `lib.rs` or `mod.rs`, then the + /// contents of each binding file in turn. + fn ensure_consistent_bindings( + self, + dir: impl AsRef, + 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 it is single file, we skip checking anything but the super + // contents + if !single_file { + for binding in self.bindings.values() { + check_binding_in_dir(dir, binding)?; + } + } + + Ok(()) + } + + /// This ensures that the already generated bindings crate matches the + /// output of a fresh new run. Run this in a rust test, to get notified in + /// CI if the newly generated bindings deviate from the already generated + /// ones, and it's time to generate them again. This could happen if the + /// ABI of a contract or the output that `ethers` generates changed. + /// + /// If this functions is run within a test during CI and fails, then it's + /// time to update all bindings. + /// + /// # Returns + /// + /// `Ok(())` if the freshly generated bindings match with the + /// existing bindings. Otherwise an `Err(_)` containing an `anyhow::Report` + /// with more information. + /// + /// # Example + /// + /// Check that the generated files are up to date + /// + /// ```no_run + /// # use ethers_contract_abigen::MultiAbigen; + /// #[test] + /// fn generated_bindings_are_fresh() { + /// let project_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR")); + /// let abi_dir = project_root.join("abi"); + /// let gen = MultiAbigen::from_json_files(&abi_dir).unwrap(); + /// let bindings = gen.build().unwrap(); + /// bindings.ensure_consistent_crate( + /// "my-crate", "0.0.1", project_root.join("src/contracts"), false + /// ).expect("inconsistent bindings"); + /// } + /// ``` + pub fn ensure_consistent_crate( + self, + name: impl AsRef, + version: impl AsRef, + crate_path: impl AsRef, + single_file: bool, + ) -> Result<()> { + let crate_path = crate_path.as_ref(); + + // additionally check the contents of the cargo + let cargo_contents = self.generate_cargo_toml(name, version)?; + check_file_in_dir(crate_path, "Cargo.toml", &cargo_contents)?; + + self.ensure_consistent_bindings(crate_path.join("src"), true, single_file)?; + Ok(()) + } + + /// This ensures that the already generated bindings module matches the + /// output of a fresh new run. Run this in a rust test, to get notified in + /// CI if the newly generated bindings deviate from the already generated + /// ones, and it's time to generate them again. This could happen if the + /// ABI of a contract or the output that `ethers` generates changed. + /// + /// If this functions is run within a test during CI and fails, then it's + /// time to update all bindings. + /// + /// # Returns + /// + /// `Ok(())` if the freshly generated bindings match with the + /// existing bindings. Otherwise an `Err(_)` containing an `anyhow::Report` + /// with more information. + /// + /// # Example + /// + /// Check that the generated files are up to date + /// + /// ```no_run + /// # use ethers_contract_abigen::MultiAbigen; + /// #[test] + /// fn generated_bindings_are_fresh() { + /// let project_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR")); + /// let abi_dir = project_root.join("abi"); + /// let gen = MultiAbigen::from_json_files(&abi_dir).unwrap(); + /// let bindings = gen.build().unwrap(); + /// bindings.ensure_consistent_module( + /// project_root.join("src/contracts"), false + /// ).expect("inconsistent bindings"); + /// } + /// ``` + pub fn ensure_consistent_module( + self, + module: impl AsRef, + single_file: bool, + ) -> Result<()> { + self.ensure_consistent_bindings(module, false, single_file)?; + Ok(()) + } +} + +fn check_file_in_dir(dir: &Path, file_name: &str, expected_contents: &[u8]) -> Result<()> { + anyhow::ensure!(dir.is_dir(), "Not a directory: {}", dir.display()); + + let file_path = dir.join(file_name); + anyhow::ensure!(file_path.is_file(), "Not a file: {}", file_path.display()); + + let contents = fs::read(file_path).expect("Unable to read file"); + anyhow::ensure!(contents == expected_contents, "file contents do not match"); + 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 std::{panic, path::PathBuf}; + + struct Context { + multi_gen: MultiAbigen, + mod_root: PathBuf, + } + + fn run_test(test: T) + where + T: FnOnce(&Context) -> () + panic::UnwindSafe, + { + let crate_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR")).to_owned(); + 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_abi.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 mod_root = tempfile::tempdir().unwrap().path().join("contracts"); + let context = Context { multi_gen, mod_root }; + + let result = panic::catch_unwind(|| test(&context)); + + assert!(result.is_ok()) + } + + #[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 + .clone() + .build() + .unwrap() + .ensure_consistent_module(&mod_root, single_file) + .expect("Inconsistent bindings"); + }) + } + + #[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 + .clone() + .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 + .clone() + .build() + .unwrap() + .ensure_consistent_crate(name, version, &mod_root, single_file) + .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 + .clone() + .build() + .unwrap() + .ensure_consistent_crate(name, version, &mod_root, single_file) + .expect("Inconsistent bindings"); + }) + } + + #[test] + fn can_detect_incosistent_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(); + + let mut cloned = multi_gen.clone(); + cloned.push( + Abigen::new( + "AdditionalContract", + r#"[ + getValue() (uint256) + ]"#, + ) + .unwrap(), + ); + + let result = + cloned.build().unwrap().ensure_consistent_module(&mod_root, single_file).is_err(); + + // ensure inconsistent bindings are detected + assert!(result, "Inconsistent bindings wrongly approved"); + }) + } + + #[test] + fn can_detect_incosistent_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(); + + let mut cloned = multi_gen.clone(); + cloned.push( + Abigen::new( + "AdditionalContract", + r#"[ + getValue() (uint256) + ]"#, + ) + .unwrap(), + ); + + let result = + cloned.build().unwrap().ensure_consistent_module(&mod_root, single_file).is_err(); + + // ensure inconsistent bindings are detected + assert!(result, "Inconsistent bindings wrongly approved"); + }) + } + + #[test] + fn can_detect_incosistent_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(); + + let mut cloned = multi_gen.clone(); + cloned.push( + Abigen::new( + "AdditionalContract", + r#"[ + getValue() (uint256) + ]"#, + ) + .unwrap(), + ); + + let result = cloned + .build() + .unwrap() + .ensure_consistent_crate(name, version, &mod_root, single_file) + .is_err(); + + // ensure inconsistent bindings are detected + assert!(result, "Inconsistent bindings wrongly approved"); + }) + } + + #[test] + fn can_detect_incosistent_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(); + + let mut cloned = multi_gen.clone(); + cloned.push( + Abigen::new( + "AdditionalContract", + r#"[ + getValue() (uint256) + ]"#, + ) + .unwrap(), + ); + + let result = cloned + .build() + .unwrap() + .ensure_consistent_crate(name, version, &mod_root, single_file) + .is_err(); + + // ensure inconsistent bindings are detected + assert!(result, "Inconsistent bindings wrongly approved"); + }) + } +}