Skip to content
New issue

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

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

Already on GitHub? # to your account

Add #[postcard(crate = ...)] attribute for derive(Schema) #186

Merged
merged 2 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion source/postcard-derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ proc-macro = true
all-features = true

[dependencies]
syn = "1.0"
syn = "2.0"
quote = "1.0"
proc-macro2 = "1.0"
2 changes: 1 addition & 1 deletion source/postcard-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ pub fn derive_max_size(item: proc_macro::TokenStream) -> proc_macro::TokenStream
}

/// Derive the `postcard_schema::Schema` trait for a struct or enum.
#[proc_macro_derive(Schema)]
#[proc_macro_derive(Schema, attributes(postcard))]
pub fn derive_schema(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
schema::do_derive_schema(item)
}
227 changes: 136 additions & 91 deletions source/postcard-derive/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,138 +2,183 @@ use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::{
parse_macro_input, parse_quote, spanned::Spanned, Data, DeriveInput, Fields, GenericParam,
Generics,
Generics, Path,
};

pub fn do_derive_schema(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(item as DeriveInput);

let span = input.span();
let name = input.ident;
let name = &input.ident;

let generator = match Generator::new(&input) {
Ok(generator) => generator,
Err(err) => return err.into_compile_error().into(),
};

// Add a bound `T: Schema` to every type parameter T.
let generics = add_trait_bounds(input.generics);
let generics = generator.add_trait_bounds(input.generics);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

let ty = generate_type(&input.data, span, name.to_string())
let ty = generator
.generate_type(&input.data, span, name.to_string())
.unwrap_or_else(syn::Error::into_compile_error);

let postcard_schema = &generator.postcard_schema;
let expanded = quote! {
impl #impl_generics ::postcard_schema::Schema for #name #ty_generics #where_clause {
const SCHEMA: &'static ::postcard_schema::schema::NamedType = #ty;
impl #impl_generics #postcard_schema::Schema for #name #ty_generics #where_clause {
const SCHEMA: &'static #postcard_schema::schema::NamedType = #ty;
}
};

expanded.into()
}

