Search code examples
rustrust-macrosrust-proc-macros

How can a procedural macro check a generic type for Option<Option<T>> and flatten it down to a single Option?


I'm writing a derive procedural macro where all of the values are converted to Options. The problem is that any Option fields in the struct can be contained within these Option types. On its own, this isn't much of an issue until I start to serialize the data with serde. I want to be able to skip any values where the value is None, but there are cases where it will come out to being something like Some(None) or Some(CustomOption::None). Both of these cases are not any more meaningful than a simple None, but I can't just write #[serde(skip_serializing_if = "Option::is_none")] on the derived fields. Of course, they output a null value in a JSON format though.

Basically, I want to be able to use the syn library to check to see if the type of a derived field's inner value is going to be an Option and flatten it out into a singular Option<T> in the derived struct instead of an Option<Option<T>> type. I wish Rust had type-based pattern matching on the generics but that's not really a thing.

I can think of two solutions to this problem but I can't really think of how to implement them. First one would be to traverse all of the fields and find the Options, then unwrap those options and re-wrap them so that they only have a single Option on the outside. One potential problem with this solution is that I may have to rewrap them in another Option after doing the computations. The second solution would be to find the Option and modify the generated code accordingly so that if the inner option contains None the entire thing becomes None; basically just have a helper function that outputs a boolean if the field is an Option. Any ideas on how to implement either of these or a better solution?

Here's a code example:

#[derive(Macro)]
struct X {
    a: usize,
    b: SomeType<String>,
    c: Option<String>,
}
struct GeneratedX {
    a: Option<usize>,
    b: Option<SomeType<String>>,
    c: Option<Option<String>>,
}

Using a function like this to wrap all of the values in Options:

pub fn wrap_typ_in_options(&self) -> TokenStream {
    // self is a struct with the type Type in it along with some other items.
    let typ: syn::Type = self.typ();

    // attribute to check if should ignore a field.
    if self.should_ignore() {
        quote! { Option<#typ> }
    } else {
        quote! { Option<<#typ as module::Trait>::Type> }
    }
}

Solution

  • I figured out a solution for this problem following the second idea that I had in the original post. I used a function like this to tell if the token was an Option:

    let idents_of_path = path
        .segments
        .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())
    

    I then added a new method called is_option which returns a boolean if the Type::Path was an option.

    pub fn is_option(&self) -> bool {
        let typ = self.typ();
    
        let opt = match typ {
            Type::Path(typepath) if typepath.qself.is_none() => Some(typepath.path.clone()),
            _ => None,
        };
    
        if let Some(o) = opt {
            check_for_option(&o).is_some()
        } else {
            false
        }
    }
    

    I modified the generated code based on the results of this call in a way that is similar to how I am handling my various attributes. All of this should work fine for my specific use-case since no aliased Option will ever be introduced into this ecosystem. Its a little messy but it gets the job done for now.