Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

impl NearSchema derive macro #891

Merged
merged 28 commits into from
Nov 8, 2022
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
15ee705
impl NearAbi derive macro
miraclx Aug 17, 2022
6138633
autoimpl BorshSchema
miraclx Aug 17, 2022
016aaf8
impl #[abi(json, borsh)] attribute macro
miraclx Aug 18, 2022
d18752f
remove abi feature gate for NearAbi derive
miraclx Aug 19, 2022
e5ad2bb
NearAbi -> NearSchema
miraclx Aug 22, 2022
b3417e7
allow single value abi impls
miraclx Aug 22, 2022
625d9cd
Merge branch 'master' into miraclx/near-abi-derive
miraclx Aug 24, 2022
f2158ad
replace bitwise flags with bools
miraclx Aug 24, 2022
851304d
Merge branch 'master' into miraclx/near-abi-derive
miraclx Aug 26, 2022
2391d9c
strip unknown attributes
miraclx Aug 26, 2022
8f19d30
feature gate ToTokens trait import
miraclx Aug 26, 2022
65eb70e
feature gate NearSchema export
miraclx Aug 26, 2022
9d23ab3
Merge branch 'master' into miraclx/near-abi-derive
miraclx Sep 2, 2022
3b9a874
Merge branch 'master' into miraclx/near-abi-derive
miraclx Nov 7, 2022
5278c49
strip all unknown attrs, regardless of expected activation
miraclx Nov 7, 2022
8a920e0
fix schema proxy
miraclx Nov 7, 2022
43f0f2c
update macro expansion format
miraclx Nov 8, 2022
f1ef003
add compilation tests
miraclx Nov 8, 2022
3cff8ca
hide NearSchema behind the unstable feature flag
miraclx Nov 8, 2022
993255c
do not import all
miraclx Nov 8, 2022
896b311
report all errors at once, instead of incrementally
miraclx Nov 8, 2022
dbb9c8b
add tests for schema errors
miraclx Nov 8, 2022
ac8f7dc
fix clippy
miraclx Nov 8, 2022
05f872c
adder builds with abi feature
miraclx Nov 8, 2022
8c2d230
localize format_ident import
miraclx Nov 8, 2022
8ad70c9
union errors take higher precedence than attr errors
miraclx Nov 8, 2022
2c54d70
Merge branch 'master' into miraclx/near-abi-derive
austinabell Nov 8, 2022
fa32145
add compilation test for edge case that should've failed
miraclx Nov 8, 2022
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
11 changes: 6 additions & 5 deletions examples/abi/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use near_sdk::__private::schemars::JsonSchema;
use near_sdk::borsh::{self, BorshDeserialize, BorshSchema, BorshSerialize};
use near_sdk::near_bindgen;
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::serde::{Deserialize, Serialize};
use near_sdk::{near_bindgen, NearSchema};

#[derive(JsonSchema, Serialize, Deserialize, BorshDeserialize, BorshSerialize, BorshSchema)]
#[derive(NearSchema, Serialize, Deserialize, BorshDeserialize, BorshSerialize)]
#[abi(json, borsh)]
pub struct Pair(u32, u32);

#[derive(JsonSchema, Serialize, Deserialize)]
#[derive(NearSchema, Serialize, Deserialize)]
#[abi(json, borsh)]
pub struct DoublePair {
first: Pair,
second: Pair,
Expand Down
2 changes: 1 addition & 1 deletion near-sdk-macros/src/core_impl/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ fn generate_abi_type(ty: &Type, serializer_type: &SerializerType) -> TokenStream
},
SerializerType::Borsh => quote! {
near_sdk::__private::AbiType::Borsh {
type_schema: <#ty>::schema_container(),
type_schema: <#ty as near_sdk::borsh::BorshSchema>::schema_container(),
}
},
}
Expand Down
170 changes: 170 additions & 0 deletions near-sdk-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use proc_macro::TokenStream;
use self::core_impl::*;
use proc_macro2::Span;
use quote::quote;
#[cfg(feature = "abi")]
use quote::ToTokens;
use syn::visit::Visit;
use syn::{File, ItemEnum, ItemImpl, ItemStruct, ItemTrait};

Expand Down Expand Up @@ -207,6 +209,174 @@ pub fn metadata(item: TokenStream) -> TokenStream {
}
}

