diff --git a/CHANGELOG.md b/CHANGELOG.md index 383fe5ef..626ae469 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,7 @@ ### Unreleased +- Add `ContractFilter` to filter contracts in `MultiAbigen` [#1564](https://github.com/gakonst/ethers-rs/pull/1564) - generate error bindings for custom errors [#1549](https://github.com/gakonst/ethers-rs/pull/1549) - Support overloaded events [#1233](https://github.com/gakonst/ethers-rs/pull/1233) diff --git a/Cargo.lock b/Cargo.lock index cee9858d..49cec0ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1259,6 +1259,7 @@ dependencies = [ "hex", "proc-macro2", "quote", + "regex", "reqwest", "serde", "serde_json", diff --git a/ethers-contract/ethers-contract-abigen/Cargo.toml b/ethers-contract/ethers-contract-abigen/Cargo.toml index 88eca922..f4d150db 100644 --- a/ethers-contract/ethers-contract-abigen/Cargo.toml +++ b/ethers-contract/ethers-contract-abigen/Cargo.toml @@ -25,6 +25,7 @@ cfg-if = "1.0.0" dunce = "1.0.2" walkdir = "2.3.2" eyre = "0.6" +regex = "1.6.0" [target.'cfg(target_arch = "wasm32")'.dependencies] # NOTE: this enables wasm compatibility for getrandom indirectly diff --git a/ethers-contract/ethers-contract-abigen/src/filter.rs b/ethers-contract/ethers-contract-abigen/src/filter.rs new file mode 100644 index 00000000..4323ab69 --- /dev/null +++ b/ethers-contract/ethers-contract-abigen/src/filter.rs @@ -0,0 +1,174 @@ +//! Filtering support for contracts used in [`Abigen`] + +use regex::bytes::Regex; +use std::collections::HashSet; + +/// Used to filter contracts that should be _included_ in the abigen generation. +#[derive(Debug, Clone)] +pub enum ContractFilter { + /// Include all contracts + All, + /// Only include contracts that match the filter + Select(SelectContracts), + /// Only include contracts that _don't_ match the filter + Exclude(ExcludeContracts), +} + +// === impl ContractFilter === + +impl ContractFilter { + /// Returns whether to include the contract with the given `name` + pub fn is_match(&self, name: impl AsRef) -> bool { + match self { + ContractFilter::All => true, + ContractFilter::Select(f) => f.is_match(name), + ContractFilter::Exclude(f) => !f.is_match(name), + } + } +} + +impl Default for ContractFilter { + fn default() -> Self { + ContractFilter::All + } +} + +impl From for ContractFilter { + fn from(f: SelectContracts) -> Self { + ContractFilter::Select(f) + } +} + +impl From for ContractFilter { + fn from(f: ExcludeContracts) -> Self { + ContractFilter::Exclude(f) + } +} + +macro_rules! impl_filter { + ($name:ident) => { + impl $name { + /// Adds an exact name to the filter + pub fn add_name>(mut self, arg: T) -> Self { + self.exact.insert(arg.into()); + self + } + + /// Adds multiple exact names to the filter + pub fn extend_names(mut self, name: I) -> Self + where + I: IntoIterator, + S: Into, + { + for arg in name { + self = self.add_name(arg); + } + self + } + + /// Adds the regex to use + /// + /// # Panics + /// + /// If `pattern` is an invalid `Regex` + pub fn add_regex(mut self, re: Regex) -> Self { + self.patterns.push(re); + self + } + + /// Adds multiple exact names to the filter + pub fn extend_regex(mut self, regexes: I) -> Self + where + I: IntoIterator, + S: Into, + { + for re in regexes { + self = self.add_regex(re.into()); + } + self + } + + /// Sets the pattern to use + /// + /// # Panics + /// + /// If `pattern` is an invalid `Regex` + pub fn add_pattern(self, pattern: impl AsRef) -> Self { + self.try_add_pattern(pattern).unwrap() + } + + /// Sets the pattern to use + pub fn try_add_pattern(mut self, s: impl AsRef) -> Result { + self.patterns.push(Regex::new(s.as_ref())?); + Ok(self) + } + + /// Adds multiple patterns to the filter + /// + /// # Panics + /// + /// If `pattern` is an invalid `Regex` + pub fn extend_pattern(self, patterns: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + self.try_extend_pattern(patterns).unwrap() + } + + /// Adds multiple patterns to the filter + /// + /// # Panics + /// + /// If `pattern` is an invalid `Regex` + pub fn try_extend_pattern(mut self, patterns: I) -> Result + where + I: IntoIterator, + S: AsRef, + { + for p in patterns { + self = self.try_add_pattern(p)?; + } + Ok(self) + } + + /// Returns true whether the `name` matches the filter + pub fn is_match(&self, name: impl AsRef) -> bool { + let name = name.as_ref(); + if self.exact.contains(name) { + return true + } + self.patterns.iter().any(|re| re.is_match(name.as_bytes())) + } + } + }; +} + +/// A Contract Filter that only includes certain contracts. +/// +/// **Note:**: matching by exact name and via regex stacks +/// +/// This is the inverse of `ExcludeContracts` +#[derive(Debug, Clone, Default)] +pub struct SelectContracts { + /// Include contracts based on their exact name + exact: HashSet, + /// Include contracts if their name matches a pattern + patterns: Vec, +} + +/// A Contract Filter that exclude certain contracts +/// +/// **Note:**: matching by exact name and via regex stacks +/// +/// This is the inverse of `SelectContracts` +#[derive(Debug, Clone, Default)] +pub struct ExcludeContracts { + /// Exclude contracts based on their exact name + exact: HashSet, + /// Exclude contracts if their name matches any pattern + patterns: Vec, +} + +impl_filter!(SelectContracts); +impl_filter!(ExcludeContracts); diff --git a/ethers-contract/ethers-contract-abigen/src/lib.rs b/ethers-contract/ethers-contract-abigen/src/lib.rs index b9482b52..43d2d537 100644 --- a/ethers-contract/ethers-contract-abigen/src/lib.rs +++ b/ethers-contract/ethers-contract-abigen/src/lib.rs @@ -20,6 +20,8 @@ mod rustfmt; mod source; mod util; +pub mod filter; +pub use filter::{ContractFilter, ExcludeContracts, SelectContracts}; pub mod multi; pub use multi::MultiAbigen; diff --git a/ethers-contract/ethers-contract-abigen/src/multi.rs b/ethers-contract/ethers-contract-abigen/src/multi.rs index 37c8ab76..32396b9c 100644 --- a/ethers-contract/ethers-contract-abigen/src/multi.rs +++ b/ethers-contract/ethers-contract-abigen/src/multi.rs @@ -1,5 +1,4 @@ -//! TODO - +//! Generate bindings for multiple `Abigen` use eyre::Result; use inflector::Inflector; use proc_macro2::TokenStream; @@ -11,7 +10,131 @@ use std::{ path::Path, }; -use crate::{util, Abigen, Context, ContractBindings, ExpandedContract}; +use crate::{util, Abigen, Context, ContractBindings, ContractFilter, ExpandedContract}; + +/// 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 + /// ... + /// ``` + /// + /// ``` + /// # fn t() { + /// # 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() + } + + /// See `apply_filter` + /// + /// # Example + /// + /// Only Select specific contracts + /// + /// ``` + /// use ethers_contract_abigen::{MultiAbigen, SelectContracts}; + /// # fn t() { + /// let gen = MultiAbigen::from_json_files("./abi").unwrap().with_filter( + /// SelectContracts::default().add_name("MyContract").add_name("MyOtherContract"), + /// ); + /// ``` + /// + /// Exclude all contracts that end with test + /// + /// ``` + /// use ethers_contract_abigen::{ExcludeContracts, MultiAbigen}; + /// # fn t() { + /// let gen = MultiAbigen::from_json_files("./abi").unwrap().with_filter( + /// ExcludeContracts::default().add_pattern(".*Test"), + /// ); + /// ``` + #[must_use] + pub fn with_filter(mut self, filter: impl Into) -> Self { + self.apply_filter(&filter.into()); + self + } + + /// Removes all `Abigen` items that should not be included based on the given filter + pub fn apply_filter(&mut self, filter: &ContractFilter) { + self.abigens.retain(|abi| filter.is_match(&abi.contract_name)) + } + + /// 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 rustfmt = self.abigens.iter().any(|gen| gen.rustfmt); + Ok(MultiBindings { + expansion: MultiExpansion::from_abigen(self.abigens)?.expand(), + rustfmt, + }) + } +} /// Represents a collection of [`Abigen::expand()`] pub struct MultiExpansion { @@ -189,94 +312,6 @@ impl MultiExpansionResult { } } -/// 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 rustfmt = self.abigens.iter().any(|gen| gen.rustfmt); - Ok(MultiBindings { - expansion: MultiExpansion::from_abigen(self.abigens)?.expand(), - rustfmt, - }) - } -} - /// Output of the [`MultiAbigen`] build process. `MultiBindings` wraps a group /// of built contract bindings that have yet to be written to disk. /// @@ -736,6 +771,7 @@ fn check_binding_in_dir(dir: &Path, binding: &ContractBindings) -> Result<()> { mod tests { use super::*; + use crate::{ExcludeContracts, SelectContracts}; use ethers_solc::project_util::TempProject; use std::{panic, path::PathBuf}; @@ -1015,6 +1051,55 @@ mod tests { 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 tmp = TempProject::dapptools().unwrap();