From f29b1d9b89f6ed4a0632a00eea208f85c9d4dccf Mon Sep 17 00:00:00 2001 From: rsd-darshan Date: Sat, 27 Jun 2026 09:51:30 +0545 Subject: [PATCH 1/2] Add buffa-remote-derive: ProtoString/ProtoBytes/ProtoList for remote types Generates the newtype + Deref/AsRef/From/buffa-trait boilerplate that the orphan rule otherwise forces consumers to hand-write when wrapping a foreign type (e.g. ecow::EcoString) to satisfy a pluggable owned-type trait. ProtoBox and MapStorage are out of scope here: their reference newtypes call inherent methods (smallbox::SmallBox::into_inner, indexmap::IndexMap::insert) rather than trait methods, so covering them needs a different, attribute-driven design and will ship as a follow-up. --- Cargo.lock | 18 +++ Cargo.toml | 1 + buffa-remote-derive/Cargo.toml | 23 ++++ buffa-remote-derive/src/bytes.rs | 59 ++++++++++ buffa-remote-derive/src/lib.rs | 111 +++++++++++++++++++ buffa-remote-derive/src/list.rs | 109 +++++++++++++++++++ buffa-remote-derive/src/remote_field.rs | 127 ++++++++++++++++++++++ buffa-remote-derive/src/string.rs | 69 ++++++++++++ buffa-remote-derive/tests/proto_bytes.rs | 27 +++++ buffa-remote-derive/tests/proto_list.rs | 43 ++++++++ buffa-remote-derive/tests/proto_string.rs | 44 ++++++++ 11 files changed, 631 insertions(+) create mode 100644 buffa-remote-derive/Cargo.toml create mode 100644 buffa-remote-derive/src/bytes.rs create mode 100644 buffa-remote-derive/src/lib.rs create mode 100644 buffa-remote-derive/src/list.rs create mode 100644 buffa-remote-derive/src/remote_field.rs create mode 100644 buffa-remote-derive/src/string.rs create mode 100644 buffa-remote-derive/tests/proto_bytes.rs create mode 100644 buffa-remote-derive/tests/proto_list.rs create mode 100644 buffa-remote-derive/tests/proto_string.rs diff --git a/Cargo.lock b/Cargo.lock index 9e6dbcb..2ddb966 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,6 +117,18 @@ dependencies = [ "serde_json", ] +[[package]] +name = "buffa-remote-derive" +version = "0.8.0" +dependencies = [ + "buffa", + "ecow", + "proc-macro2", + "quote", + "smallvec", + "syn", +] + [[package]] name = "buffa-smolstr" version = "0.8.0" @@ -792,6 +804,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + [[package]] name = "smol_str" version = "0.3.2" diff --git a/Cargo.toml b/Cargo.toml index a9f1617..a549353 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "buffa-descriptor", "buffa-codegen", "buffa-build", + "buffa-remote-derive", "buffa-test", "buffa-yaml", "protoc-gen-buffa", diff --git a/buffa-remote-derive/Cargo.toml b/buffa-remote-derive/Cargo.toml new file mode 100644 index 0000000..a74c345 --- /dev/null +++ b/buffa-remote-derive/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "buffa-remote-derive" +description = "Derive macros that implement buffa's pluggable owned-type traits (ProtoString, ProtoBytes, ProtoList) for a newtype wrapping a foreign remote type" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +keywords = ["protobuf", "protocol-buffers", "derive"] +categories = ["encoding"] + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = { workspace = true } +quote = { workspace = true } +syn = { workspace = true } + +[dev-dependencies] +buffa = { workspace = true, features = ["std"] } +ecow = { workspace = true } +smallvec = { version = "1", default-features = false } diff --git a/buffa-remote-derive/src/bytes.rs b/buffa-remote-derive/src/bytes.rs new file mode 100644 index 0000000..12e8597 --- /dev/null +++ b/buffa-remote-derive/src/bytes.rs @@ -0,0 +1,59 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::DeriveInput; + +use crate::remote_field::{self, RemoteField}; + +pub fn derive(input: DeriveInput) -> syn::Result { + let remote = remote_field::parse(&input)?; + let RemoteField { + ident, + generics, + remote_ty, + accessor, + .. + } = &remote; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let from_vec = remote_field::qualified_call( + remote_ty, + quote! { ::core::convert::From<::buffa::alloc::vec::Vec> }, + "from", + ); + + let ctor_from_vec = remote.construct(quote! { #from_vec(v) }); + let ctor_from_wire = remote.construct(quote! { #from_vec(payload.as_slice().to_vec()) }); + + Ok(quote! { + impl #impl_generics ::core::ops::Deref for #ident #ty_generics #where_clause { + type Target = [u8]; + #[inline] + fn deref(&self) -> &[u8] { + #accessor.as_ref() + } + } + + impl #impl_generics ::core::convert::AsRef<[u8]> for #ident #ty_generics #where_clause { + #[inline] + fn as_ref(&self) -> &[u8] { + #accessor.as_ref() + } + } + + impl #impl_generics ::core::convert::From<::buffa::alloc::vec::Vec> for #ident #ty_generics #where_clause { + #[inline] + fn from(v: ::buffa::alloc::vec::Vec) -> Self { + #ctor_from_vec + } + } + + impl #impl_generics ::buffa::ProtoBytes for #ident #ty_generics #where_clause { + #[inline] + fn from_wire( + payload: ::buffa::WirePayload<'_>, + ) -> ::core::result::Result { + ::core::result::Result::Ok(#ctor_from_wire) + } + } + }) +} diff --git a/buffa-remote-derive/src/lib.rs b/buffa-remote-derive/src/lib.rs new file mode 100644 index 0000000..b28583b --- /dev/null +++ b/buffa-remote-derive/src/lib.rs @@ -0,0 +1,111 @@ +//! Derive macros that implement buffa's pluggable owned-type traits for a +//! newtype wrapping a **foreign** ("remote") type. +//! +//! The owned Rust representation backing a proto `string`/`bytes`/`repeated` +//! field is pluggable (see [`buffa::ProtoString`], [`buffa::ProtoBytes`], +//! [`buffa::ProtoList`]). A custom representation implements one of those +//! traits. The friction is the orphan rule: a type from another crate (e.g. +//! `ecow::EcoString`) cannot implement a buffa-owned trait directly, so it +//! must be wrapped in a crate-local newtype with the trait impl — plus +//! `Deref`, `AsRef`, and the `From` conversions the trait requires — +//! hand-written on the wrapper. That boilerplate is mechanical and identical +//! in shape every time; these derives generate it from one annotation, +//! mirroring `serde`'s `remote` attribute pattern. +//! +//! ```rust +//! #[derive(Clone, PartialEq, Default, Debug, buffa_remote_derive::ProtoString)] +//! #[buffa(remote = ecow::EcoString)] +//! pub struct MyEcoString(pub ecow::EcoString); +//! ``` +//! +//! expands the `Deref`, `AsRef`, `From`, +//! `From<&str>`, and `buffa::ProtoString` impls that would otherwise be +//! hand-written (compare to the worked example in `buffa-smolstr` or +//! `examples/custom-types`). The remote type must already satisfy +//! `ProtoString`'s non-buffa-owned supertraits (`Clone`, `PartialEq`, +//! `Default`, `Debug`, `Send`, `Sync`, `AsRef`, `From`, +//! `From<&str>`) — true of essentially every inline/shared-string crate, since +//! that's the API surface they're built to offer as a `String` substitute. +//! Derive those on the newtype yourself (they forward to the inner field +//! automatically via `#[derive(..)]`); this crate only generates the +//! buffa-specific pieces that the orphan rule blocks. If the remote type is +//! missing one of those supertraits, the compiler error names the missing +//! trait bound against the newtype's field — there is no need to expand the +//! macro to diagnose it. +//! +//! [`ProtoBytes`](macro@ProtoBytes) and [`ProtoList`](macro@ProtoList) follow +//! the same shape for `bytes` and `repeated` fields respectively. `ProtoList` +//! additionally requires the remote collection to implement `Extend` (used +//! to implement `push`); its generated `clear` reinitializes the field via +//! `Default::default()`, which drops the existing allocation rather than +//! retaining capacity — acceptable per `ProtoList`'s contract ("retaining +//! capacity *where the underlying type allows*"), but worth knowing if a +//! decoder reuses long-lived buffers and capacity retention matters for that +//! workload. Hand-write `clear` to forward to the remote's own clearing +//! method instead, in that case. +//! +//! # Scope +//! +//! `ProtoBox` and `MapStorage` are deliberately not covered here — their +//! reference newtypes call **inherent** methods on the remote type (e.g. +//! `smallbox::SmallBox::into_inner()`, `indexmap::IndexMap::insert()`) rather +//! than trait methods, so a derive covering them needs a different, +//! attribute-driven design and ships separately. +//! +//! # Why a `remote` attribute that just repeats the field's type? +//! +//! It doesn't change what's generated — the macro always reads the wrapped +//! field's actual type, never the type written in the attribute — and its +//! content is not checked against the field (comparing two type *spellings* +//! for equality isn't possible from within a derive macro without resolving +//! `use` imports). It exists so the newtype's purpose is legible without +//! reading the field declaration, the same role `serde`'s `remote` attribute +//! plays. The value still has to parse as a type, so a typo is caught even +//! though its content isn't otherwise used. + +use proc_macro::TokenStream; +use syn::{parse_macro_input, DeriveInput}; + +mod bytes; +mod list; +mod remote_field; +mod string; + +/// See the [crate-level docs](crate) for the full pattern. Generates +/// `Deref`, `AsRef`, `From`, `From<&str>`, and +/// `buffa::ProtoString` for a single-field newtype wrapping the type named by +/// `#[buffa(remote = ...)]`. +#[proc_macro_derive(ProtoString, attributes(buffa))] +pub fn derive_proto_string(input: TokenStream) -> TokenStream { + expand(input, string::derive) +} + +/// See the [crate-level docs](crate). Generates `Deref`, +/// `AsRef<[u8]>`, `From>`, and `buffa::ProtoBytes` for a single-field +/// newtype wrapping the type named by `#[buffa(remote = ...)]`. +#[proc_macro_derive(ProtoBytes, attributes(buffa))] +pub fn derive_proto_bytes(input: TokenStream) -> TokenStream { + expand(input, bytes::derive) +} + +/// See the [crate-level docs](crate). Generates `Deref`, +/// `FromIterator`, `From>`, and `buffa::ProtoList` for a +/// single-field, single-type-parameter newtype wrapping the type named by +/// `#[buffa(remote = ...)]`. Requires the remote type to implement +/// `Extend`, and the newtype itself to implement `Default` by hand (not +/// `#[derive(Default)]`, which would wrongly force `T: Default`). +#[proc_macro_derive(ProtoList, attributes(buffa))] +pub fn derive_proto_list(input: TokenStream) -> TokenStream { + expand(input, list::derive) +} + +fn expand( + input: TokenStream, + f: impl FnOnce(DeriveInput) -> syn::Result, +) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + match f(input) { + Ok(tokens) => tokens.into(), + Err(err) => err.to_compile_error().into(), + } +} diff --git a/buffa-remote-derive/src/list.rs b/buffa-remote-derive/src/list.rs new file mode 100644 index 0000000..efd7d76 --- /dev/null +++ b/buffa-remote-derive/src/list.rs @@ -0,0 +1,109 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::{DeriveInput, GenericParam}; + +use crate::remote_field::{self, RemoteField}; + +pub fn derive(input: DeriveInput) -> syn::Result { + let remote = remote_field::parse(&input)?; + let RemoteField { + ident, + generics, + remote_ty, + accessor, + .. + } = &remote; + + let element_ty = single_type_param(generics)?; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let from_iter = remote_field::qualified_call( + remote_ty, + quote! { ::core::iter::FromIterator<#element_ty> }, + "from_iter", + ); + let from_vec = remote_field::qualified_call( + remote_ty, + quote! { ::core::convert::From<::buffa::alloc::vec::Vec<#element_ty>> }, + "from", + ); + + let ctor_from_iter = remote.construct(quote! { #from_iter(iter) }); + let ctor_from_vec = remote.construct(quote! { #from_vec(v) }); + + Ok(quote! { + impl #impl_generics ::core::ops::Deref for #ident #ty_generics #where_clause { + type Target = [#element_ty]; + #[inline] + fn deref(&self) -> &[#element_ty] { + #accessor.as_ref() + } + } + + impl #impl_generics ::core::iter::FromIterator<#element_ty> for #ident #ty_generics #where_clause { + #[inline] + fn from_iter<__BuffaIter: ::core::iter::IntoIterator>( + iter: __BuffaIter, + ) -> Self { + #ctor_from_iter + } + } + + impl #impl_generics ::core::convert::From<::buffa::alloc::vec::Vec<#element_ty>> for #ident #ty_generics #where_clause { + #[inline] + fn from(v: ::buffa::alloc::vec::Vec<#element_ty>) -> Self { + #ctor_from_vec + } + } + + impl #impl_generics ::buffa::ProtoList<#element_ty> for #ident #ty_generics + where + #element_ty: ::core::clone::Clone + + ::core::cmp::PartialEq + + ::core::fmt::Debug + + ::core::marker::Send + + ::core::marker::Sync, + #remote_ty: ::core::iter::Extend<#element_ty>, + Self: ::core::default::Default, + { + #[inline] + fn push(&mut self, value: #element_ty) { + #accessor.extend(::core::iter::once(value)); + } + + // Reinitializes via `Default` rather than forwarding to a native + // `clear` (no such method is assumed to exist on the remote + // type), so the existing allocation is dropped rather than + // retained. `ProtoList`'s contract only asks for capacity + // retention "where the underlying type allows" — see the crate + // docs if that matters for your workload. + #[inline] + fn clear(&mut self) { + *self = ::core::default::Default::default(); + } + } + }) +} + +/// Requires the struct to have exactly one type parameter — the list's +/// element type — which keeps the generated `ProtoList` bound +/// unambiguous. A remote collection wrapped by more than one type parameter +/// (e.g. a custom hasher parameter) is out of scope for this derive; hand-write +/// the impl in that case. +fn single_type_param(generics: &syn::Generics) -> syn::Result { + let type_params: Vec<_> = generics + .params + .iter() + .filter_map(|p| match p { + GenericParam::Type(t) => Some(t.ident.clone()), + _ => None, + }) + .collect(); + match type_params.as_slice() { + [single] => Ok(single.clone()), + _ => Err(syn::Error::new_spanned( + &generics.params, + "this derive requires exactly one type parameter, the list's element type", + )), + } +} diff --git a/buffa-remote-derive/src/remote_field.rs b/buffa-remote-derive/src/remote_field.rs new file mode 100644 index 0000000..ad29305 --- /dev/null +++ b/buffa-remote-derive/src/remote_field.rs @@ -0,0 +1,127 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::spanned::Spanned; +use syn::{Data, DeriveInput, Fields}; + +/// The single field a remote-derive newtype wraps, plus the struct's name, +/// generics, and the `remote` path it claims to wrap. +pub struct RemoteField { + pub ident: syn::Ident, + pub generics: syn::Generics, + pub remote_ty: syn::Type, + /// `self.0` for a tuple struct, `self.field_name` for a named-field one. + pub accessor: TokenStream, + /// `Some(name)` for a named-field struct, `None` for a tuple struct — + /// used to build a `Self { name: value }` vs. `Self(value)` constructor. + pub field_name: Option, +} + +/// Extracts the single field from a tuple or named-field struct, and the +/// `#[buffa(remote = ...)]` attribute naming the wrapped foreign type. +/// +/// The generated code always operates on the field's actual type, never on +/// the type written in the attribute — comparing the two would require +/// resolving `use` imports and module paths to decide whether two *spellings* +/// name the same type, which isn't possible from within a derive macro. The +/// attribute is therefore documentation, not codegen input: it must be +/// present (so the newtype's purpose is legible without reading the field +/// declaration) and must parse as a type (catching outright typos), but its +/// content is not checked against the field. +/// +/// Requires the struct to have exactly one field (newtype shape). +pub fn parse(input: &DeriveInput) -> syn::Result { + let (field_ty, accessor, field_name) = single_field(input)?; + require_remote_attr(input)?; + + Ok(RemoteField { + ident: input.ident.clone(), + generics: input.generics.clone(), + remote_ty: field_ty, + accessor, + field_name, + }) +} + +fn single_field(input: &DeriveInput) -> syn::Result<(syn::Type, TokenStream, Option)> { + let Data::Struct(data) = &input.data else { + return Err(syn::Error::new( + input.span(), + "this derive only applies to a single-field newtype struct", + )); + }; + match &data.fields { + Fields::Named(f) if f.named.len() == 1 => { + let field = &f.named[0]; + let name = field.ident.as_ref().expect("named field has an ident"); + Ok((field.ty.clone(), quote! { self.#name }, Some(name.clone()))) + } + Fields::Unnamed(f) if f.unnamed.len() == 1 => { + let field = &f.unnamed[0]; + Ok((field.ty.clone(), quote! { self.0 }, None)) + } + Fields::Named(f) => Err(syn::Error::new( + input.span(), + format!( + "this derive requires exactly one field wrapping the remote type, found {}", + f.named.len() + ), + )), + Fields::Unnamed(f) => Err(syn::Error::new( + input.span(), + format!( + "this derive requires exactly one field wrapping the remote type, found {}", + f.unnamed.len() + ), + )), + Fields::Unit => Err(syn::Error::new( + input.span(), + "this derive requires exactly one field wrapping the remote type", + )), + } +} + +/// Validates that `#[buffa(remote = ...)]` is present and its value parses as +/// a type (catching typos), without using the parsed type for codegen — see +/// [`parse`] for why. +fn require_remote_attr(input: &DeriveInput) -> syn::Result<()> { + for attr in &input.attrs { + if !attr.path().is_ident("buffa") { + continue; + } + let mut found = false; + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("remote") { + let _: syn::Type = meta.value()?.parse()?; + found = true; + Ok(()) + } else { + Err(meta.error("unsupported `buffa` attribute key, expected `remote`")) + } + })?; + if found { + return Ok(()); + } + } + Err(syn::Error::new( + input.span(), + "missing `#[buffa(remote = ...)]` naming the foreign type this newtype wraps", + )) +} + +/// Renders a `::method` fully-qualified call path, for +/// disambiguating which impl a generated body invokes. +pub fn qualified_call(remote_ty: &syn::Type, trait_path: TokenStream, method: &str) -> TokenStream { + let method = syn::Ident::new(method, proc_macro2::Span::call_site()); + quote! { <#remote_ty as #trait_path>::#method } +} + +impl RemoteField { + /// Builds `Self(value)` or `Self { field_name: value }`, matching whichever + /// shape the wrapped struct uses. + pub fn construct(&self, value: TokenStream) -> TokenStream { + match &self.field_name { + Some(name) => quote! { Self { #name: #value } }, + None => quote! { Self(#value) }, + } + } +} diff --git a/buffa-remote-derive/src/string.rs b/buffa-remote-derive/src/string.rs new file mode 100644 index 0000000..a5bec9f --- /dev/null +++ b/buffa-remote-derive/src/string.rs @@ -0,0 +1,69 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::DeriveInput; + +use crate::remote_field::{self, RemoteField}; + +pub fn derive(input: DeriveInput) -> syn::Result { + let remote = remote_field::parse(&input)?; + let RemoteField { + ident, + generics, + remote_ty, + accessor, + .. + } = &remote; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let from_string = remote_field::qualified_call( + remote_ty, + quote! { ::core::convert::From<::buffa::alloc::string::String> }, + "from", + ); + let from_str = + remote_field::qualified_call(remote_ty, quote! { ::core::convert::From<&str> }, "from"); + + let ctor_from_string = remote.construct(quote! { #from_string(s) }); + let ctor_from_str = remote.construct(quote! { #from_str(s) }); + let ctor_from_wire = remote.construct(quote! { #from_str(s) }); + + Ok(quote! { + impl #impl_generics ::core::ops::Deref for #ident #ty_generics #where_clause { + type Target = str; + #[inline] + fn deref(&self) -> &str { + #accessor.as_ref() + } + } + + impl #impl_generics ::core::convert::AsRef for #ident #ty_generics #where_clause { + #[inline] + fn as_ref(&self) -> &str { + #accessor.as_ref() + } + } + + impl #impl_generics ::core::convert::From<::buffa::alloc::string::String> for #ident #ty_generics #where_clause { + #[inline] + fn from(s: ::buffa::alloc::string::String) -> Self { + #ctor_from_string + } + } + + impl #impl_generics ::core::convert::From<&str> for #ident #ty_generics #where_clause { + #[inline] + fn from(s: &str) -> Self { + #ctor_from_str + } + } + + impl #impl_generics ::buffa::ProtoString for #ident #ty_generics #where_clause { + #[inline] + fn from_wire( + payload: ::buffa::WirePayload<'_>, + ) -> ::core::result::Result { + payload.to_str().map(|s| #ctor_from_wire) + } + } + }) +} diff --git a/buffa-remote-derive/tests/proto_bytes.rs b/buffa-remote-derive/tests/proto_bytes.rs new file mode 100644 index 0000000..957d7a1 --- /dev/null +++ b/buffa-remote-derive/tests/proto_bytes.rs @@ -0,0 +1,27 @@ +use buffa::{ProtoBytes, WirePayload}; +use buffa_remote_derive::ProtoBytes as DeriveProtoBytes; +use smallvec::SmallVec; + +#[derive(Clone, PartialEq, Default, Debug, DeriveProtoBytes)] +#[buffa(remote = smallvec::SmallVec<[u8; 16]>)] +struct MyBytes(pub SmallVec<[u8; 16]>); + +#[test] +fn from_wire_copies_payload() { + let b = MyBytes::from_wire(WirePayload::Borrowed(b"hello bytes")).unwrap(); + assert_eq!(b.as_ref(), b"hello bytes"); +} + +#[test] +fn deref_and_as_ref_agree() { + let b = MyBytes::from(b"abc".to_vec()); + assert_eq!(&*b, b"abc"); + assert_eq!(b.as_ref(), b"abc"); +} + +#[test] +fn from_vec_round_trips() { + let v = vec![1u8, 2, 3, 4]; + let b = MyBytes::from(v.clone()); + assert_eq!(b.as_ref(), v.as_slice()); +} diff --git a/buffa-remote-derive/tests/proto_list.rs b/buffa-remote-derive/tests/proto_list.rs new file mode 100644 index 0000000..d7d6e7c --- /dev/null +++ b/buffa-remote-derive/tests/proto_list.rs @@ -0,0 +1,43 @@ +use buffa::ProtoList; +use buffa_remote_derive::ProtoList as DeriveProtoList; +use smallvec::SmallVec; + +#[derive(Clone, PartialEq, Debug, DeriveProtoList)] +#[buffa(remote = smallvec::SmallVec<[T; 4]>)] +struct MyList(pub SmallVec<[T; 4]>); + +// Hand-written, not `#[derive(Default)]` — a derived impl would force +// `T: Default`, which `ProtoList` does not require. +impl Default for MyList { + fn default() -> Self { + Self(SmallVec::new()) + } +} + +#[test] +fn push_and_clear() { + let mut list = MyList::::default(); + list.push(1); + list.push(2); + list.push(3); + assert_eq!(&*list, &[1, 2, 3]); + list.clear(); + assert!(list.is_empty()); +} + +#[test] +fn from_iter_and_from_vec() { + let from_iter: MyList = (1..=3).collect(); + let from_vec = MyList::from(vec![1i64, 2, 3]); + assert_eq!(from_iter, from_vec); +} + +#[test] +fn works_for_non_default_element_type() { + // `f64` has no `Eq`/`Ord`, exercising that the derive does not require + // bounds beyond what `ProtoList` itself demands. + let mut list = MyList::::default(); + list.push(1.5); + list.push(2.5); + assert_eq!(&*list, &[1.5, 2.5]); +} diff --git a/buffa-remote-derive/tests/proto_string.rs b/buffa-remote-derive/tests/proto_string.rs new file mode 100644 index 0000000..3073994 --- /dev/null +++ b/buffa-remote-derive/tests/proto_string.rs @@ -0,0 +1,44 @@ +use buffa::{ProtoString, WirePayload}; +use buffa_remote_derive::ProtoString as DeriveProtoString; + +#[derive(Clone, PartialEq, Default, Debug, DeriveProtoString)] +#[buffa(remote = ecow::EcoString)] +struct MyEcoString(pub ecow::EcoString); + +#[test] +fn from_wire_decodes_valid_utf8() { + let s = MyEcoString::from_wire(WirePayload::Borrowed(b"hello")).unwrap(); + assert_eq!(s.as_ref(), "hello"); +} + +#[test] +fn from_wire_rejects_invalid_utf8() { + assert!(MyEcoString::from_wire(WirePayload::Borrowed(&[0xff, 0xfe])).is_err()); +} + +#[test] +fn deref_and_as_ref_agree() { + let s = MyEcoString::from("hi there"); + assert_eq!(&*s, "hi there"); + assert_eq!(s.as_ref(), "hi there"); +} + +#[test] +fn from_string_and_from_str_round_trip() { + let from_owned = MyEcoString::from(String::from("owned")); + let from_borrowed = MyEcoString::from("owned"); + assert_eq!(from_owned, from_borrowed); +} + +// Named-field struct shape (not just tuple structs) is also supported. +#[derive(Clone, PartialEq, Default, Debug, DeriveProtoString)] +#[buffa(remote = ecow::EcoString)] +struct NamedEcoString { + inner: ecow::EcoString, +} + +#[test] +fn named_field_struct_works() { + let s = NamedEcoString::from("named"); + assert_eq!(s.as_ref(), "named"); +} From bbb0cd06f2861d117bad17ff07dcdc7ad0c31d07 Mon Sep 17 00:00:00 2001 From: rsd-darshan Date: Sat, 27 Jun 2026 12:37:57 +0545 Subject: [PATCH 2/2] Add ProtoBox/MapStorage remote derives, completing #212 These two call inherent methods on the remote type (smallbox::SmallBox:: into_inner, indexmap::IndexMap::insert) rather than trait methods, so unlike ProtoString/ProtoBytes/ProtoList they can't be synthesized purely from supertrait bounds. Default to the conventional method names (new/into_inner for pointers, len/insert/clear/iter for maps) with an attribute override for remote types that name them differently. Also renames RemoteField::remote_ty to field_ty, since it always holds the wrapped field's actual type rather than the (documentation-only) attribute value -- the old name was misleading about what's actually used for codegen. --- Cargo.lock | 8 ++ buffa-remote-derive/Cargo.toml | 4 +- buffa-remote-derive/src/box_ptr.rs | 62 +++++++++ buffa-remote-derive/src/bytes.rs | 4 +- buffa-remote-derive/src/lib.rs | 94 +++++++++++++- buffa-remote-derive/src/list.rs | 35 +----- buffa-remote-derive/src/map.rs | 62 +++++++++ buffa-remote-derive/src/remote_field.rs | 153 +++++++++++++++++++---- buffa-remote-derive/src/string.rs | 6 +- buffa-remote-derive/tests/map_storage.rs | 97 ++++++++++++++ buffa-remote-derive/tests/proto_box.rs | 54 ++++++++ 11 files changed, 517 insertions(+), 62 deletions(-) create mode 100644 buffa-remote-derive/src/box_ptr.rs create mode 100644 buffa-remote-derive/src/map.rs create mode 100644 buffa-remote-derive/tests/map_storage.rs create mode 100644 buffa-remote-derive/tests/proto_box.rs diff --git a/Cargo.lock b/Cargo.lock index 2ddb966..bda8183 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -123,8 +123,10 @@ version = "0.8.0" dependencies = [ "buffa", "ecow", + "indexmap", "proc-macro2", "quote", + "smallbox", "smallvec", "syn", ] @@ -804,6 +806,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "smallbox" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aca054fd9f8c2ebe8557a2433f307e038c0716124efd045daa0388afa5172189" + [[package]] name = "smallvec" version = "1.15.2" diff --git a/buffa-remote-derive/Cargo.toml b/buffa-remote-derive/Cargo.toml index a74c345..d279acb 100644 --- a/buffa-remote-derive/Cargo.toml +++ b/buffa-remote-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "buffa-remote-derive" -description = "Derive macros that implement buffa's pluggable owned-type traits (ProtoString, ProtoBytes, ProtoList) for a newtype wrapping a foreign remote type" +description = "Derive macros that implement buffa's pluggable owned-type traits (ProtoString, ProtoBytes, ProtoList, ProtoBox, MapStorage) for a newtype wrapping a foreign remote type" version.workspace = true edition.workspace = true rust-version.workspace = true @@ -21,3 +21,5 @@ syn = { workspace = true } buffa = { workspace = true, features = ["std"] } ecow = { workspace = true } smallvec = { version = "1", default-features = false } +smallbox = { version = "0.8", default-features = false } +indexmap = { version = "2", default-features = false, features = ["std"] } diff --git a/buffa-remote-derive/src/box_ptr.rs b/buffa-remote-derive/src/box_ptr.rs new file mode 100644 index 0000000..dc32d8e --- /dev/null +++ b/buffa-remote-derive/src/box_ptr.rs @@ -0,0 +1,62 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::DeriveInput; + +use crate::remote_field::{self, RemoteField}; + +pub fn derive(input: DeriveInput) -> syn::Result { + let (remote, overrides) = remote_field::parse_with_overrides(&input, &["new", "into_inner"])?; + let RemoteField { + ident, + generics, + field_ty, + accessor, + .. + } = &remote; + + let element_ty = remote_field::single_type_param(generics)?; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + // Defaults assume the remote pointer follows the `Type::new(value) -> + // Self` / `Type::into_inner(self) -> T` convention (e.g. + // `smallbox::SmallBox`); override with `#[buffa(remote = ..., new = path, + // into_inner = path)]` for a remote type that names them differently — + // notably plain `std::boxed::Box`, whose `into_inner` is nightly-only + // (`box_into_inner`), so wrapping `Box` with this derive needs an + // `into_inner` override (or just use buffa's built-in `Box` impl, + // which never needs this derive). + let new_call = remote_field::overridable_call(&overrides, "new", field_ty, "new"); + let into_inner_call = + remote_field::overridable_call(&overrides, "into_inner", field_ty, "into_inner"); + + let ctor_new = remote.construct(quote! { #new_call(value) }); + + Ok(quote! { + impl #impl_generics ::core::ops::Deref for #ident #ty_generics #where_clause { + type Target = #element_ty; + #[inline] + fn deref(&self) -> &#element_ty { + &#accessor + } + } + + impl #impl_generics ::core::ops::DerefMut for #ident #ty_generics #where_clause { + #[inline] + fn deref_mut(&mut self) -> &mut #element_ty { + &mut #accessor + } + } + + impl #impl_generics ::buffa::ProtoBox<#element_ty> for #ident #ty_generics #where_clause { + #[inline] + fn new(value: #element_ty) -> Self { + #ctor_new + } + + #[inline] + fn into_inner(self) -> #element_ty { + #into_inner_call(#accessor) + } + } + }) +} diff --git a/buffa-remote-derive/src/bytes.rs b/buffa-remote-derive/src/bytes.rs index 12e8597..a1beb1d 100644 --- a/buffa-remote-derive/src/bytes.rs +++ b/buffa-remote-derive/src/bytes.rs @@ -9,14 +9,14 @@ pub fn derive(input: DeriveInput) -> syn::Result { let RemoteField { ident, generics, - remote_ty, + field_ty, accessor, .. } = &remote; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let from_vec = remote_field::qualified_call( - remote_ty, + field_ty, quote! { ::core::convert::From<::buffa::alloc::vec::Vec> }, "from", ); diff --git a/buffa-remote-derive/src/lib.rs b/buffa-remote-derive/src/lib.rs index b28583b..7c587a0 100644 --- a/buffa-remote-derive/src/lib.rs +++ b/buffa-remote-derive/src/lib.rs @@ -44,13 +44,70 @@ //! workload. Hand-write `clear` to forward to the remote's own clearing //! method instead, in that case. //! -//! # Scope +//! # `ProtoBox` and `MapStorage`: inherent methods, not trait methods //! -//! `ProtoBox` and `MapStorage` are deliberately not covered here — their -//! reference newtypes call **inherent** methods on the remote type (e.g. -//! `smallbox::SmallBox::into_inner()`, `indexmap::IndexMap::insert()`) rather -//! than trait methods, so a derive covering them needs a different, -//! attribute-driven design and ships separately. +//! [`ProtoBox`](macro@ProtoBox) and [`MapStorage`](macro@MapStorage) follow a +//! different shape from the three above. Their reference newtypes +//! (`smallbox::SmallBox::into_inner()`, `indexmap::IndexMap::insert()`) call +//! **inherent** methods on the remote type, not trait methods — `ProtoBox`'s +//! and `MapStorage`'s own supertraits (`Deref`/`DerefMut`; none, for +//! `MapStorage`) don't give a generic derive enough to call through to +//! `new`/`into_inner`/`insert`/`clear`/`iter`/`len` the way `From`/ +//! `FromIterator`/`Extend` did for `ProtoString`/`ProtoBytes`/`ProtoList`. +//! +//! So these two derives default to the near-universal naming convention +//! (`Type::new`/`Type::into_inner` for pointers — `Rc`, `Arc`, +//! `smallbox::SmallBox` all use these names, though plain `std::boxed::Box` +//! does not, since its `into_inner` is nightly-only; `len`/`insert`/`clear`/ +//! `iter` for maps — `HashMap`, `BTreeMap`, `indexmap::IndexMap`, +//! `dashmap::DashMap` all use these names), with an attribute escape hatch +//! when a remote type names them differently. If a default doesn't match — +//! the remote names its insert method `put`, say — the compiler reports +//! `no function or associated item named 'insert' found for struct '...'`; +//! that's the signal to add the matching override, named below. +//! +//! ```rust +//! #[derive(buffa_remote_derive::ProtoBox)] +//! #[buffa(remote = smallbox::SmallBox)] +//! pub struct SmallBox(pub smallbox::SmallBox); +//! ``` +//! +//! ```rust +//! #[derive(Clone, PartialEq, Debug, buffa_remote_derive::MapStorage)] +//! #[buffa(remote = indexmap::IndexMap)] +//! pub struct MyIndexMap(pub indexmap::IndexMap); +//! +//! impl Default for MyIndexMap { +//! fn default() -> Self { +//! Self(indexmap::IndexMap::new()) +//! } +//! } +//! impl FromIterator<(K, V)> for MyIndexMap { +//! fn from_iter>(iter: I) -> Self { +//! Self(indexmap::IndexMap::from_iter(iter)) +//! } +//! } +//! ``` +//! +//! `Default` and `FromIterator<(Key, Value)>` are required by the message +//! codec that drives every `MapStorage` field, not by this derive — `cargo` +//! won't suggest them, since the derive itself compiles fine without them; the +//! failure shows up later, in generated message code, as `the trait bound +//! '...: Default' is not satisfied`. They aren't generated here for the same +//! reason `ProtoList`'s `Default` isn't (see above): a derived impl would +//! force `K: Default`/`V: Default`, which `MapStorage` does not require. +//! +//! To override a default, name the method explicitly: +//! `#[buffa(remote = ..., into_inner = MyType::unwrap)]` for `ProtoBox`, or +//! any of `len`/`insert`/`clear`/`iter` for `MapStorage`. The override path is +//! called the same way the default is — as a free function taking the +//! receiver as its first argument (`Type::method(&self.0, ...)`) — so it +//! **must** accept the same receiver as the method it replaces: `new` takes +//! the value by ownership and returns `Self`; `into_inner` takes `self` by +//! ownership; `insert`/`clear` take `&mut self`; `len`/`iter` take `&self`. +//! `iter` additionally must yield `(&Key, &Value)` pairs, matching +//! `storage_iter`'s contract. A receiver or item-type mismatch is a type error +//! at the generated call site, not a special diagnostic from this macro. //! //! # Why a `remote` attribute that just repeats the field's type? //! @@ -66,8 +123,10 @@ use proc_macro::TokenStream; use syn::{parse_macro_input, DeriveInput}; +mod box_ptr; mod bytes; mod list; +mod map; mod remote_field; mod string; @@ -99,6 +158,29 @@ pub fn derive_proto_list(input: TokenStream) -> TokenStream { expand(input, list::derive) } +/// See the [crate-level docs](crate). Generates `Deref`, +/// `DerefMut`, and `buffa::ProtoBox` for a single-field, +/// single-type-parameter newtype wrapping the type named by +/// `#[buffa(remote = ...)]`. Calls the remote type's `new`/`into_inner` +/// methods by the conventional names unless overridden with +/// `#[buffa(remote = ..., new = path, into_inner = path)]`. +#[proc_macro_derive(ProtoBox, attributes(buffa))] +pub fn derive_proto_box(input: TokenStream) -> TokenStream { + expand(input, box_ptr::derive) +} + +/// See the [crate-level docs](crate). Generates `buffa::MapStorage` for a +/// single-field, two-type-parameter (`Key`, `Value`) newtype wrapping the +/// type named by `#[buffa(remote = ...)]`. Calls the remote map's +/// `len`/`insert`/`clear`/`iter` methods by their conventional names unless +/// overridden with `#[buffa(remote = ..., insert = path, ...)]`. The newtype +/// itself must implement `Default` and `FromIterator<(Key, Value)>` by hand — +/// see the crate docs' example. +#[proc_macro_derive(MapStorage, attributes(buffa))] +pub fn derive_map_storage(input: TokenStream) -> TokenStream { + expand(input, map::derive) +} + fn expand( input: TokenStream, f: impl FnOnce(DeriveInput) -> syn::Result, diff --git a/buffa-remote-derive/src/list.rs b/buffa-remote-derive/src/list.rs index efd7d76..8f0fb92 100644 --- a/buffa-remote-derive/src/list.rs +++ b/buffa-remote-derive/src/list.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; use quote::quote; -use syn::{DeriveInput, GenericParam}; +use syn::DeriveInput; use crate::remote_field::{self, RemoteField}; @@ -9,21 +9,21 @@ pub fn derive(input: DeriveInput) -> syn::Result { let RemoteField { ident, generics, - remote_ty, + field_ty, accessor, .. } = &remote; - let element_ty = single_type_param(generics)?; + let element_ty = remote_field::single_type_param(generics)?; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let from_iter = remote_field::qualified_call( - remote_ty, + field_ty, quote! { ::core::iter::FromIterator<#element_ty> }, "from_iter", ); let from_vec = remote_field::qualified_call( - remote_ty, + field_ty, quote! { ::core::convert::From<::buffa::alloc::vec::Vec<#element_ty>> }, "from", ); @@ -63,7 +63,7 @@ pub fn derive(input: DeriveInput) -> syn::Result { + ::core::fmt::Debug + ::core::marker::Send + ::core::marker::Sync, - #remote_ty: ::core::iter::Extend<#element_ty>, + #field_ty: ::core::iter::Extend<#element_ty>, Self: ::core::default::Default, { #[inline] @@ -84,26 +84,3 @@ pub fn derive(input: DeriveInput) -> syn::Result { } }) } - -/// Requires the struct to have exactly one type parameter — the list's -/// element type — which keeps the generated `ProtoList` bound -/// unambiguous. A remote collection wrapped by more than one type parameter -/// (e.g. a custom hasher parameter) is out of scope for this derive; hand-write -/// the impl in that case. -fn single_type_param(generics: &syn::Generics) -> syn::Result { - let type_params: Vec<_> = generics - .params - .iter() - .filter_map(|p| match p { - GenericParam::Type(t) => Some(t.ident.clone()), - _ => None, - }) - .collect(); - match type_params.as_slice() { - [single] => Ok(single.clone()), - _ => Err(syn::Error::new_spanned( - &generics.params, - "this derive requires exactly one type parameter, the list's element type", - )), - } -} diff --git a/buffa-remote-derive/src/map.rs b/buffa-remote-derive/src/map.rs new file mode 100644 index 0000000..7fa301c --- /dev/null +++ b/buffa-remote-derive/src/map.rs @@ -0,0 +1,62 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::DeriveInput; + +use crate::remote_field::{self, RemoteField}; + +pub fn derive(input: DeriveInput) -> syn::Result { + let (remote, overrides) = + remote_field::parse_with_overrides(&input, &["len", "insert", "clear", "iter"])?; + let RemoteField { + ident, + generics, + field_ty, + accessor, + .. + } = &remote; + + let (key_ty, value_ty) = remote_field::two_type_params(generics)?; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + // Defaults assume the near-universal map naming convention (`HashMap`, + // `BTreeMap`, `indexmap::IndexMap`, `dashmap::DashMap` all use these + // names); override with `#[buffa(remote = ..., insert = path, ...)]` for + // a remote map that names them differently. + let len_call = remote_field::overridable_call(&overrides, "len", field_ty, "len"); + let insert_call = remote_field::overridable_call(&overrides, "insert", field_ty, "insert"); + let clear_call = remote_field::overridable_call(&overrides, "clear", field_ty, "clear"); + let iter_call = remote_field::overridable_call(&overrides, "iter", field_ty, "iter"); + + Ok(quote! { + impl #impl_generics ::buffa::MapStorage for #ident #ty_generics #where_clause { + type Key = #key_ty; + type Value = #value_ty; + + #[inline] + fn storage_len(&self) -> usize { + #len_call(&#accessor) + } + + #[inline] + fn storage_insert(&mut self, key: #key_ty, value: #value_ty) { + #insert_call(&mut #accessor, key, value); + } + + #[inline] + fn storage_clear(&mut self) { + #clear_call(&mut #accessor); + } + + #[inline] + fn storage_iter<'a>( + &'a self, + ) -> impl ::core::iter::Iterator + where + #key_ty: 'a, + #value_ty: 'a, + { + #iter_call(&#accessor) + } + } + }) +} diff --git a/buffa-remote-derive/src/remote_field.rs b/buffa-remote-derive/src/remote_field.rs index ad29305..46b1ac2 100644 --- a/buffa-remote-derive/src/remote_field.rs +++ b/buffa-remote-derive/src/remote_field.rs @@ -1,14 +1,19 @@ +use std::collections::HashMap; + use proc_macro2::TokenStream; use quote::quote; use syn::spanned::Spanned; -use syn::{Data, DeriveInput, Fields}; +use syn::{Data, DeriveInput, Fields, GenericParam}; -/// The single field a remote-derive newtype wraps, plus the struct's name, -/// generics, and the `remote` path it claims to wrap. +/// The single field a remote-derive newtype wraps, plus the struct's name and +/// generics. pub struct RemoteField { pub ident: syn::Ident, pub generics: syn::Generics, - pub remote_ty: syn::Type, + /// The wrapped field's actual type — not the type written in the + /// `#[buffa(remote = ...)]` attribute, which is documentation only and + /// never read for codegen (see [`parse`]). + pub field_ty: syn::Type, /// `self.0` for a tuple struct, `self.field_name` for a named-field one. pub accessor: TokenStream, /// `Some(name)` for a named-field struct, `None` for a tuple struct — @@ -30,18 +35,44 @@ pub struct RemoteField { /// /// Requires the struct to have exactly one field (newtype shape). pub fn parse(input: &DeriveInput) -> syn::Result { + parse_overrides(input, &[])?; let (field_ty, accessor, field_name) = single_field(input)?; - require_remote_attr(input)?; Ok(RemoteField { ident: input.ident.clone(), generics: input.generics.clone(), - remote_ty: field_ty, + field_ty, accessor, field_name, }) } +/// Like [`parse`], but also collects any of `allowed_overrides` present in +/// `#[buffa(remote = ..., key = path, ...)]` as `syn::Path`s — used by derives +/// (`ProtoBox`, `MapStorage`) whose reference implementations call **inherent** +/// methods on the remote type (e.g. `into_inner()`, `insert()`) rather than +/// trait methods, so the method path can't be synthesized generically and +/// instead defaults to the common naming convention with an escape hatch to +/// override it. +pub fn parse_with_overrides( + input: &DeriveInput, + allowed_overrides: &[&str], +) -> syn::Result<(RemoteField, HashMap)> { + let overrides = parse_overrides(input, allowed_overrides)?; + let (field_ty, accessor, field_name) = single_field(input)?; + + Ok(( + RemoteField { + ident: input.ident.clone(), + generics: input.generics.clone(), + field_ty, + accessor, + field_name, + }, + overrides, + )) +} + fn single_field(input: &DeriveInput) -> syn::Result<(syn::Type, TokenStream, Option)> { let Data::Struct(data) = &input.data else { return Err(syn::Error::new( @@ -81,38 +112,118 @@ fn single_field(input: &DeriveInput) -> syn::Result<(syn::Type, TokenStream, Opt } /// Validates that `#[buffa(remote = ...)]` is present and its value parses as -/// a type (catching typos), without using the parsed type for codegen — see -/// [`parse`] for why. -fn require_remote_attr(input: &DeriveInput) -> syn::Result<()> { +/// a type (catching typos, without using the parsed type for codegen — see +/// [`parse`] for why), and collects any of `allowed_overrides` present +/// alongside it (e.g. `#[buffa(remote = ..., into_inner = MyType::unwrap)]`). +fn parse_overrides( + input: &DeriveInput, + allowed_overrides: &[&str], +) -> syn::Result> { + let mut overrides = HashMap::new(); + let mut has_remote = false; for attr in &input.attrs { if !attr.path().is_ident("buffa") { continue; } - let mut found = false; attr.parse_nested_meta(|meta| { if meta.path.is_ident("remote") { let _: syn::Type = meta.value()?.parse()?; - found = true; + has_remote = true; + Ok(()) + } else if let Some(key) = allowed_overrides + .iter() + .find(|key| meta.path.is_ident(*key)) + { + let path: syn::Path = meta.value()?.parse()?; + overrides.insert((*key).to_string(), path); Ok(()) } else { - Err(meta.error("unsupported `buffa` attribute key, expected `remote`")) + Err(meta.error(format!( + "unsupported `buffa` attribute key, expected `remote`{}", + allowed_overrides + .iter() + .map(|k| format!(" or `{k}`")) + .collect::() + ))) } })?; - if found { - return Ok(()); - } } - Err(syn::Error::new( - input.span(), - "missing `#[buffa(remote = ...)]` naming the foreign type this newtype wraps", - )) + if !has_remote { + return Err(syn::Error::new( + input.span(), + "missing `#[buffa(remote = ...)]` naming the foreign type this newtype wraps", + )); + } + Ok(overrides) +} + +/// Requires the struct to have exactly one type parameter and returns it — +/// used by derives whose element type is the struct's sole generic parameter +/// (`ProtoList`'s element, `ProtoBox`'s pointee). A struct with more +/// than one type parameter (e.g. a custom hasher parameter) is out of scope; +/// hand-write the impl in that case. Lifetime and const generics don't count +/// toward the limit. +pub fn single_type_param(generics: &syn::Generics) -> syn::Result { + match type_params(generics).as_slice() { + [single] => Ok(single.clone()), + _ => Err(syn::Error::new_spanned( + &generics.params, + "this derive requires exactly one type parameter", + )), + } +} + +/// Like [`single_type_param`], but for derives keyed on two type parameters +/// in declaration order (`MapStorage`'s `Key`, `Value`). +pub fn two_type_params(generics: &syn::Generics) -> syn::Result<(syn::Ident, syn::Ident)> { + match type_params(generics).as_slice() { + [key, value] => Ok((key.clone(), value.clone())), + _ => Err(syn::Error::new_spanned( + &generics.params, + "this derive requires exactly two type parameters, the map's key and value types, \ + in that order", + )), + } +} + +fn type_params(generics: &syn::Generics) -> Vec { + generics + .params + .iter() + .filter_map(|p| match p { + GenericParam::Type(t) => Some(t.ident.clone()), + _ => None, + }) + .collect() } /// Renders a `::method` fully-qualified call path, for /// disambiguating which impl a generated body invokes. -pub fn qualified_call(remote_ty: &syn::Type, trait_path: TokenStream, method: &str) -> TokenStream { +pub fn qualified_call(field_ty: &syn::Type, trait_path: TokenStream, method: &str) -> TokenStream { let method = syn::Ident::new(method, proc_macro2::Span::call_site()); - quote! { <#remote_ty as #trait_path>::#method } + quote! { <#field_ty as #trait_path>::#method } +} + +/// Resolves an overridable inherent-method call path: the user's +/// `#[buffa(remote = ..., key = path)]` override if present, otherwise +/// `::default_method`. Not built through `syn::Path` — a leading +/// `::` qualified-self segment isn't valid plain-`Path` syntax, only +/// valid as a qualified-path *expression*, so the default has to be assembled +/// as a `TokenStream` directly rather than round-tripped through `syn::Path` +/// like the override is. +pub fn overridable_call( + overrides: &HashMap, + key: &str, + field_ty: &syn::Type, + default_method: &str, +) -> TokenStream { + match overrides.get(key) { + Some(path) => quote! { #path }, + None => { + let method = syn::Ident::new(default_method, proc_macro2::Span::call_site()); + quote! { <#field_ty>::#method } + } + } } impl RemoteField { diff --git a/buffa-remote-derive/src/string.rs b/buffa-remote-derive/src/string.rs index a5bec9f..7cc8916 100644 --- a/buffa-remote-derive/src/string.rs +++ b/buffa-remote-derive/src/string.rs @@ -9,19 +9,19 @@ pub fn derive(input: DeriveInput) -> syn::Result { let RemoteField { ident, generics, - remote_ty, + field_ty, accessor, .. } = &remote; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let from_string = remote_field::qualified_call( - remote_ty, + field_ty, quote! { ::core::convert::From<::buffa::alloc::string::String> }, "from", ); let from_str = - remote_field::qualified_call(remote_ty, quote! { ::core::convert::From<&str> }, "from"); + remote_field::qualified_call(field_ty, quote! { ::core::convert::From<&str> }, "from"); let ctor_from_string = remote.construct(quote! { #from_string(s) }); let ctor_from_str = remote.construct(quote! { #from_str(s) }); diff --git a/buffa-remote-derive/tests/map_storage.rs b/buffa-remote-derive/tests/map_storage.rs new file mode 100644 index 0000000..b458042 --- /dev/null +++ b/buffa-remote-derive/tests/map_storage.rs @@ -0,0 +1,97 @@ +use buffa::MapStorage; +use buffa_remote_derive::MapStorage as DeriveMapStorage; +use indexmap::IndexMap; + +#[derive(Clone, PartialEq, Debug, DeriveMapStorage)] +#[buffa(remote = indexmap::IndexMap)] +struct MyIndexMap(pub IndexMap); + +impl Default for MyIndexMap { + fn default() -> Self { + Self(IndexMap::new()) + } +} + +impl FromIterator<(K, V)> for MyIndexMap { + fn from_iter>(iter: I) -> Self { + Self(IndexMap::from_iter(iter)) + } +} + +#[test] +fn insert_len_and_clear() { + let mut map = MyIndexMap::::default(); + assert_eq!(map.storage_len(), 0); + map.storage_insert(1, "one"); + map.storage_insert(2, "two"); + assert_eq!(map.storage_len(), 2); + map.storage_clear(); + assert_eq!(map.storage_len(), 0); +} + +#[test] +fn iter_preserves_insertion_order() { + let map: MyIndexMap = [(2, "two"), (1, "one")].into_iter().collect(); + let keys: Vec = map.storage_iter().map(|(k, _)| *k).collect(); + assert_eq!(keys, vec![2, 1]); +} + +#[test] +fn insert_overwrites_last_write_wins() { + let mut map = MyIndexMap::::default(); + map.storage_insert(1, "first"); + map.storage_insert(1, "second"); + assert_eq!(map.storage_len(), 1); + assert_eq!(map.storage_iter().next(), Some((&1, &"second"))); +} + +// A map with non-conventional method names, exercising the `len`/`insert`/ +// `clear`/`iter` attribute overrides instead of the defaults. +#[derive(Clone, PartialEq, Debug)] +struct OddMap(std::collections::BTreeMap); +impl OddMap { + fn count(&self) -> usize { + self.0.len() + } + fn put(&mut self, key: K, value: V) { + self.0.insert(key, value); + } + fn wipe(&mut self) { + self.0.clear(); + } + fn entries(&self) -> impl Iterator { + self.0.iter() + } +} + +#[derive(Clone, PartialEq, Debug, DeriveMapStorage)] +#[buffa( + remote = std::collections::BTreeMap, + len = OddMap::count, + insert = OddMap::put, + clear = OddMap::wipe, + iter = OddMap::entries +)] +struct MyOddMap(pub OddMap); + +impl Default for MyOddMap { + fn default() -> Self { + Self(OddMap(std::collections::BTreeMap::new())) + } +} + +impl FromIterator<(K, V)> for MyOddMap { + fn from_iter>(iter: I) -> Self { + Self(OddMap(std::collections::BTreeMap::from_iter(iter))) + } +} + +#[test] +fn overridden_method_names_are_used() { + let mut map = MyOddMap::::default(); + map.storage_insert(1, "one"); + assert_eq!(map.storage_len(), 1); + assert_eq!(map.storage_iter().next(), Some((&1, &"one"))); + map.storage_clear(); + assert_eq!(map.storage_len(), 0); +} diff --git a/buffa-remote-derive/tests/proto_box.rs b/buffa-remote-derive/tests/proto_box.rs new file mode 100644 index 0000000..68eaef2 --- /dev/null +++ b/buffa-remote-derive/tests/proto_box.rs @@ -0,0 +1,54 @@ +use buffa::ProtoBox; +use buffa_remote_derive::ProtoBox as DeriveProtoBox; + +#[derive(Clone, PartialEq, Debug, DeriveProtoBox)] +#[buffa(remote = smallbox::SmallBox)] +struct MyBox(pub smallbox::SmallBox); + +#[test] +fn new_and_into_inner_round_trip() { + let boxed = MyBox::new(42i64); + assert_eq!(*boxed, 42); + assert_eq!(boxed.into_inner(), 42); +} + +#[test] +fn deref_mut_writes_through() { + let mut boxed = MyBox::new(String::from("hi")); + boxed.push_str(" there"); + assert_eq!(boxed.into_inner(), "hi there"); +} + +// A pointer with non-conventional method names, exercising the +// `new`/`into_inner` attribute overrides instead of the defaults. +struct Holder(T); +impl Holder { + fn wrap(value: T) -> Self { + Self(value) + } + fn unwrap(self) -> T { + self.0 + } +} +impl core::ops::Deref for Holder { + type Target = T; + fn deref(&self) -> &T { + &self.0 + } +} +impl core::ops::DerefMut for Holder { + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +#[derive(DeriveProtoBox)] +#[buffa(remote = Holder, new = Holder::wrap, into_inner = Holder::unwrap)] +struct MyHolder(pub Holder); + +#[test] +fn overridden_method_names_are_used() { + let h = MyHolder::new(7i32); + assert_eq!(*h, 7); + assert_eq!(h.into_inner(), 7); +}