Skip to content

Commit

Permalink
Merge pull request #186 from max-heller/postcard-crate
Browse files Browse the repository at this point in the history
Add #[postcard(crate = ...)] attribute for derive(Schema)
  • Loading branch information
jamesmunns authored Nov 27, 2024
2 parents 448ebb8 + 94dd9d3 commit bcbb917
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 92 deletions.
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
},
])
}
);
}
}

0 comments on commit bcbb917

Please sign in to comment.