Skip to content

Commit

Permalink
feat(core/binding-macro): Add read and write proc-macro. (nervosn…
Browse files Browse the repository at this point in the history
  • Loading branch information
yejiayu committed Dec 26, 2019
1 parent c06fa64 commit 2246a59
Show file tree
Hide file tree
Showing 6 changed files with 342 additions and 6 deletions.
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ members = [
"core/mempool",
"core/network",
"core/storage",
"core/binding",
"core/binding-macro",

"protocol",
]
11 changes: 11 additions & 0 deletions core/binding-macro/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,15 @@ edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
proc-macro = true

[dependencies]
syn = { version = "1.0", features = ["full"] }
proc-macro2 = "1.0"
quote = "1.0"

[dev-dependencies]
protocol = { path = "../../protocol" }
json = "0.12"
bytes = "0.4"
75 changes: 69 additions & 6 deletions core/binding-macro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,70 @@
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
extern crate proc_macro;

mod read_write;

use proc_macro::TokenStream;

use crate::read_write::verify_read_or_write;

// `#[read]` marks a service method as readable.
//
// Methods marked with this macro will have:
// Methods with this macro allow access (readable) from outside (RPC or other
// services).
//
// - Verification
// 1. Is it a struct method marked with #[service]?
// 2. Is visibility private?
// 3. Does function generics constrain `fn f<Context: RequestContext>`?
// 4. Parameter signature contains `&self and ctx:Context`?
// 5. Is the return value `ProtocolResult <JsonValue>`?
//
// example:
//
// struct Service;
// #[service]
// impl Service {
// #[read]
// fn test_read_fn<Context: RequestContext>(
// &self,
// _ctx: Context,
// ) -> ProtocolResult<JsonValue> {
// Ok(JsonValue::Null)
// }
// }
#[proc_macro_attribute]
pub fn read(_: TokenStream, item: TokenStream) -> TokenStream {
verify_read_or_write(item, false)
}

// `#[write]` marks a service method as writeable.
//
// Methods marked with this macro will have:
// - Accessibility
// Methods with this macro allow access (writeable) from outside (RPC or other
// services).
//
// - Verification
// 1. Is it a struct method marked with #[service]?
// 2. Is visibility private?
// 3. Does function generics constrain `fn f<Context: RequestContext>`?
// 4. Parameter signature contains `&mut self and ctx:Context`?
// 5. Is the return value `ProtocolResult <JsonValue>`?
//
// example:
//
// struct Service;
// #[service]
// impl Service {
// #[write]
// fn test_write_fn<Context: RequestContext>(
// &mut self,
// _ctx: Context,
// ) -> ProtocolResult<JsonValue> {
// Ok(JsonValue::Null)
// }
// }
#[proc_macro_attribute]
pub fn write(_: TokenStream, item: TokenStream) -> TokenStream {
verify_read_or_write(item, true)
}
173 changes: 173 additions & 0 deletions core/binding-macro/src/read_write.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::punctuated::Punctuated;
use syn::{
parse_macro_input, FnArg, GenericArgument, GenericParam, Generics, ImplItemMethod, Path,
PathArguments, ReturnType, Token, Type, TypeParamBound, Visibility,
};

pub fn verify_read_or_write(item: TokenStream, mutable: bool) -> TokenStream {
let method_item = parse_macro_input!(item as ImplItemMethod);

let visibility = &method_item.vis;
let inputs = &method_item.sig.inputs;
let generics = &method_item.sig.generics;
let ret_type = &method_item.sig.output;

// TODO(@yejiayu): verify #[service]

verify_visibiity(visibility);

verify_inputs(inputs, generics, mutable);

verify_ret_type(ret_type);

TokenStream::from(quote! {#method_item})
}

fn verify_visibiity(visibility: &Visibility) {
match visibility {
Visibility::Inherited => {}
_ => panic!("The visibility of read/write method must be private"),
};
}

fn verify_inputs(inputs: &Punctuated<FnArg, Token![,]>, generics: &Generics, mutable: bool) {
if inputs.len() < 2 {
panic!("The two required parameters are missing: `&self/&mut self` and `RequestContext`.")
}

if mutable {
if !arg_is_mutable_receiver(&inputs[0]) {
panic!("The receiver must be `&mut self`.")
}
} else {
if !arg_is_inmutable_receiver(&inputs[0]) {
panic!("The receiver must be `&self`.")
}
}
arg_is_request_context(&inputs[1], generics);
}

fn verify_ret_type(ret_type: &ReturnType) {
let real_ret_type = match ret_type {
ReturnType::Type(_, t) => t.as_ref(),
_ => panic!("The return type of read/write method must be protocol::ProtocolResult"),
};

match real_ret_type {
Type::Path(type_path) => {
let path = &type_path.path;
let args = get_protocol_result_args(&path)
.expect("The return type of read/write method must be protocol::ProtocolResult");

match args {
PathArguments::AngleBracketed(arg) => {
let generic_args = &arg.args;
let generic_type = &generic_args[0];
generic_type_is_jsonvalue(generic_type);
}
_ => panic!("The return value of read/write method must be json::JsonValue"),
};
}
_ => panic!("The return type of read/write method must be protocol::ProtocolResult"),
}
}

fn get_protocol_result_args(path: &Path) -> Option<&PathArguments> {
// ::<a>::<b>
if path.leading_colon.is_some() {
return None;
}

// ProtocolResult<T>
if path.segments.len() == 1 && path.segments[0].ident == "ProtocolResult" {
return Some(&path.segments[0].arguments);
}

return None;
}

fn path_is_jsonvalue(path: &Path) -> bool {
// ::<a>::<b>
if path.leading_colon.is_some() {
return false;
}

// JsonValue
path.segments.len() == 1 && path.segments[0].ident == "JsonValue"
}

fn path_is_request_context(path: &Path, bound_name: &str) -> bool {
// ::<a>::<b>
if path.leading_colon.is_some() {
return false;
}

// RequestContext
path.segments.len() == 1 && path.segments[0].ident == bound_name
}

fn generic_type_is_jsonvalue(generic_type: &GenericArgument) -> bool {
match generic_type {
GenericArgument::Type(t) => match t {
Type::Path(type_path) => path_is_jsonvalue(&type_path.path),
_ => false,
},
_ => false,
}
}

// expect fn foo<Context: RequestContext>
fn get_bounds_name_of_request_context(generics: &Generics) -> Option<String> {
if generics.params.len() != 1 {
return None;
}

let generics_type = &generics.params[0];

if let GenericParam::Type(t) = generics_type {
let bound_name = t.ident.to_string();

if let TypeParamBound::Trait(bound_trait) = &t.bounds[0] {
let ident = &bound_trait.path.segments[0].ident;
if ident == "RequestContext" {
return Some(bound_name);
}
}
}

None
}

// expect fn foo<Context: RequestContext>(&self, ctx: Context)
fn arg_is_request_context(fn_arg: &FnArg, generics: &Generics) -> bool {
let ty = match fn_arg {
FnArg::Typed(pat_type) => &*pat_type.ty,
_ => return false,
};

let bound_name = get_bounds_name_of_request_context(generics)
.expect("The `read/write` method must bound the trait `RequestContext`");

match ty {
Type::Path(type_path) => path_is_request_context(&type_path.path, &bound_name),
_ => false,
}
}

// expect &mut self
fn arg_is_mutable_receiver(fn_arg: &FnArg) -> bool {
match fn_arg {
FnArg::Receiver(receiver) => receiver.reference.is_some() && receiver.mutability.is_some(),
_ => false,
}
}

// expect &self
fn arg_is_inmutable_receiver(fn_arg: &FnArg) -> bool {
match fn_arg {
FnArg::Receiver(receiver) => receiver.reference.is_some() && receiver.mutability.is_none(),
_ => false,
}
}
79 changes: 79 additions & 0 deletions core/binding-macro/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#[macro_use]
extern crate core_binding_macro;

use bytes::Bytes;
use json::JsonValue;

use protocol::traits::RequestContext;
use protocol::types::{Address, Hash};
use protocol::ProtocolResult;

#[test]
fn test_read() {
struct Tests;

impl Tests {
#[read]
fn test_read_fn<Context: RequestContext>(
&self,
_ctx: Context,
) -> ProtocolResult<JsonValue> {
Ok(JsonValue::Null)
}

#[write]
fn test_write_fn<Context: RequestContext>(
&mut self,
_ctx: Context,
) -> ProtocolResult<JsonValue> {
Ok(JsonValue::Null)
}
}

let context = MockRequestContext {};

let mut t = Tests {};
assert_eq!(t.test_read_fn(context.clone()).unwrap(), JsonValue::Null);
assert_eq!(t.test_write_fn(context.clone()).unwrap(), JsonValue::Null);
}

#[derive(Clone)]
struct MockRequestContext;

impl RequestContext for MockRequestContext {
fn sub_cycles(&self, _cycels: u64) -> ProtocolResult<()> {
Ok(())
}

fn get_cycles_price(&self) -> ProtocolResult<u64> {
Ok(0)
}

fn get_cycles_limit(&self) -> ProtocolResult<u64> {
Ok(0)
}

fn get_cycles_used(&self) -> ProtocolResult<u64> {
Ok(0)
}

fn get_caller(&self) -> ProtocolResult<Address> {
Address::from_hash(Hash::digest(Bytes::from("test")))
}

fn get_current_epoch_id(&self) -> ProtocolResult<u64> {
Ok(0)
}

fn get_service_name(&self) -> ProtocolResult<&str> {
Ok("service")
}

fn get_service_method(&self) -> ProtocolResult<&str> {
Ok("method")
}

fn get_payload(&self) -> ProtocolResult<&str> {
Ok("payload")
}
}

0 comments on commit 2246a59

Please sign in to comment.