Search code examples
rustmetaprogrammingrust-proc-macros

How can I get the T from an Option<T> when using syn?


I'm using syn to parse Rust code. When I read a named field's type using field.ty, I get a syn::Type. When I print it using quote!{#ty}.to_string() I get "Option<String>".

How can I get just "String"? I want to use #ty in quote! to print "String" instead of "Option<String>".

I want to generate code like:

impl Foo {
    pub set_bar(&mut self, v: String) {
        self.bar = Some(v);
    }
}

starting from

struct Foo {
    bar: Option<String>
}

My attempt:

let ast: DeriveInput = parse_macro_input!(input as DeriveInput);

let data: Data = ast.data;

match data {
    Data::Struct(ref data) => match data.fields {
        Fields::Named(ref fields) => {

            fields.named.iter().for_each(|field| {
                let name = &field.ident.clone().unwrap();

                let ty = &field.ty;
                quote!{
                    impl Foo {
                        pub set_bar(&mut self, v: #ty) {
                            self.bar = Some(v);
                        }
                    }
                };      
            });
        }
        _ => {}
    },
    _ => panic!("You can derive it only from struct"),
}

Solution

  • You should do something like this untested example:

    use syn::{GenericArgument, PathArguments, Type};
    
    fn extract_type_from_option(ty: &Type) -> Type {
        fn path_is_option(path: &Path) -> bool {
            leading_colon.is_none()
                && path.segments.len() == 1
                && path.segments.iter().next().unwrap().ident == "Option"
        }
    
        match ty {
            Type::Path(typepath) if typepath.qself.is_none() && path_is_option(typepath.path) => {
                // Get the first segment of the path (there is only one, in fact: "Option"):
                let type_params = typepath.path.segments.iter().first().unwrap().arguments;
                // It should have only on angle-bracketed param ("<String>"):
                let generic_arg = match type_params {
                    PathArguments::AngleBracketed(params) => params.args.iter().first().unwrap(),
                    _ => panic!("TODO: error handling"),
                };
                // This argument must be a type:
                match generic_arg {
                    GenericArgument::Type(ty) => ty,
                    _ => panic!("TODO: error handling"),
                }
            }
            _ => panic!("TODO: error handling"),
        }
    }
    

    There's not many things to explain, it just "unrolls" the diverse components of a type:

    Type -> TypePath -> Path -> PathSegment -> PathArguments -> AngleBracketedGenericArguments -> GenericArgument -> Type.

    If there is an easier way to do that, I would be happy to know it.


    Note that since syn is a parser, it works with tokens. You cannot know for sure that this is an Option. The user could, for example, type std::option::Option, or write type MaybeString = std::option::Option<String>;. You cannot handle those arbitrary names.