Skip to content

Commit

Permalink
Merge pull request #146 from ModProg/derive_collection
Browse files Browse the repository at this point in the history
Support deriving Collection
  • Loading branch information
ecton committed Feb 6, 2022
2 parents e434d04 + 1afac85 commit f0001ec
Show file tree
Hide file tree
Showing 22 changed files with 430 additions and 51 deletions.
1 change: 1 addition & 0 deletions crates/bonsaidb-client/Cargo.toml
Expand Up @@ -23,6 +23,7 @@ password-hashing = ["bonsaidb-core/password-hashing"]
[dependencies]
bonsaidb-core = { path = "../bonsaidb-core", version = "0.1.0", default-features = false, features = [
"networking",
"included-from-client"
] }
bonsaidb-utils = { path = "../bonsaidb-utils", version = "0.1.0" }
thiserror = "1"
Expand Down
4 changes: 4 additions & 0 deletions crates/bonsaidb-core/Cargo.toml
Expand Up @@ -21,6 +21,10 @@ instrument = ["pot/tracing"]
multiuser = ["zeroize"]
encryption = []
password-hashing = ["multiuser"]
included-from-omnibus = ["bonsaidb-macros/omnibus-path"]
included-from-server = ["bonsaidb-macros/server-path"]
included-from-local = ["bonsaidb-macros/local-path"]
included-from-client = ["bonsaidb-macros/client-path"]

[dependencies]
bonsaidb-macros = { path = "../bonsaidb-macros", version = "0.1.0" }
Expand Down
2 changes: 2 additions & 0 deletions crates/bonsaidb-core/src/schema/mod.rs
Expand Up @@ -20,6 +20,8 @@ pub use self::{
};
use crate::Error;

pub use bonsaidb_macros::{Collection, View};

/// Defines a group of collections that are stored into a single database.
pub trait Schema: Send + Sync + Debug + 'static {
/// Returns the unique [`SchemaName`] for this schema.
Expand Down
2 changes: 1 addition & 1 deletion crates/bonsaidb-local/Cargo.toml
Expand Up @@ -39,7 +39,7 @@ included-from-omnibus = []

[dependencies]
async-trait = "0.1"
bonsaidb-core = { path = "../bonsaidb-core", version = "0.1.0" }
bonsaidb-core = { path = "../bonsaidb-core", version = "0.1.0", features = ["included-from-local"] }
bonsaidb-utils = { path = "../bonsaidb-utils", version = "0.1.0" }
nebari = { version = "0.2" }
thiserror = "1"
Expand Down
22 changes: 20 additions & 2 deletions crates/bonsaidb-macros/Cargo.toml
Expand Up @@ -15,9 +15,27 @@ homepage = "https://bonsaidb.io/"
proc-macro = true

[dependencies]
syn = "1"
quote = "1"
attribute-derive = "0.1.0"
proc-macro-crate = "1.1.0"
proc-macro-error = "1"
proc-macro2 = "1.0.36"
quote = "1"
syn = "1"
trybuild = "1.0.54"

[dev-dependencies]
compiletest_rs = "0.7"
transmog-bincode = "0.1.0-dev.2"

[dev-dependencies.bonsaidb]
path = "../bonsaidb/"

[dev-dependencies.serde]
version = "1.0.133"
features = ["derive"]

[features]
omnibus-path = []
server-path = []
local-path = []
client-path = []
235 changes: 188 additions & 47 deletions crates/bonsaidb-macros/src/lib.rs
Expand Up @@ -10,63 +10,204 @@
future_incompatible,
rust_2018_idioms,
)]
#![allow(clippy::option_if_let_else)]
#![cfg_attr(doc, deny(rustdoc::all))]

use proc_macro::TokenStream;
use proc_macro_error::{abort, abort_call_site, proc_macro_error};
use attribute_derive::Attribute;
use proc_macro2::{Span, TokenStream};
use proc_macro_crate::{crate_name, FoundCrate};
use proc_macro_error::{proc_macro_error, ResultExt};
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput};
use syn::{
parse_macro_input, parse_quote, punctuated::Punctuated, token::Paren, DeriveInput, Ident,
LitStr, Path, Type, TypeTuple,
};

/// Derives the `bonsaidb_core::permissions::Action` trait.
fn core_path() -> Path {
match crate_name("bonsaidb")
.or_else(|_| crate_name("bonsaidb_server"))
.or_else(|_| crate_name("bonsaidb_local"))
.or_else(|_| crate_name("bonsaidb_client"))
{
Ok(FoundCrate::Name(name)) => {
let ident = Ident::new(&name, Span::call_site());
parse_quote!(::#ident::core)
}
Ok(FoundCrate::Itself) => parse_quote!(crate),
Err(_) => match crate_name("bonsaidb_core") {
Ok(FoundCrate::Name(name)) => {
let ident = Ident::new(&name, Span::call_site());
parse_quote!(::#ident::core)
}
Ok(FoundCrate::Itself) => parse_quote!(crate),
Err(_) => match () {
() if cfg!(feature = "omnibus-path") => parse_quote!(::bonsaidb::core),
() if cfg!(feature = "server-path") => parse_quote!(::bonsaidb_server::core),
() if cfg!(feature = "local-path") => parse_quote!(::bonsaidb_local::core),
() if cfg!(feature = "client-path") => parse_quote!(::bonsaidb_client::core),
_ => parse_quote!(::bonsaidb_core),
},
},
}
}

#[derive(Attribute)]
#[attribute(ident = "collection")]
#[attribute(
invalid_field = r#"Only `authority = "some-authority"`, `name = "some-name"`, `views = [SomeView, AnotherView]`, `serialization = Serialization` are supported attributes"#
)]
struct CollectionAttribute {
authority: Option<String>,
#[attribute(
missing = r#"You need to specify the collection name via `#[collection(name = "name")]`"#
)]
name: String,
#[attribute(default)]
#[attribute(expected = r#"Specify the `views` like so: `view = [SomeView, AnotherView]`"#)]
views: Vec<Type>,
#[attribute(
expected = r#"Specify the `serialization` like so: `serialization = Format` or `serialization = None` to disable deriving it"#
)]
serialization: Option<Path>,
#[attribute(expected = r#"Specify the the path to `core` like so: `core = bosaidb::core`"#)]
core: Option<Path>,
}

/// Derives the `bonsaidb::core::schema::Collection` trait.
#[proc_macro_error]
#[proc_macro_derive(Action)]
pub fn permissions_action_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);

let name = input.ident;

let mut fields = Vec::new();
match input.data {
Data::Enum(data) => {
for variant in data.variants.iter() {
let ident = variant.ident.clone();
let ident_as_string = ident.to_string();
match variant.fields.len() {
0 => {
fields.push(quote! { Self::#ident => ActionName(vec![::std::borrow::Cow::Borrowed(#ident_as_string)]) });
}
1 => {
fields.push(quote! {
Self::#ident(subaction) => {
let mut name = Action::name(subaction);
name.0.insert(0, ::std::borrow::Cow::Borrowed(#ident_as_string));
name
}
});
}
_ => {
abort!(
variant.ident,
"For derive(Action), all enum variants may have at most 1 field"
)
}
/// `#[collection(authority = "Authority", name = "Name", views = [a, b, c])]`
#[proc_macro_derive(Collection, attributes(collection))]
pub fn collection_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let DeriveInput {
attrs,
ident,
generics,
..
} = parse_macro_input!(input as DeriveInput);

let CollectionAttribute {
authority,
name,
views,
serialization,
core,
} = CollectionAttribute::from_attributes(attrs).unwrap_or_abort();

let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

let core = core.unwrap_or_else(core_path);

let serialization = match serialization {
Some(serialization) if serialization.is_ident("None") => TokenStream::new(),
Some(serialization) => quote! {
impl #impl_generics #core::schema::SerializedCollection for #ident #ty_generics #where_clause {
type Contents = #ident #ty_generics;
type Format = #serialization;

fn format() -> Self::Format {
#serialization::default()
}
}
},
None => quote! {
impl #impl_generics #core::schema::DefaultSerialization for #ident #ty_generics #where_clause {}
},
};

let name = authority.map_or_else(
|| quote!(#core::schema::CollectionName::private(#name)),
|authority| quote!(#core::schema::CollectionName::new(#authority, #name)),
);

quote! {
impl #impl_generics #core::schema::Collection for #ident #ty_generics #where_clause {
fn collection_name() -> #core::schema::CollectionName {
#name
}
fn define_views(schema: &mut #core::schema::Schematic) -> ::core::result::Result<(), #core::Error>{
#( schema.define_view(#views)?; )*
::core::result::Result::Ok(())
}
}
_ => abort_call_site!("Action can only be derived for an enum."),
#serialization
}
.into()
}

let expanded = quote! {
impl Action for #name {
fn name(&self) -> ActionName {
match self {
#(
#fields
),*
}
#[derive(Attribute)]
#[attribute(ident = "view")]
#[attribute(
invalid_field = r#"Only `collection = CollectionType`, `key = KeyType`, `name = "by-name"`, `value = ValueType` are supported attributes"#
)]
struct ViewAttribute {
#[attribute(
missing = r#"You need to specify the collection type via `#[view(collection = CollectionType)]`"#
)]
#[attribute(
expected = r#"Specify the collection type like so: `collection = CollectionType`"#
)]
collection: Type,
#[attribute(missing = r#"You need to specify the key type via `#[view(key = KeyType)]`"#)]
#[attribute(expected = r#"Specify the key type like so: `key = KeyType`"#)]
key: Type,
name: Option<LitStr>,
#[attribute(expected = r#"Specify the value type like so: `value = ValueType`"#)]
value: Option<Type>,
#[attribute(expected = r#"Specify the the path to `core` like so: `core = bosaidb::core`"#)]
core: Option<Path>,
}

/// Derives the `bonsaidb::core::schema::View` trait.
#[proc_macro_error]
/// `#[view(collection=CollectionType, key=KeyType, value=ValueType, name = "by-name")]`
/// `name` and `value` are optional
#[proc_macro_derive(View, attributes(view))]
pub fn view_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let DeriveInput {
attrs,
ident,
generics,
..
} = parse_macro_input!(input as DeriveInput);

let ViewAttribute {
collection,
key,
name,
value,
core,
} = ViewAttribute::from_attributes(attrs).unwrap_or_abort();

let core = core.unwrap_or_else(core_path);

let value = value.unwrap_or_else(|| {
Type::Tuple(TypeTuple {
paren_token: Paren::default(),
elems: Punctuated::new(),
})
});
let name = name
.as_ref()
.map_or_else(|| ident.to_string(), LitStr::value);

let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

quote! {
impl #impl_generics #core::schema::View for #ident #ty_generics #where_clause {
type Collection = #collection;
type Key = #key;
type Value = #value;

fn name(&self) -> #core::schema::Name {
#core::schema::Name::new(#name)
}
}
};
}
.into()
}

#[test]
fn ui() {
use trybuild::TestCases;

TokenStream::from(expanded)
TestCases::new().compile_fail("tests/ui/*/*.rs");
}

0 comments on commit f0001ec

Please sign in to comment.