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

2

17

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"),
}
Wrac answered 20/3, 2019 at 23:56 Comment(1)
One way to achieve a similar effect by relying on the actual type system might be to connect Option<T> back to T by using a trait, for example: impl<T> MyTrait for Option<T> { type Assoc = T; }. Then your macro can expand to set_bar(&mut self, v: <Option<String> as MyTrait>::Assoc)Blaise
B
12

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.

Breastwork answered 21/3, 2019 at 9:34 Comment(4)
Thank you, but just to say that typepath.path.is_ident("Option") is false. is_ident checks for self.leading_colon.is_none() && self.segments.len() == 1 && self.segments[0].arguments.is_none() && self.segments[0].ident == ident and self.segments[0].arguments.is_none() is falseIntracellular
@ĐorđeZeljić I think that's better now. Once again, I cannot test it right now. Also, this can be refactored to be nicer, but I give you a general idea of how to do it.Breastwork
You cannot handle those arbitrary names — this is the true key.Cominform
@Breastwork would this be easier with syn::visit?Timberland
P
16

My updated version of the response from @Boiethios, tested and used in a public crate, with support of several syntaxes for Option:

  • Option
  • std::option::Option
  • ::std::option::Option
  • core::option::Option
  • ::core::option::Option
fn extract_type_from_option(ty: &syn::Type) -> Option<&syn::Type> {
    use syn::{GenericArgument, Path, PathArguments, PathSegment};

    fn extract_type_path(ty: &syn::Type) -> Option<&Path> {
        match *ty {
            syn::Type::Path(ref typepath) if typepath.qself.is_none() => Some(&typepath.path),
            _ => None,
        }
    }

    // TODO store (with lazy static) the vec of string
    // TODO maybe optimization, reverse the order of segments
    fn extract_option_segment(path: &Path) -> Option<&PathSegment> {
        let idents_of_path = path
            .segments
            .iter()
            .into_iter()
            .fold(String::new(), |mut acc, v| {
                acc.push_str(&v.ident.to_string());
                acc.push('|');
                acc
            });
        vec!["Option|", "std|option|Option|", "core|option|Option|"]
            .into_iter()
            .find(|s| &idents_of_path == *s)
            .and_then(|_| path.segments.last())
    }

    extract_type_path(ty)
        .and_then(|path| extract_option_segment(path))
        .and_then(|path_seg| {
            let type_params = &path_seg.arguments;
            // It should have only on angle-bracketed param ("<String>"):
            match *type_params {
                PathArguments::AngleBracketed(ref params) => params.args.first(),
                _ => None,
            }
        })
        .and_then(|generic_arg| match *generic_arg {
            GenericArgument::Type(ref ty) => Some(ty),
            _ => None,
        })
}
Perigynous answered 22/5, 2019 at 19:51 Comment(2)
this is what I call a time-saver ! much thanks @DavidHeaddress
I think there is no need of vec!. Slice also can be turned into an iterator.Arbiter
B
12

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.

Breastwork answered 21/3, 2019 at 9:34 Comment(4)
Thank you, but just to say that typepath.path.is_ident("Option") is false. is_ident checks for self.leading_colon.is_none() && self.segments.len() == 1 && self.segments[0].arguments.is_none() && self.segments[0].ident == ident and self.segments[0].arguments.is_none() is falseIntracellular
@ĐorđeZeljić I think that's better now. Once again, I cannot test it right now. Also, this can be refactored to be nicer, but I give you a general idea of how to do it.Breastwork
You cannot handle those arbitrary names — this is the true key.Cominform
@Breastwork would this be easier with syn::visit?Timberland

© 2022 - 2024 — McMap. All rights reserved.