Skip to content
This repository has been archived by the owner on Sep 13, 2022. It is now read-only.

[ᚬframework] feat(core/binding-macro): Add read and write proc-macro. #49

Merged
merged 1 commit into from
Dec 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -48,6 +48,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")
}
}