Skip to content

Instantly share code, notes, and snippets.

@ytaras
Created January 16, 2020 09:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ytaras/042c40adc8e29f30302d709df7e1d067 to your computer and use it in GitHub Desktop.
Save ytaras/042c40adc8e29f30302d709df7e1d067 to your computer and use it in GitHub Desktop.
[package]
name = "specs_saveload_derive"
edition = "2018"
[dependencies]
specs = { version = "0.15.1", features = ["serde", "shred-derive"] }
quote = "1.0.2"
syn = { version = "1.0.13", features = ["full", "extra-traits"] }
proc-macro2 = "1.0.7"
[lib]
proc-macro = true
extern crate proc_macro;
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::{format_ident, quote};
use syn::punctuated::Punctuated;
use syn::{
parse_macro_input, parse_quote, AngleBracketedGenericArguments, Data, DataStruct, DeriveInput,
Fields, FieldsNamed, FieldsUnnamed, GenericArgument, Ident, Pat, Path, PathArguments,
PathSegment, Type, TypePath,
};
#[proc_macro_derive(SerializeComponents)]
pub fn derive_components_serialize(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let (struct_name, field_name, component_type) = parse_struct(&input);
let impl_module = format_ident!("__SER_IMPL_FOR_{}", struct_name);
let deconstruct: Pat = deconstruct_pattern(&input, &field_name);
let result = quote! {
#[allow(non_snake_case)]
mod #impl_module {
#[derive(serde::Serialize)]
pub struct SerDeData(
#(
pub Option<super::#component_type>
),*
);
}
#[automatically_derived]
impl<'a, E, M> specs::saveload::SerializeComponents<E, M> for #struct_name<'a>
where M: specs::saveload::Marker,
#(E: From<<#component_type as specs::saveload::ConvertSaveload<M>>::Error>),*
{
type Data = #impl_module::SerDeData;
fn serialize_entity<F>(&self, entity: specs::prelude::Entity, mut ids: F)
-> std::result::Result<<Self as specs::saveload::SerializeComponents<E, M>>::Data, E>
where
F: std::ops::FnMut(specs::prelude::Entity) -> std::option::Option<M>
{
use specs::saveload::ConvertSaveload;
let #deconstruct = self;
#(
let #field_name = #field_name
.get(entity)
.map(|c| c.convert_into(&mut ids).map(Some))
.unwrap_or(Ok(None))?;
)*
Ok(#impl_module::SerDeData (
#(#field_name),*
))
}
}
};
TokenStream::from(result)
}
#[proc_macro_derive(DeserializeComponents)]
pub fn derive_components_deserialize(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let (struct_name, field_name, component_type) = parse_struct(&input);
let data_name = field_name
.iter()
.map(|x| format_ident!("{}_data", x))
.collect::<Vec<_>>();
let deconstruct_storage = deconstruct_pattern(&input, &field_name);
let impl_module = format_ident!("__DE_IMPL_FOR_{}", struct_name);
let result = quote! {
#[allow(non_snake_case)]
mod #impl_module {
#[derive(serde::Deserialize)]
pub struct DeData(
#(
pub Option<super::#component_type>
),*
);
}
#[automatically_derived]
impl<'a, E, M> specs::saveload::DeserializeComponents<E, M> for #struct_name<'a>
where M: specs::saveload::Marker,
E: std::fmt::Display,
E: From<specs::error::Error>,
#(E: From<<#component_type as specs::saveload::ConvertSaveload<M>>::Error>),*
{
type Data = #impl_module::DeData;
fn deserialize_entity<F>(&mut self,
entity: specs::world::Entity,
data: <Self as specs::saveload::DeserializeComponents<E, M>>::Data,
mut ids: F) -> std::result::Result<(), E>
where
F: FnMut(M) -> Option<Entity>
{
use specs::saveload::ConvertSaveload;
let #deconstruct_storage = self;
let #impl_module::DeData(#(#data_name),*) = data;
#(
if let Some(value) = #data_name {
let value = <#component_type as ConvertSaveload<M>>::convert_from(value, &mut ids)?;
#field_name.insert(entity, value)?;
}
)*
Ok(())
}
}
};
TokenStream::from(result)
}
fn deconstruct_pattern(data: &DeriveInput, field_name: &[Ident]) -> Pat {
let struct_name = &data.ident;
match &data.data {
Data::Struct(DataStruct {
fields: Fields::Named(..),
..
}) => parse_quote! {
#struct_name {
#(#field_name),*
}
},
Data::Struct(DataStruct {
fields: Fields::Unnamed(..),
..
}) => parse_quote! {
#struct_name (
#(#field_name),*
)
},
x => panic!("Expected struct, received {:?}", x),
}
}
fn deconstruct_named_fields(fields: &FieldsNamed) -> (Vec<Ident>, Vec<Type>) {
let field_name: Vec<_> = fields
.named
.iter()
.map(|x| x.ident.as_ref().cloned().expect("Only named fields"))
.collect();
let component_type = fields
.named
.iter()
.map(|f| get_component_type(&f.ty))
.collect::<Vec<_>>();
(field_name, component_type)
}
fn deconstruct_unnamed_fields(fields: &FieldsUnnamed) -> (Vec<Ident>, Vec<Type>) {
let field_name = fields
.unnamed
.iter()
.enumerate()
.map(|(i, _)| Ident::new(&format!("_{}", i), Span::call_site()))
.collect();
let field_types = fields
.unnamed
.iter()
.map(|x| get_component_type(&x.ty))
.collect();
(field_name, field_types)
}
fn parse_struct(input: &DeriveInput) -> (Ident, Vec<Ident>, Vec<Type>) {
let struct_name = &input.ident;
let fields = match &input.data {
Data::Struct(data) => &data.fields,
x => panic!("Expected struct type, received {:?}", x),
};
let (field_name, component_type) = match fields {
Fields::Named(named) => deconstruct_named_fields(&named),
Fields::Unnamed(unnamed) => deconstruct_unnamed_fields(&unnamed),
x => panic!("Expected fields, got {:?}", x),
};
(struct_name.clone(), field_name, component_type)
}
fn get_component_type(storage: &Type) -> Type {
fn type_arguments<T>(args: &Punctuated<GenericArgument, T>) -> Type {
assert_eq!(args.len(), 2);
match args.last() {
Some(GenericArgument::Type(ty)) => ty.clone(),
x => panic!("Expected type, but received {:?}", x),
}
}
match storage {
Type::Path(TypePath {
qself: None,
path:
Path {
leading_colon: None,
segments,
},
}) => match segments.last() {
Some(PathSegment {
ident,
arguments:
PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }),
}) if ident.to_string() == "ReadStorage" || ident.to_string() == "WriteStorage" => {
type_arguments(args)
}
x => panic!("Expected ReadStorage, but received {:?}", x),
},
_ => panic!("Expected type path but received {:?}", storage),
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment