Need help with my first derive proc-macro: processing struct field types

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).

Looks like it might help you to know that <String>::from_value and <Option<i32>>::from_value are valid syntax.

Perfect! Thank you, that solved the problem I had.

To avoid any potential ambiguities, you might even want to use fully-qualified syntax like

<#stype as dxr::FromValue<#name>>::from_value
2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.