#[cfg(feature = "abi")]
#[proc_macro_derive(NearSchema, attributes(abi, serde, borsh_skip, schemars, validate))]
pub fn derive_near_schema(input: TokenStream) -> TokenStream {
let mut input = syn::parse_macro_input!(input as syn::DeriveInput);

let (mut json_schema, mut borsh_schema) = (false, false);
let mut type_attrs = vec![];
for attr in input.attrs {
match attr.parse_meta() {
// #[abi]
Ok(syn::Meta::Path(meta)) if meta.is_ident("abi") => {
return TokenStream::from(
syn::Error::new_spanned(
meta.into_token_stream(),
"attribute requires at least one argument",
)
.to_compile_error(),
);
}
// #[abi(json, borsh)]
Ok(syn::Meta::List(meta)) if meta.path.is_ident("abi") => {
// #[abi()]
if meta.nested.is_empty() {
return TokenStream::from(
syn::Error::new_spanned(
meta.into_token_stream(),
"attribute requires at least one argument",
)
.to_compile_error(),
);
}
json_schema = false;
miraclx marked this conversation as resolved.
Show resolved Hide resolved
for meta in meta.nested {
match meta {
syn::NestedMeta::Meta(m) if m.path().is_ident("json") => json_schema = true,
syn::NestedMeta::Meta(m) if m.path().is_ident("borsh") => {
borsh_schema = true
}
_ => {
return TokenStream::from(
syn::Error::new_spanned(
meta.into_token_stream(),
format!("invalid argument, expected: `json` or `borsh`",),
)
.to_compile_error(),
);
}
}
}
}
// #[serde(..)], #[schemars(..)]
Ok(syn::Meta::List(meta))
if meta.path.is_ident("serde") || meta.path.is_ident("schemars") =>
{
type_attrs.push(attr)
}
_ => continue,
}
}
input.attrs = type_attrs;

let strip_unknown_attr = |attrs: &mut Vec<syn::Attribute>| {
attrs.retain(|attr| {
[
(json_schema, &["serde", "schemars", "validate"][..]),
(borsh_schema, &["borsh_skip"][..]),
]
.iter()
.any(|&(case, paths)| case && paths.iter().any(|path| attr.path.is_ident(path)))
});
};

match &mut input.data {
syn::Data::Struct(data) => {
for field in &mut data.fields {
strip_unknown_attr(&mut field.attrs);
}
}
syn::Data::Enum(data) => {
for variant in &mut data.variants {
strip_unknown_attr(&mut variant.attrs);
for field in &mut variant.fields {
strip_unknown_attr(&mut field.attrs);
}
}
}
syn::Data::Union(_) => {
return TokenStream::from(
syn::Error::new(Span::call_site(), "Near schema does not support unions yet.")
.to_compile_error(),
);
}
}

let derive = match (json_schema, borsh_schema) {
// <unspecified> or #[abi(json)]
(_, false) => quote! {
#[derive(schemars::JsonSchema)]
},
// #[abi(borsh)]
(false, true) => quote! {
#[derive(borsh::BorshSchema)]
},
// #[abi(json, borsh)]
(true, true) => quote! {
#[derive(schemars::JsonSchema, borsh::BorshSchema)]
},
};

let input_ident = &input.ident;

let json_impl = if json_schema {
quote! {
#[automatically_derived]
impl schemars::JsonSchema for super::#input_ident {
fn schema_name() -> ::std::string::String {
stringify!(#input_ident).to_string()
}

fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
<#input_ident as schemars::JsonSchema>::json_schema(gen)
}
}
}
} else {
quote! {}
};

let borsh_impl = if borsh_schema {
quote! {
#[automatically_derived]
impl borsh::BorshSchema for super::#input_ident {
fn declaration() -> ::std::string::String {
stringify!(#input_ident).to_string()
}

fn add_definitions_recursively(
definitions: &mut borsh::maybestd::collections::HashMap<
borsh::schema::Declaration,
borsh::schema::Definition,
>,
) {
<#input_ident as borsh::BorshSchema>::add_definitions_recursively(definitions);
}
}
}
} else {
quote! {}
};

TokenStream::from(quote! {
#[cfg(not(target_arch = "wasm32"))]
const _: () = {
mod __near_abi_private {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does putting this in a mod do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NearSchema's expansion effectively adds derives to a clone of the structure.

#[derive(NearSchema)]
struct Value {
    field: InnerValue
}

expands into

struct Value {
    field: InnerValue
}

const _: () = {
    mod __near_abi_private {
        #[derive(schemars::JsonSchema)]
        struct Value {
            field: InnerValue
        }
        
        impl schemars::JsonSchema for super::Value { ... }
        //                            ^^^^^
        // disambiguates the cloned structure from the original one
    }
};

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, I see, so you want to avoid the type name being something different from the original for the purposes of schemars, which uses the type name or something?

Copy link
Contributor Author

@miraclx miraclx Aug 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, borsh also uses the type name. Honestly, it's mostly because of Borsh. Depending on the structure, borsh uses the name quite extensively.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bump on this. super::* requires the type to exist in a module. And would fail in this case.

mod x {
    fn a() {
        #[derive(NearSchema)]
        pub struct MyType;
    }
}

super::MyType from the injected __near_abi_private module would reference x::MyType, which is nonexistent.

Copy link
Contributor Author

@miraclx miraclx Nov 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved to expanding to this instead:

struct Value {
    field: InnerValue
}

const _: () = {
    type Value__NEAR_SCHEMA_PROXY = Value;
    {
        #[derive(schemars::JsonSchema)]
        struct Value {
            field: InnerValue
        }
        
        impl schemars::JsonSchema for Value__NEAR_SCHEMA_PROXY { ... }
    }
};

use super::*;
use near_sdk::borsh;
use near_sdk::__private::schemars;

#derive
#input

#json_impl
#borsh_impl
}
};
})
}

/// `PanicOnDefault` generates implementation for `Default` trait that panics with the following
/// message `The contract is not initialized` when `default()` is called.
/// This is a helpful macro in case the contract is required to be initialized with either `init` or
Expand Down
2 changes: 2 additions & 0 deletions near-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#[cfg(test)]
extern crate quickcheck;

#[cfg(feature = "abi")]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we put this export under unstable as well for now? Doesn't seem clear this will be more intuitive than deriving the traits directly yet and the internal semantics aren't tested yet.

pub use near_sdk_macros::NearSchema;
pub use near_sdk_macros::{
ext_contract, metadata, near_bindgen, BorshStorageKey, FunctionError, PanicOnDefault,
};
Expand Down