Using syn::Type to get the name (or some other identifier) from field

Hi all,

To start off with the context. I am writing a fuzzy-importer that lets me import .toml files from disk into a given struct whilst allowing fields to be missing (or for extra fields to be present).

Since I don't want to implement a fuzzy function for each struct by hand, I thought I'd try my hand at using a macro to automagically derive the function.

As the base format, I am using toml::Table as an intermediary since it retains all the data, whilst not requiring me to specify a struct when loading in a file.

To read in the keys I am using the toml::Table.get() function which returns a Value type.
The issue is that each type has it's own fetch method.
e.g. .as_bool(), .as_integer(), .as_str() and .as_float()

Which brings me to the question.
Since I'm using the syn crate, I theoretically have all the information about the struct necessary to assign the proper conversion method to each type.
The problem is that I don't know how to get that information out of the syn::Type type.

Long story short, I would like a struct like this:

struct MyStruct {
    my_int: i32,
    my_str: String,
    my_other_struct: OtherStruct
}

struct OtherStruct {
    str_1: String,
    str_2: String
}

To generate the following reading code:

impl ReadTable for MyStruct {
    fn read_data(table: toml::Table) -> Self {
        Self {
            my_int: table.get("my_int").unwrap_or_default().as_integer(),
            my_str: table.get("my_str").unwrap_or_default().as_str().to_string(),
            my_other_struct: OtherStruct::read_data(table.get("my_other_struct").unwrap()),
        }
    }
}

impl ReadTable for OtherStruct {
    fn read_data(table: toml::Table) -> Self {
        Self {
            str_1: table.get("str_1").unwrap_or_default().as_str().to_string(),
            str_2: table.get("str_2").unwrap_or_default().as_str().to_string(),
        }
    }
}

I should add that I have 90% of this code done, the only thing missing is how to distinguish which .as_x() function I should use.

Thank you in advance for any suggestions!

this is not true. macros in rust only have the syntactical representation of the source code, not type information. for example, in the following snippet, my_macro can only see the struct has a field of type MyCounter, but it's impossible to know MyCounter is an alias to u32:

// this module could be from an external dependency crate
pub mod my_types {
  type MyCounter = u32;
}
use my_types::MyCounter:
#[my_macro]
struct MyStruct {
    pub counter: MyCounter
}

that being said, if the code your macro parses are known to only use toml compatible types directly (i.e. without any indirection such as type aliases, associated types, macros in type positions, etc), I think the Type::Path variant is probably what you're interested in.

note, for built-in types like i32 or bool, the path should contain a single segment (see Path::is_ident()), but for types defined in libraries like String or Vec, the source code might use different paths for the same type, e.g. String vs alloc::string::String vs std::string::String, you should keep this in mind when you write your macro.

2 Likes

just checked the toml crate, and it turns out toml::Value already implements the conversion to the types you need, see Value::try_into()

so your macro can simply generate code that looks like this:

impl ReadTable for MyStruct {
    fn read_data(table: toml::Table) -> Self {
        Self {
            my_int: table.get("my_int").expect("my_int missing").try_into().expect("my_int wrong type"),
            my_str: table.get("my_str").expect("my_str missing").try_into().expect("my_str wrong type"),
            my_other_struct: OtherStruct::read_data(table.get("my_other_struct").unwrap()),
        }
    }
}

or better yet, just derive Deserialize for your struct and let Table::try_into() do the work for you:

#[derive(Deserialize)]
struct MyStruct { ... }
#[derive(Deserialize)]
struct OtherStruct { ... }
impl ReadTable for MyStruct {
    fn read_data(table: toml::Table) -> Self {
        table.try_into().expect("MyStruct invalid")
    }
}
2 Likes

@nerditation One of the saving graces is that the files will only have toml compatible types, or types I define myself (like OtherStruct) so I think the Path method you mentioned might just work!
I'll give that a go.

For the .try_into() since the field does not necessarily have to be present (e.g. in the file my_int might not exist) I can not use expect but need to use something like .unwrap_or_default(). Somehow, this function prevents me from using .into().

This is what I currently have:

#ident: table.get(stringify!(#ident)).unwrap_or_default().into()

Where #ident would be i32 or some other type.
The problem is that the Value type does not have a default type.
Though, as I am typing this, I realize I could just write a custom wrapper to get a default type.
I'll have two possible solutions now at least! Thank you!

when deriving Deserialize, you can add a default attribute to the field to provide a "handler" function for a missing value. see:

https://serde.rs/attr-default.html

you can also refer to primitives like ::core::primitive::u32, which you might want to do in macro generated code to make sure it can't be shadowed.

1 Like

well, although I know primititive types are not reserved keywords (like C++), I didn't know you can shadow primitive types. I always assumed they are somewhat special. now I know. thanks.

You can kinda do this with a syn::Data or syn::DataStruct, but a syn::Type won't contain the necessary informations to do this.

Hi, thank you for your reply!
What do you mean when you say syn::Data and syn::DataStruct contain the data?
I have looked through the docs and could not find a way to use these to get a type identifier out of them. That said, I wouldn't be surprised if I missed something

The fields field of DataStruct contains a list of fields, each with its name and type. Given this you can generate the tokens for the ReadTable implementation.

Ah okay I see, I think we're talking about the same thing then.
I am getting the "Type" type through the following path: DataStruct -> Fields -> FieldsNamed -> Field -> Type but I'm unable to get concrete information out of this Type type.
Or did you mean something different?

Which kind of concrete informations? With macros you can only know which tokens the user typed.

@nerditation @SkiFire13 thank you for your help!
I ended up going with @nerditation 's suggestion to use Type::Path.
The code is quite messy, but it does what I need it to, so cleaning it up fortunately can wait for later.

This is what I ended up putting into the macro:

let mut type_path: Vec<String> = match field.ty {
    syn::Type::Path(path) => {
        path.into_token_stream().into_iter().filter_map(|e| {
            let out = e.to_string();

            if out == "<" || out == ">" {
                return None;
            } else {
                return Some(out);
            }
        }).collect()
    },
    _ => panic!("Type '{}' has no path", field.ty.to_token_stream())
};

with the above type_path being read using this code:

let field_type = type_path.last().expect(&format!("No type found for field: {ident}"));

let conversion = if field_type == "String" {
    if is_vec {
        quote!{ value.as_array().unwrap().iter().map(|item| item.as_str().unwrap().to_owned() ).collect() }
    } else {
        quote!{ value.as_str().unwrap().to_owned() }
    }
} else if field_type == "PathBuf" {
    quote!{ value.as_str().unwrap().to_owned().into() }
} else {
    let typeident = proc_macro2::Ident::new(&field_type, proc_macro2::Span::call_site());

    if is_vec {
        quote!{ value.as_array().unwrap().iter().map(|i| #typeident::read_data(i.as_table().unwrap()) ).collect() }
    } else {
        quote!{ #typeident::read_data(&i.as_table().unwrap()) }
    }
};

Again, not the cleanest solution, but it covers all my current cases and should be easy (enough) to expand for future cases.

Thank you again!