This is my first (meaningfully successful) attempt at writing a proc-macro, so please bear with me.
I've been working on a crate for making XML-RPC calls easy with Rust (yes, I looked, but all existing crates for xml-rpc lack features or don't work for my use-case): https://github.com/ironthree/dxr
I've finished the methods for de/serializing XML into Values and implemented a FromValue
trait for all supported types. Now I tried implementing a derive macro, so arbitrary structs can be deserialized from Values as well.
I have a working prototype of the derive macro, but I've now hit a wall with parsing some struct fields, particularly the types. It works for all primitive types, but hits problems for generic types with type parameters.
So I have a struct field name: String
that gets filled with name: String::from_value(map.get("name").unwrap())?,
with map being deserialized from a Value::Struct
. This also works for i32
, i64
, f64
, etc.
However, this fails for types like Option<i32>
, where the macro generates something like Option<i32>::from_value(...)
(invalid syntax), but it should be Option::from_value(...)
, without the generic parameters. I can't figure out how to get the "base type" without generic parameters here, since parsing the struct with syn
matches those types as Type::Path
values, and I can't figure out how to process those other than passing them through directly.
For completeness, this is the code I have right now (on GitHub):
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, parse_quote, Data, DeriveInput, Fields, GenericParam, Type};
#[proc_macro_derive(FromValue)]
pub fn from_value(input: TokenStream) -> TokenStream {
let mut input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
for param in &mut input.generics.params {
if let GenericParam::Type(ref mut type_param) = *param {
type_param.bounds.push(parse_quote!(dxr::FromValue));
}
}
let generics = input.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let mut field_impls = Vec::new();
match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => {
for field in &fields.named {
let ident = field.ident.as_ref().unwrap();
let stype = match &field.ty {
Type::Path(v) => v,
_ => unimplemented!("Deriving FromValue not possible for field: {}", ident),
};
field_impls.push(
// FIXME 1: replace unwrap with a nice error message about a missing field
// FIXME 2: replace ? with nice error message about wrong type
// FIXME 3: this does not work for types like Option<T> and HashMap<K, V>
// where only Option::from_value and HashMap::from_value should be
// used, but I can't find a way to strip generics from the path
quote! { #ident: #stype::from_value(map.get("#ident").unwrap())?,
},
);
}
},
Fields::Unnamed(_) | Fields::Unit => unimplemented!(),
},
_ => unimplemented!(),
}
let mut fields = proc_macro2::TokenStream::new();
fields.extend(field_impls.into_iter());
let impl_block = quote! {
impl #impl_generics dxr::FromValue<#name> for #name #ty_generics #where_clause {
fn from_value(value: &::dxr_shared::Value) -> Result<#name, ()> {
use ::std::collections::HashMap;
use ::std::string::String;
use ::dxr_shared::Value;
let map: HashMap<String, Value> = HashMap::from_value(value)?;
Ok(#name {
#fields
})
}
}
};
proc_macro::TokenStream::from(impl_block)
}
And to illustrate the purpose of the derive macro, here's what it should do:
#[derive(FromValue)]
struct MyStruct {
string: String,
integer: i32,
float: f64,
optional: Option<i32>,
}
// will get this impl block
impl FromValue<MyStruct> for MyStruct {
fn from_value(value: &Value) -> Result<MyStruct, ()> {
let map: HashMap<String, Value> = HashMap::from_value(value)?;
Ok(MyStruct {
string: String::from_value(map.get("string").unwrap())?,
integer: i32::from_value(map.get("integer").unwrap())?,
float: f64::from_value(map.get("float").unwrap())?,
optional: Option<i32>::from_value(map.get("optional").unwrap())?,
})
}
}
And the problem is that the constructor for any Option<T>
(and also HashMap<K, V>) values is not valid syntax, but I don't know how to strip the <T>
from the Option<T>
syn::Type::Path
value (the FIXME 3
item in the code above).