diff --git a/crates/bonsaidb-client/Cargo.toml b/crates/bonsaidb-client/Cargo.toml index 7f044574d9e..10ccaedf893 100644 --- a/crates/bonsaidb-client/Cargo.toml +++ b/crates/bonsaidb-client/Cargo.toml @@ -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" diff --git a/crates/bonsaidb-core/Cargo.toml b/crates/bonsaidb-core/Cargo.toml index 1f5c164333e..0346e214e0e 100644 --- a/crates/bonsaidb-core/Cargo.toml +++ b/crates/bonsaidb-core/Cargo.toml @@ -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" } diff --git a/crates/bonsaidb-core/src/schema/mod.rs b/crates/bonsaidb-core/src/schema/mod.rs index 3bcc6de9033..c16b1487bfa 100644 --- a/crates/bonsaidb-core/src/schema/mod.rs +++ b/crates/bonsaidb-core/src/schema/mod.rs @@ -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. diff --git a/crates/bonsaidb-local/Cargo.toml b/crates/bonsaidb-local/Cargo.toml index 5775b1447aa..5c2de05e6ac 100644 --- a/crates/bonsaidb-local/Cargo.toml +++ b/crates/bonsaidb-local/Cargo.toml @@ -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" diff --git a/crates/bonsaidb-macros/Cargo.toml b/crates/bonsaidb-macros/Cargo.toml index febe4da18f3..e957dace14d 100644 --- a/crates/bonsaidb-macros/Cargo.toml +++ b/crates/bonsaidb-macros/Cargo.toml @@ -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 = [] diff --git a/crates/bonsaidb-macros/src/lib.rs b/crates/bonsaidb-macros/src/lib.rs index 858852364fa..0d2154506c9 100644 --- a/crates/bonsaidb-macros/src/lib.rs +++ b/crates/bonsaidb-macros/src/lib.rs @@ -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, + #[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, + #[attribute( + expected = r#"Specify the `serialization` like so: `serialization = Format` or `serialization = None` to disable deriving it"# + )] + serialization: Option, + #[attribute(expected = r#"Specify the the path to `core` like so: `core = bosaidb::core`"#)] + core: Option, +} + +/// 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, + #[attribute(expected = r#"Specify the value type like so: `value = ValueType`"#)] + value: Option, + #[attribute(expected = r#"Specify the the path to `core` like so: `core = bosaidb::core`"#)] + core: Option, +} + +/// 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"); } diff --git a/crates/bonsaidb-macros/tests/collection.rs b/crates/bonsaidb-macros/tests/collection.rs new file mode 100644 index 00000000000..a006ec6f776 --- /dev/null +++ b/crates/bonsaidb-macros/tests/collection.rs @@ -0,0 +1,106 @@ +use core::fmt::Debug; + +use bonsaidb::core::{document::CollectionDocument, schema::Schematic}; + +#[test] +fn name_only() { + use bonsaidb::core::schema::Collection; + #[derive(Collection, Debug)] + #[collection(name = "Name", core = ::bonsaidb::core)] + struct Test(T); + + assert_eq!( + Test::::collection_name(), + bonsaidb::core::schema::CollectionName::private("Name") + ); +} +#[test] +fn name_and_authority() { + use bonsaidb::core::schema::Collection; + #[derive(Collection, Debug)] + #[collection(name = "Name", authority = "Authority")] + struct Test(T); + + assert_eq!( + Test::::collection_name(), + bonsaidb::core::schema::CollectionName::new("Authority", "Name") + ); +} +#[test] +fn views() { + use bonsaidb::core::schema::{ + Collection, CollectionViewSchema, DefaultViewSerialization, Name, View, ViewMapResult, + }; + use serde::{Deserialize, Serialize}; + + #[derive(Collection, Debug, Serialize, Deserialize)] + #[collection(name = "Name", authority = "Authority", views = [ShapesByNumberOfSides])] + struct Shape { + pub sides: u32, + } + + let schematic = Schematic::from_schema::().unwrap(); + assert!(schematic.view::().is_some()); + + #[derive(Debug, Clone)] + struct ShapesByNumberOfSides; + + impl View for ShapesByNumberOfSides { + type Collection = Shape; + type Key = u32; + type Value = usize; + + fn name(&self) -> Name { + Name::new("by-number-of-sides") + } + } + + impl CollectionViewSchema for ShapesByNumberOfSides { + type View = Self; + + fn map(&self, document: CollectionDocument) -> ViewMapResult { + Ok(document.emit_key_and_value(document.contents.sides, 1)) + } + } + + impl DefaultViewSerialization for ShapesByNumberOfSides {} +} + +#[test] +fn serialization() { + use bonsaidb::core::schema::Collection; + use bonsaidb::core::schema::SerializedCollection; + use serde::{Deserialize, Serialize}; + + #[derive(Collection, Debug, Deserialize, Serialize)] + #[collection( + name = "Name", + authority = "Authority", + serialization = transmog_bincode::Bincode + )] + struct Test; + + assert_eq!( + Test::collection_name(), + bonsaidb::core::schema::CollectionName::new("Authority", "Name") + ); + + let _: transmog_bincode::Bincode = Test::format(); +} + +#[test] +fn serialization_none() { + use bonsaidb::core::schema::{Collection, DefaultSerialization}; + use serde::{Deserialize, Serialize}; + + #[derive(Collection, Debug, Deserialize, Serialize)] + #[collection(name = "Name", authority = "Authority", serialization = None)] + struct Test; + + impl DefaultSerialization for Test {} + + assert_eq!( + Test::collection_name(), + bonsaidb::core::schema::CollectionName::new("Authority", "Name") + ); +} diff --git a/crates/bonsaidb-macros/tests/ui/collection/invalid_attribute.rs b/crates/bonsaidb-macros/tests/ui/collection/invalid_attribute.rs new file mode 100644 index 00000000000..7b845c23af3 --- /dev/null +++ b/crates/bonsaidb-macros/tests/ui/collection/invalid_attribute.rs @@ -0,0 +1,11 @@ +use bonsaidb::core::schema::Collection; + +#[derive(Collection)] +#[collection(name = "hi", authority = "hello", "hi")] +struct Test; + +#[derive(Collection)] +#[collection(name = "hi", authority = "hello", field = 200)] +struct Test2; + +fn main() {} diff --git a/crates/bonsaidb-macros/tests/ui/collection/invalid_attribute.stderr b/crates/bonsaidb-macros/tests/ui/collection/invalid_attribute.stderr new file mode 100644 index 00000000000..0f74d8a9a3e --- /dev/null +++ b/crates/bonsaidb-macros/tests/ui/collection/invalid_attribute.stderr @@ -0,0 +1,11 @@ +error: expected identifier + --> tests/ui/collection/invalid_attribute.rs:4:48 + | +4 | #[collection(name = "hi", authority = "hello", "hi")] + | ^^^^ + +error: Only `authority = \"some-authority\"`, `name = \"some-name\"`, `views = [SomeView, AnotherView]`, `serialization = Serialization` are supported attributes + --> tests/ui/collection/invalid_attribute.rs:8:48 + | +8 | #[collection(name = "hi", authority = "hello", field = 200)] + | ^^^^^ diff --git a/crates/bonsaidb-macros/tests/ui/collection/missing_name.rs b/crates/bonsaidb-macros/tests/ui/collection/missing_name.rs new file mode 100644 index 00000000000..c22fa1e1988 --- /dev/null +++ b/crates/bonsaidb-macros/tests/ui/collection/missing_name.rs @@ -0,0 +1,7 @@ +use bonsaidb::core::schema::Collection; + +#[derive(Collection)] +#[collection(authority = "hello")] +struct Test; + +fn main() {} diff --git a/crates/bonsaidb-macros/tests/ui/collection/missing_name.stderr b/crates/bonsaidb-macros/tests/ui/collection/missing_name.stderr new file mode 100644 index 00000000000..4bdc2d3d53d --- /dev/null +++ b/crates/bonsaidb-macros/tests/ui/collection/missing_name.stderr @@ -0,0 +1,7 @@ +error: You need to specify the collection name via `#[collection(name = \"name\")]` + --> tests/ui/collection/missing_name.rs:3:10 + | +3 | #[derive(Collection)] + | ^^^^^^^^^^ + | + = note: this error originates in the derive macro `Collection` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/bonsaidb-macros/tests/ui/view/invalid_attribute.rs b/crates/bonsaidb-macros/tests/ui/view/invalid_attribute.rs new file mode 100644 index 00000000000..62799c4e05f --- /dev/null +++ b/crates/bonsaidb-macros/tests/ui/view/invalid_attribute.rs @@ -0,0 +1,7 @@ +use bonsaidb::core::schema::View; + +#[derive(View)] +#[view(name = "hi", authority = "hello", "hi")] +struct Test; + +fn main() {} diff --git a/crates/bonsaidb-macros/tests/ui/view/invalid_attribute.stderr b/crates/bonsaidb-macros/tests/ui/view/invalid_attribute.stderr new file mode 100644 index 00000000000..5c9ff145e44 --- /dev/null +++ b/crates/bonsaidb-macros/tests/ui/view/invalid_attribute.stderr @@ -0,0 +1,5 @@ +error: Only `collection = CollectionType`, `key = KeyType`, `name = \"by-name\"`, `value = ValueType` are supported attributes + --> tests/ui/view/invalid_attribute.rs:4:21 + | +4 | #[view(name = "hi", authority = "hello", "hi")] + | ^^^^^^^^^ diff --git a/crates/bonsaidb-macros/tests/ui/view/missing_collection.rs b/crates/bonsaidb-macros/tests/ui/view/missing_collection.rs new file mode 100644 index 00000000000..0e34a02c19c --- /dev/null +++ b/crates/bonsaidb-macros/tests/ui/view/missing_collection.rs @@ -0,0 +1,7 @@ +use bonsaidb::core::schema::View; + +#[derive(View)] +#[view(key = ())] +struct Test; + +fn main() {} diff --git a/crates/bonsaidb-macros/tests/ui/view/missing_collection.stderr b/crates/bonsaidb-macros/tests/ui/view/missing_collection.stderr new file mode 100644 index 00000000000..9da167e34bb --- /dev/null +++ b/crates/bonsaidb-macros/tests/ui/view/missing_collection.stderr @@ -0,0 +1,7 @@ +error: You need to specify the collection type via `#[view(collection = CollectionType)]` + --> tests/ui/view/missing_collection.rs:3:10 + | +3 | #[derive(View)] + | ^^^^ + | + = note: this error originates in the derive macro `View` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/bonsaidb-macros/tests/ui/view/missing_key.rs b/crates/bonsaidb-macros/tests/ui/view/missing_key.rs new file mode 100644 index 00000000000..c72744424d0 --- /dev/null +++ b/crates/bonsaidb-macros/tests/ui/view/missing_key.rs @@ -0,0 +1,7 @@ +use bonsaidb::core::schema::View; + +#[derive(View)] +#[view(collection = ())] +struct Test; + +fn main() {} diff --git a/crates/bonsaidb-macros/tests/ui/view/missing_key.stderr b/crates/bonsaidb-macros/tests/ui/view/missing_key.stderr new file mode 100644 index 00000000000..fdae0981b52 --- /dev/null +++ b/crates/bonsaidb-macros/tests/ui/view/missing_key.stderr @@ -0,0 +1,7 @@ +error: You need to specify the key type via `#[view(key = KeyType)]` + --> tests/ui/view/missing_key.rs:3:10 + | +3 | #[derive(View)] + | ^^^^ + | + = note: this error originates in the derive macro `View` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/bonsaidb-macros/tests/ui/view/missing_name.rs b/crates/bonsaidb-macros/tests/ui/view/missing_name.rs new file mode 100644 index 00000000000..c22fa1e1988 --- /dev/null +++ b/crates/bonsaidb-macros/tests/ui/view/missing_name.rs @@ -0,0 +1,7 @@ +use bonsaidb::core::schema::Collection; + +#[derive(Collection)] +#[collection(authority = "hello")] +struct Test; + +fn main() {} diff --git a/crates/bonsaidb-macros/tests/ui/view/missing_name.stderr b/crates/bonsaidb-macros/tests/ui/view/missing_name.stderr new file mode 100644 index 00000000000..5f5af847c00 --- /dev/null +++ b/crates/bonsaidb-macros/tests/ui/view/missing_name.stderr @@ -0,0 +1,7 @@ +error: You need to specify the collection name via `#[collection(name = \"name\")]` + --> tests/ui/view/missing_name.rs:3:10 + | +3 | #[derive(Collection)] + | ^^^^^^^^^^ + | + = note: this error originates in the derive macro `Collection` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/bonsaidb-macros/tests/view.rs b/crates/bonsaidb-macros/tests/view.rs new file mode 100644 index 00000000000..f3c69716595 --- /dev/null +++ b/crates/bonsaidb-macros/tests/view.rs @@ -0,0 +1,16 @@ +use bonsaidb::core::schema::Collection; +use core::fmt::Debug; + +#[test] +fn name_only() { + use bonsaidb::core::schema::View; + + #[derive(Collection, Debug)] + #[collection(name = "name", authority = "authority")] + struct TestCollection; + + #[derive(View, Debug)] + #[view(collection = TestCollection, name = "some strange name äöü")] + #[view(key = ())] + struct TestView; +} diff --git a/crates/bonsaidb-server/Cargo.toml b/crates/bonsaidb-server/Cargo.toml index 75332223549..fa661cca271 100644 --- a/crates/bonsaidb-server/Cargo.toml +++ b/crates/bonsaidb-server/Cargo.toml @@ -38,6 +38,7 @@ bonsaidb-core = { path = "../bonsaidb-core", version = "0.1.0", default-features "networking", "actionable-traits", "multiuser", + "included-from-server" ] } bonsaidb-local = { path = "../bonsaidb-local", version = "0.1.0", default-features = false, features = [ "internal-apis", diff --git a/crates/bonsaidb/Cargo.toml b/crates/bonsaidb/Cargo.toml index 1725faa36c1..ad1e184e3c4 100644 --- a/crates/bonsaidb/Cargo.toml +++ b/crates/bonsaidb/Cargo.toml @@ -103,7 +103,7 @@ client-password-hashing = [ ] [dependencies] -bonsaidb-core = { path = "../bonsaidb-core", version = "0.1.0", default-features = false } +bonsaidb-core = { path = "../bonsaidb-core", version = "0.1.0", default-features = false, features=["included-from-omnibus"] } bonsaidb-local = { path = "../bonsaidb-local", version = "0.1.0", default-features = false, optional = true } bonsaidb-client = { path = "../bonsaidb-client", version = "0.1.0", default-features = false, optional = true } bonsaidb-server = { path = "../bonsaidb-server", version = "0.1.0", default-features = false, optional = true }