feat(abigen): add contract filter (#1564)

* feat(abigen): add contract filter

* refactor: move to top of file

* add tests

* update changelog
This commit is contained in:
Matthias Seitz 2022-08-04 17:22:00 +02:00 committed by GitHub
parent 3b67e0c560
commit e6c1927a1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 355 additions and 91 deletions

View File

@ -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)

1
Cargo.lock generated
View File

@ -1259,6 +1259,7 @@ dependencies = [
"hex",
"proc-macro2",
"quote",
"regex",
"reqwest",
"serde",
"serde_json",

View File

@ -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

View File

@ -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<str>) -> 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<SelectContracts> for ContractFilter {
fn from(f: SelectContracts) -> Self {
ContractFilter::Select(f)
}
}
impl From<ExcludeContracts> 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<T: Into<String>>(mut self, arg: T) -> Self {
self.exact.insert(arg.into());
self
}
/// Adds multiple exact names to the filter
pub fn extend_names<I, S>(mut self, name: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
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<I, S>(mut self, regexes: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<Regex>,
{
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<str>) -> Self {
self.try_add_pattern(pattern).unwrap()
}
/// Sets the pattern to use
pub fn try_add_pattern(mut self, s: impl AsRef<str>) -> Result<Self, regex::Error> {
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<I, S>(self, patterns: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.try_extend_pattern(patterns).unwrap()
}
/// Adds multiple patterns to the filter
///
/// # Panics
///
/// If `pattern` is an invalid `Regex`
pub fn try_extend_pattern<I, S>(mut self, patterns: I) -> Result<Self, regex::Error>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
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<str>) -> 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<String>,
/// Include contracts if their name matches a pattern
patterns: Vec<Regex>,
}
/// 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<String>,
/// Exclude contracts if their name matches any pattern
patterns: Vec<Regex>,
}
impl_filter!(SelectContracts);
impl_filter!(ExcludeContracts);

View File

@ -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;

View File

@ -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<Abigen>,
}
impl std::ops::Deref for MultiAbigen {
type Target = Vec<Abigen>;
fn deref(&self) -> &Self::Target {
&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 {
/// Create a new instance from a series (`contract name`, `abi_source`)
///
/// See `Abigen::new`
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))
}
/// Create a new instance from a series of already resolved `Abigen`
pub fn from_abigens(abis: impl IntoIterator<Item = Abigen>) -> 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<Path>) -> Result<Self> {
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<ContractFilter>) -> 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<MultiBindings> {
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<Abigen>,
}
impl std::ops::Deref for MultiAbigen {
type Target = Vec<Abigen>;
fn deref(&self) -> &Self::Target {
&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 {
/// Create a new instance from a series (`contract name`, `abi_source`)
///
/// See `Abigen::new`
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))
}
/// Create a new instance from a series of already resolved `Abigen`
pub fn from_abigens(abis: impl IntoIterator<Item = Abigen>) -> 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<Path>) -> Result<Self> {
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<MultiBindings> {
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();