fn generate_type(data: &Data, span: Span, name: String) -> Result<TokenStream, syn::Error> {
let ty = match data {
Data::Struct(data) => generate_struct(&data.fields),
Data::Enum(data) => {
let name = data.variants.iter().map(|v| v.ident.to_string());
let ty = data.variants.iter().map(|v| generate_variants(&v.fields));

quote! {
&::postcard_schema::schema::DataModelType::Enum(&[
#( &::postcard_schema::schema::NamedVariant { name: #name, ty: #ty } ),*
])
struct Generator {
postcard_schema: Path,
}

impl Generator {
fn new(input: &DeriveInput) -> syn::Result<Self> {
let mut generator = Self {
postcard_schema: parse_quote!(::postcard_schema),
};
for attr in &input.attrs {
if attr.path().is_ident("postcard") {
attr.parse_nested_meta(|meta| {
// #[postcard(crate = path::to::postcard)]
if meta.path.is_ident("crate") {
generator.postcard_schema = meta.value()?.parse()?;
return Ok(());
}

Err(meta.error("unsupported #[postcard] attribute"))
})?;
}
}
Data::Union(_) => {
return Err(syn::Error::new(
span,
"unions are not supported by `postcard::experimental::schema`",
))
}
};
Ok(generator)
}

Ok(quote! {
&::postcard_schema::schema::NamedType {
name: #name,
ty: #ty,
}
})
}
fn generate_type(
&self,
data: &Data,
span: Span,
name: String,
) -> Result<TokenStream, syn::Error> {
let postcard_schema = &self.postcard_schema;
let ty = match data {
Data::Struct(data) => self.generate_struct(&data.fields),
Data::Enum(data) => {
let name = data.variants.iter().map(|v| v.ident.to_string());
let ty = data
.variants
.iter()
.map(|v| self.generate_variants(&v.fields));

fn generate_struct(fields: &Fields) -> TokenStream {
match fields {
syn::Fields::Named(fields) => {
let fields = fields.named.iter().map(|f| {
quote! {
&#postcard_schema::schema::DataModelType::Enum(&[
#( &#postcard_schema::schema::NamedVariant { name: #name, ty: #ty } ),*
])
}
}
Data::Union(_) => {
return Err(syn::Error::new(
span,
"unions are not supported by `postcard::experimental::schema`",
))
}
};

Ok(quote! {
&#postcard_schema::schema::NamedType {
name: #name,
ty: #ty,
}
})
}

fn generate_struct(&self, fields: &Fields) -> TokenStream {
let postcard_schema = &self.postcard_schema;
match fields {
syn::Fields::Named(fields) => {
let fields = fields.named.iter().map(|f| {
let ty = &f.ty;
let name = f.ident.as_ref().unwrap().to_string();
quote_spanned!(f.span() => &::postcard_schema::schema::NamedValue { name: #name, ty: <#ty as ::postcard_schema::Schema>::SCHEMA })
quote_spanned!(f.span() => &#postcard_schema::schema::NamedValue { name: #name, ty: <#ty as #postcard_schema::Schema>::SCHEMA })
});
quote! { &::postcard_schema::schema::DataModelType::Struct(&[
#( #fields ),*
]) }
}
syn::Fields::Unnamed(fields) => {
if fields.unnamed.len() == 1 {
let f = fields.unnamed[0].clone();
let ty = &f.ty;
let qs = quote_spanned!(f.span() => <#ty as ::postcard_schema::Schema>::SCHEMA);

quote! { &::postcard_schema::schema::DataModelType::NewtypeStruct(#qs) }
} else {
let fields = fields.unnamed.iter().map(|f| {
let ty = &f.ty;
quote_spanned!(f.span() => <#ty as ::postcard_schema::Schema>::SCHEMA)
});
quote! { &::postcard_schema::schema::DataModelType::TupleStruct(&[
quote! { &#postcard_schema::schema::DataModelType::Struct(&[
#( #fields ),*
]) }
}
}
syn::Fields::Unit => {
quote! { &::postcard_schema::schema::DataModelType::UnitStruct }
syn::Fields::Unnamed(fields) => {
if fields.unnamed.len() == 1 {
let f = fields.unnamed[0].clone();
let ty = &f.ty;
let qs = quote_spanned!(f.span() => <#ty as #postcard_schema::Schema>::SCHEMA);

quote! { &#postcard_schema::schema::DataModelType::NewtypeStruct(#qs) }
} else {
let fields = fields.unnamed.iter().map(|f| {
let ty = &f.ty;
quote_spanned!(f.span() => <#ty as #postcard_schema::Schema>::SCHEMA)
});
quote! { &#postcard_schema::schema::DataModelType::TupleStruct(&[
#( #fields ),*
]) }
}
}
syn::Fields::Unit => {
quote! { &#postcard_schema::schema::DataModelType::UnitStruct }
}
}
}
}

fn generate_variants(fields: &Fields) -> TokenStream {
match fields {
syn::Fields::Named(fields) => {
let fields = fields.named.iter().map(|f| {
fn generate_variants(&self, fields: &Fields) -> TokenStream {
let postcard_schema = &self.postcard_schema;
match fields {
syn::Fields::Named(fields) => {
let fields = fields.named.iter().map(|f| {
let ty = &f.ty;
let name = f.ident.as_ref().unwrap().to_string();
quote_spanned!(f.span() => &::postcard_schema::schema::NamedValue { name: #name, ty: <#ty as ::postcard_schema::Schema>::SCHEMA })
quote_spanned!(f.span() => &#postcard_schema::schema::NamedValue { name: #name, ty: <#ty as #postcard_schema::Schema>::SCHEMA })
});
quote! { &::postcard_schema::schema::DataModelVariant::StructVariant(&[
#( #fields ),*
]) }
}
syn::Fields::Unnamed(fields) => {
if fields.unnamed.len() == 1 {
let f = fields.unnamed[0].clone();
let ty = &f.ty;
let qs = quote_spanned!(f.span() => <#ty as ::postcard_schema::Schema>::SCHEMA);

quote! { &::postcard_schema::schema::DataModelVariant::NewtypeVariant(#qs) }
} else {
let fields = fields.unnamed.iter().map(|f| {
let ty = &f.ty;
quote_spanned!(f.span() => <#ty as ::postcard_schema::Schema>::SCHEMA)
});
quote! { &::postcard_schema::schema::DataModelVariant::TupleVariant(&[
quote! { &#postcard_schema::schema::DataModelVariant::StructVariant(&[
#( #fields ),*
]) }
}
}
syn::Fields::Unit => {
quote! { &::postcard_schema::schema::DataModelVariant::UnitVariant }
syn::Fields::Unnamed(fields) => {
if fields.unnamed.len() == 1 {
let f = fields.unnamed[0].clone();
let ty = &f.ty;
let qs = quote_spanned!(f.span() => <#ty as #postcard_schema::Schema>::SCHEMA);

quote! { &#postcard_schema::schema::DataModelVariant::NewtypeVariant(#qs) }
} else {
let fields = fields.unnamed.iter().map(|f| {
let ty = &f.ty;
quote_spanned!(f.span() => <#ty as #postcard_schema::Schema>::SCHEMA)
});
quote! { &#postcard_schema::schema::DataModelVariant::TupleVariant(&[
#( #fields ),*
]) }
}
}
syn::Fields::Unit => {
quote! { &#postcard_schema::schema::DataModelVariant::UnitVariant }
}
}
}
}

/// Add a bound `T: MaxSize` to every type parameter T.
fn add_trait_bounds(mut generics: Generics) -> Generics {
for param in &mut generics.params {
if let GenericParam::Type(ref mut type_param) = *param {
type_param
.bounds
.push(parse_quote!(::postcard_schema::Schema));
/// Add a bound `T: MaxSize` to every type parameter T.
fn add_trait_bounds(&self, mut generics: Generics) -> Generics {
let postcard_schema = &self.postcard_schema;
for param in &mut generics.params {
if let GenericParam::Type(ref mut type_param) = *param {
type_param
.bounds
.push(parse_quote!(#postcard_schema::Schema));
}
}
generics
}
generics
}
66 changes: 66 additions & 0 deletions source/postcard-schema/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,39 @@
pub mod impls;
pub mod schema;

/// Derive [`Schema`] for a struct or enum
///
/// # Examples
///
/// ```
/// use postcard_schema::Schema;
///
/// #[derive(Schema)]
/// struct Point {
/// x: i32,
/// y: i32,
/// }
/// ```
///
/// # Attributes
///
/// ## `#[postcard(crate = ...)]`
///
/// The `#[postcard(crate = ...)]` attribute can be used to specify a path to the `postcard_schema`
/// crate instance to use when referring to [`Schema`] and [schema types](schema) from generated
/// code. This is normally only applicable when invoking re-exported derives from a different crate.
///
/// ```
/// # use postcard_schema::Schema;
/// use postcard_schema as reexported_postcard_schema;
///
/// #[derive(Schema)]
/// #[postcard(crate = reexported_postcard_schema)]
/// struct Point {
/// x: i32,
/// y: i32,
/// }
/// ```
#[cfg(feature = "derive")]
pub use postcard_derive::Schema;

Expand All @@ -15,3 +48,36 @@ pub trait Schema {
/// type.
const SCHEMA: &'static schema::NamedType;
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn crate_path() {
#[allow(unused)]
#[derive(Schema)]
#[postcard(crate = crate)]
struct Point {
x: i32,
y: i32,
}

assert_eq!(
Point::SCHEMA,
&schema::NamedType {
name: "Point",
ty: &schema::DataModelType::Struct(&[
&schema::NamedValue {
name: "x",
ty: i32::SCHEMA
},
&schema::NamedValue {
name: "y",
ty: i32::SCHEMA
},
])
}
);
}
}