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:
parent
3b67e0c560
commit
e6c1927a1c
|
@ -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)
|
||||
|
|
|
@ -1259,6 +1259,7 @@ dependencies = [
|
|||
"hex",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue