Skip to content

Commit

Permalink
Decouple code generation from Truffle artifact (#555)
Browse files Browse the repository at this point in the history
Fix #196, part of #512, merge after #554.

Refactor builder interface to separate code that loads artifacts, parses them and generates bindings into independent modules. Now code generation takes a reference to `Contract`, loading contracts from source is no longer builder's job.

Additionally, `Builder` was renamed to `ContractBuilder` in anticipation that we will have `ArtifactBuilder` in the future. Also `ethcontract_generate::contract` was renamed to `ethcontract_generate::generate`. I can move these changes into separate pullrequests if anyone things it's necessary.

Procedural macro will be updated later in #512.
  • Loading branch information
Tamika Nomara committed Jul 7, 2021
1 parent 9384d5e commit d79f0a9
Show file tree
Hide file tree
Showing 16 changed files with 410 additions and 416 deletions.
7 changes: 4 additions & 3 deletions ethcontract-common/src/artifact/hardhat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,11 @@ impl HardHatLoader {
}

/// Loads an artifact from disk.
pub fn load_from_file(&self, p: &Path) -> Result<Artifact, ArtifactError> {
let file = File::open(p)?;
pub fn load_from_file(&self, p: impl AsRef<Path>) -> Result<Artifact, ArtifactError> {
let path = p.as_ref();
let file = File::open(path)?;
let reader = BufReader::new(file);
self.load_artifact(p.display(), reader, from_reader, from_reader)
self.load_artifact(path.display(), reader, from_reader, from_reader)
}

fn load_artifact<T>(
Expand Down
12 changes: 7 additions & 5 deletions ethcontract-common/src/artifact/truffle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,11 @@ impl TruffleLoader {
}

/// Loads an artifact from disk.
pub fn load_from_file(&self, p: &Path) -> Result<Artifact, ArtifactError> {
let file = File::open(p)?;
pub fn load_from_file(&self, p: impl AsRef<Path>) -> Result<Artifact, ArtifactError> {
let path = p.as_ref();
let file = File::open(path)?;
let reader = BufReader::new(file);
self.load_artifact(p.display(), reader, from_reader)
self.load_artifact(path.display(), reader, from_reader)
}

/// Loads a contract from a loaded JSON value.
Expand All @@ -112,8 +113,9 @@ impl TruffleLoader {
}

/// Loads a contract from disk.
pub fn load_contract_from_file(&self, p: &Path) -> Result<Contract, ArtifactError> {
let file = File::open(p)?;
pub fn load_contract_from_file(&self, p: impl AsRef<Path>) -> Result<Contract, ArtifactError> {
let path = p.as_ref();
let file = File::open(path)?;
let reader = BufReader::new(file);
self.load_contract(reader, from_reader)
}
Expand Down
7 changes: 6 additions & 1 deletion ethcontract-common/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,13 @@ pub struct Contract {
impl Contract {
/// Creates an empty contract instance.
pub fn empty() -> Self {
Contract::with_name(String::default())
}

/// Creates an empty contract instance with the given name.
pub fn with_name(name: impl Into<String>) -> Self {
Contract {
name: String::new(),
name: name.into(),
abi: Abi {
constructor: None,
functions: HashMap::new(),
Expand Down
1 change: 1 addition & 0 deletions ethcontract-derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Proc macro for generating type-safe bindings to Ethereum smart contracts.
proc-macro = true

[dependencies]
anyhow = "1.0"
ethcontract-common = { version = "0.12.2", path = "../ethcontract-common" }
ethcontract-generate = { version = "0.12.2", path = "../ethcontract-generate" }
proc-macro2 = "1.0"
Expand Down
66 changes: 43 additions & 23 deletions ethcontract-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ extern crate proc_macro;
mod spanned;

use crate::spanned::{ParseInner, Spanned};
use anyhow::Result;
use ethcontract_common::abi::{Function, Param, ParamType};
use ethcontract_common::abiext::{FunctionExt, ParamTypeExt};
use ethcontract_generate::{parse_address, Address, Builder};
use ethcontract_common::artifact::truffle::TruffleLoader;
use ethcontract_common::contract::Network;
use ethcontract_common::{Address, Contract};
use ethcontract_generate::{parse_address, ContractBuilder, Source};
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens as _};
use std::collections::HashSet;
use std::error::Error;
use syn::ext::IdentExt;
use syn::parse::{Error as ParseError, Parse, ParseStream, Result as ParseResult};
use syn::{
Expand Down Expand Up @@ -102,15 +105,21 @@ use syn::{
#[proc_macro]
pub fn contract(input: TokenStream) -> TokenStream {
let args = parse_macro_input!(input as Spanned<ContractArgs>);

let span = args.span();
expand(args.into_inner())
Source::parse(&args.artifact_path)
.and_then(|s| s.artifact_json())
.and_then(|j| {
TruffleLoader::new()
.load_contract_from_str(&j)
.map_err(Into::into)
})
.and_then(|c| expand(args.into_inner(), &c))
.unwrap_or_else(|e| SynError::new(span, format!("{:?}", e)).to_compile_error())
.into()
}

fn expand(args: ContractArgs) -> Result<TokenStream2, Box<dyn Error>> {
Ok(args.into_builder()?.generate()?.into_tokens())
fn expand(args: ContractArgs, contract: &Contract) -> Result<TokenStream2> {
Ok(args.into_builder().generate(contract)?.into_tokens())
}

/// Contract procedural macro arguments.
Expand All @@ -122,30 +131,41 @@ struct ContractArgs {
}

impl ContractArgs {
fn into_builder(self) -> Result<Builder, Box<dyn Error>> {
let mut builder = Builder::from_source_url(&self.artifact_path)?
.with_visibility_modifier(self.visibility);
fn into_builder(self) -> ContractBuilder {
let mut builder = ContractBuilder::new();

builder.visibility_modifier = self.visibility;

for parameter in self.parameters.into_iter() {
builder = match parameter {
Parameter::Mod(name) => builder.with_contract_mod_override(Some(name)),
Parameter::Contract(name) => builder.with_contract_name_override(Some(name)),
Parameter::Crate(name) => builder.with_runtime_crate_name(name),
match parameter {
Parameter::Mod(name) => builder.contract_mod_override = Some(name),
Parameter::Contract(name) => builder.contract_name_override = Some(name),
Parameter::Crate(name) => builder.runtime_crate_name = name,
Parameter::Deployments(deployments) => {
deployments.into_iter().fold(builder, |builder, d| {
builder.add_deployment(d.network_id, d.address, None)
})
for deployment in deployments {
builder.networks.insert(
deployment.network_id.to_string(),
Network {
address: deployment.address,
deployment_information: None,
},
);
}
}
Parameter::Methods(methods) => {
for method in methods {
builder
.method_aliases
.insert(method.signature, method.alias);
}
}
Parameter::EventDerives(derives) => {
builder.event_derives.extend(derives);
}
Parameter::Methods(methods) => methods.into_iter().fold(builder, |builder, m| {
builder.add_method_alias(m.signature, m.alias)
}),
Parameter::EventDerives(derives) => derives
.into_iter()
.fold(builder, |builder, derive| builder.add_event_derive(derive)),
};
}

Ok(builder)
builder
}
}

Expand Down
30 changes: 20 additions & 10 deletions ethcontract-generate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
An alternative API for generating type-safe contract bindings from `build.rs`
scripts. Using this method instead of the procedural macro has a couple
advantages:
- Proper integration with with RLS and Racer for autocomplete support
- Ability to inspect the generated code

- proper integration with with RLS and Racer for autocomplete support;
- ability to inspect the generated code.

The downside of using the generator API is the requirement of having a build
script instead of a macro invocation.
Expand All @@ -27,19 +28,28 @@ behaviour may occur.

Then, in your `build.rs` include the following code:

```rs
use ethcontract_generate::Builder;
use std::env;
use std::path::Path;
```rust
use ethcontract_generate::loaders::TruffleLoader;
use ethcontract_generate::ContractBuilder;

fn main() {
let dest = env::var("OUT_DIR").unwrap();
Builder::new("path/to/truffle/build/contract/Contract.json")
.generate()
// Prepare filesystem paths.
let out_dir = std::env::var("OUT_DIR").unwrap();
let dest = std::path::Path::new(&out_dir).join("rust_coin.rs");

// Load a contract.
let contract = TruffleLoader::new()
.load_contract_from_file("../build/Contract.json")
.unwrap();

// Generate bindings for it.
ContractBuilder::new()
.generate(&contract)
.unwrap()
.write_to_file(Path::new(&dest).join("rust_coin.rs"))
.write_to_file(dest)
.unwrap();
}

```

## Relation to `ethcontract-derive`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#![deny(missing_docs)]

//! Crate for generating type-safe bindings to Ethereum smart contracts. This
//! crate is intended to be used either indirectly with the `ethcontract`
//! crate's `contract` procedural macro or directly from a build script.
Expand All @@ -10,83 +8,69 @@ mod events;
mod methods;
mod types;

use crate::util;
use crate::Args;
use crate::{util, ContractBuilder};
use anyhow::{anyhow, Context as _, Result};
use ethcontract_common::{Address, Contract, DeploymentInformation};
use ethcontract_common::contract::Network;
use ethcontract_common::Contract;
use inflector::Inflector;
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use std::collections::HashMap;
use syn::{Path, Visibility};

pub(crate) struct Deployment {
pub address: Address,
pub deployment_information: Option<DeploymentInformation>,
}

/// Internal shared context for generating smart contract bindings.
pub(crate) struct Context {
pub(crate) struct Context<'a> {
/// The parsed contract.
contract: Contract,
contract: &'a Contract,

/// The identifier for the runtime crate. Usually this is `ethcontract` but
/// it can be different if the crate was renamed in the Cargo manifest for
/// example.
runtime_crate: Ident,

/// The visibility for the generated module and re-exported contract type.
visibility: Visibility,

/// The name of the module as an identifier in which to place the contract
/// implementation. Note that the main contract type gets re-exported in the
/// root.
contract_mod: Ident,

/// The contract name as an identifier.
contract_name: Ident,

/// Additional contract deployments.
deployments: HashMap<u32, Deployment>,
networks: HashMap<String, Network>,

/// Manually specified method aliases.
method_aliases: HashMap<String, Ident>,

/// Derives added to event structs and enums.
event_derives: Vec<Path>,
}

impl Context {
impl<'a> Context<'a> {
/// Create a context from the code generation arguments.
fn from_args(args: Args) -> Result<Self> {
let contract = {
let artifact_json = args
.artifact_source
.artifact_json()
.context("failed to get artifact JSON")?;

Contract::from_json(&artifact_json)
.with_context(|| format!("invalid artifact JSON '{}'", artifact_json))
.with_context(|| {
format!(
"failed to parse artifact from source {:?}",
args.artifact_source,
)
})?
};

let raw_contract_name = if let Some(name) = args.contract_name_override.as_ref() {
fn from_builder(contract: &'a Contract, builder: ContractBuilder) -> Result<Self> {
let raw_contract_name = if let Some(name) = &builder.contract_name_override {
name
} else if !contract.name.is_empty() {
&contract.name
} else {
return Err(anyhow!(
"contract artifact is missing a name, this can happen when \
using a source that does not provide a contract name such as \
using a source that does not provide a contract name such as \
Etherscan; in this case the contract must be manually \
specified"
));
};

let runtime_crate = util::ident(&args.runtime_crate_name);
let visibility = match args.visibility_modifier.as_ref() {
let runtime_crate = util::ident(&builder.runtime_crate_name);
let visibility = match &builder.visibility_modifier {
Some(vis) => syn::parse_str(vis)?,
None => Visibility::Inherited,
};
let contract_mod = if let Some(name) = args.contract_mod_override.as_ref() {
let contract_mod = if let Some(name) = &builder.contract_mod_override {
util::ident(name)
} else {
util::ident(&raw_contract_name.to_snake_case())
Expand All @@ -97,7 +81,7 @@ impl Context {
// duplicate aliases, the compiler will produce a warning because a
// method will be re-defined.
let mut method_aliases = HashMap::new();
for (signature, alias) in args.method_aliases.into_iter() {
for (signature, alias) in builder.method_aliases.into_iter() {
let alias = syn::parse_str(&alias)?;
if method_aliases.insert(signature.clone(), alias).is_some() {
return Err(anyhow!(
Expand All @@ -107,7 +91,7 @@ impl Context {
}
}

let event_derives = args
let event_derives = builder
.event_derives
.iter()
.map(|derive| syn::parse_str::<Path>(derive))
Expand All @@ -120,31 +104,15 @@ impl Context {
visibility,
contract_mod,
contract_name,
deployments: args.deployments,
networks: builder.networks,
method_aliases,
event_derives,
})
}
}

#[cfg(test)]
impl Default for Context {
fn default() -> Self {
Context {
contract: Contract::empty(),
runtime_crate: util::ident("ethcontract"),
visibility: Visibility::Inherited,
contract_mod: util::ident("contract"),
contract_name: util::ident("Contract"),
deployments: HashMap::new(),
method_aliases: HashMap::new(),
event_derives: Vec::new(),
}
}
}

pub(crate) fn expand(args: Args) -> Result<TokenStream> {
let cx = Context::from_args(args)?;
pub(crate) fn expand(contract: &Contract, builder: ContractBuilder) -> Result<TokenStream> {
let cx = Context::from_builder(contract, builder)?;
let contract = expand_contract(&cx).context("error expanding contract from its ABI")?;

Ok(contract)
Expand Down
Loading

0 comments on commit d79f0a9

Please sign in to comment.