A Rust-like Way to Check if Option Type Wraps another Option?

My application uses Rocket.rs (version 0.5-rc) which is trying to parse forms submitted by the client.
Using Rocket has been very convenient, being able to parse structs with traits FromForm straight from GET queries and POST forms. This trait allows for Option parameters to default to None values if no keys are provided.

Long story short, we have the following situation:

struct MyDataForm {
   line_one: Option<String>
   line_two: Option<Option<String>
}

We have a data type which represents a query to the server. In certain endpoints (such as searches) we are okay with optional parameters, so we wrap an Option around all struct members.

However, in some places like POST, we want to be "strict" and only allow the secondary Option to be respected. We are trying to share MyDataForm across both POST and GET requests to avoid having to redefine more structs than necessary, especially if they share similar structures.

In doing so, I am trying to determine:

How can we tell during runtime or build time that a certain parameter has a type of Option<Option<*>>?

We have a uniform type defined by bson::Document (doc) which allows us to iterate through values so we can, during runtime, dismiss nulled parameters (via string encodings of struct entry names and Bson::Null checks) whose values are not set if and only if the entry contain types of Option<Option<*>>.

Any feedback or ideas are much appreciated. Thank you.

Hi, I think this is what you want (forgive my phone's typing):

if let Some(Some(Val)) = line_two_val {
<Logic goes here>
}

You could always use good old match, especially if you want to handle other cases like Some(None). Hope that helps.

1 Like

Reading this on it's own, I would think you want an enum where you sometimes have an Option and sometimes don't.

What do you mean by "parameter" here? Because,

  • Rust is strictly typed, so variables are an Option<Option<*>> or aren't, and if you try to use it as something else that's a compile error
  • enum variants like Option::<T>::Some and Option::<T>::None are not distinct types; the type of variants are the types of their enum.

(If the question requires knowledge about bson::Document, or Rocket, those might be what I'm missing.)

2 Likes

Hi quinedot,

Yes, Document is a special class that can serialize any type into itself and vice versa. It is like serde.
This special property allows us to have a scenario where a class inherits a generic trait's function which serializes it self via to_document and then we can iterate through the class's members.

This allows us to examine, during runtime, types:

        let d = to_document(&self).unwrap();
        let mut result = Document::new();
        for v in d {
            match v.1 {
                Bson::Null => {
                    if strict {
                        result.insert(v.0, v.1);
                    }
                }
                _ => {
                    result.insert(v.0, v.1);
                }
            }
        }
        return Ok(result);

We can check nested types too within the match.

Apologies for the delay in response, I've been a bit busy as of late. However, I was able to find a solution to my predicament. Turns out, I had been thinking about the problem wrong.

As @quinedot had pointed out, the problem should be viewed under the Document class as Rust itself is strongly typed. So I should rephrase my question:

Let's say for the following that type DOpt = Option<Option<u8>> and type SOpt = Option<u8>. Given a value a and under the transformation D(a) where D: Document, can we infer what the type of a should be if a:DOpt OR a:SOpt?

There were two ways to address the issue:

  1. Post-D(a) transformation.

I believe the key observation is that SOpt is injective to DOpt but DOpt is onto SOpt by only one value (consider None vs Some(None)). However, None is what prevents us from being able to infer the type, everything else is possible.

Suppose we are now in a generic trait function that attempts to convert itself into a Document and we need to condition upon an attribute a:DOpt or a:SOpt, then we can do so by the following:

        let d = to_document(&self).unwrap();
        let mut result = Document::new();
        for v in d {
            match v.1 {
                Bson::Null => {
                        println!("This is a ??? null value.");
                        // Custom behavior
                        result.insert(v.0, Self::custom());
                }
                b => {
                    match b {
                         Bson::Null => { println!("This is a DOpt null value."); }
                         match c {
                             _ => { println!("Type is DOpt") }
                         }
                         _ => { println!("Nested value. Type is SOpt"); }
                }
            }
        }
        return Ok(result);

If A != None then we know that immediately either A is DOpt or SOpt by examining what A contains.If A == None however, we would never be able to discern what the type of A is under the transformation Document.

However, DOpt and SOpt have the same null kernel, None. So regardless of which type we are assuming A to be, the behavior of each should treat that kernel as their own type.

So we can introduce some sort of custom behavior via an external trait function call as shown above.

  1. Pre-D(a) Transformation

The solution here is hopefully more straightforward. Since Rust is strongly typed, we can hard-code a look-up function in the trait's generic that can implement custom behavior.

    if Self::is_double_option() {
        let d = to_document(&self).unwrap();
    } else {
        // ... something else
       let d = to_document(&self).unwrap();
    }

I ended up doing this approach as it was much more straightforward. You can push this decision one level up to the caller even if your application is able to determine what path to call and branch.