1

I am using the Serde crate to deserialise a JSON file, which has a nested structure like this:

struct Nested {
    a: Vec<Foo>,
    b: u8,
}

struct Foo {
    c: Bar,
    d: Vec<f32>,
}

Struct Bar {
    e: u32,
    f: String,
}

Part of the applications purpose is to check for missing parameters (or incorrect types in parameters), and then display a nicely printed list of errors found in the file, so I need to handle the structure missing parameters or wrongly typed.

I came across this great post that helped solved my issue, by wrapping each parameter in an enum result that contains the value if it passed, the value if it failed, or a final enum if it was missing (since the nested structures might also be missing I wrapped them in the same enum):

pub enum TryParse<T> {
    Parsed(T),
    Unparsed(Value),
    NotPresent
}

struct Nested {
    a: TryParse<Vec<Foo>>,
    b: TryParse<u8>,
}

struct Foo {
    c: TryParse<Bar>,
    d: TryParse<Vec<f32>>,
}

Struct Bar {
    e: TryParse<u32>,
    f: TryParse<String>,
}

However, I'm not sure how to access them now without unpacking every step into a match statement. For example, I can access B very easily:

    match file.b {
        Parsed(val) => {println!("Got parameter of {}", val)},
        Unparsed(val) => {println!("Invalid type: {:?}", val)}
        NotPresent => {println!("b not found")},
    };

However, I'm not sure how to access the nested ones (C D E and F). I can't use Unwrap or expect since this isn't technically a 'Result', so how do I unpack these?:

if file.a.c.e.Parsed() && file.a.c.e == 32 {... //invalid
if file.a.d && file.a.d.len() == 6... //invalid

I know in a way this flies against rust's 'handle every outcome' philosophy, and I want to handle them, but I want to know if there is a nicer way than 400 nested match statements (the above example is very simplified, the files I am using have up to 6 nested layers, each parameter in the top node has at least 3 layers, some are vectors as well)…

Perhaps I need to implement a function similar to unwrap() on my 'TryParse'? or would it be better to wrap each parameter in a 'Result', extend that with the deserialise trait, and then somehow store the error in the Err option that says if it was a type error or missing parameter?


EDIT

I tried adding the following, some of which works and some of which does not:

impl <T> TryParse<T> {
    pub fn is_ok(self) -> bool { //works
        match self {
            Self::Parsed(_t) => true,
            _ => false,
        }
    }
    pub fn is_absent(self) -> bool { //works
        match self {
            Self::NotPresent => true,
            _ => false,
        }
    }
    pub fn is_invalid(self) -> bool { //works
        match self {
            Self::Unparsed(_) => true,
            _ => false,
        }
    }
    pub fn get(self) -> Result<T, dyn Error> { //doesnt work
        match self {
            Self::Parsed(t) => Ok(t),
            Self::Unparsed(e) => Err(e),
            Self::NotPresent => Err("Invalid")
        }
    }
}

I can't believe it is this hard just to get the result, should I just avoid nested enums or get rid of the TryParse enums/ functions all together and wrap everything in a result, so the user simply knows if it worked or didn't work (but no explanation why it failed)

birdistheword99
  • 179
  • 1
  • 15
  • I don't know how nice you want your error messages, but I'm wondering if you couldn't achieve your goals by: a) generating a JSON schema for your nested structs with the schemars create, and b) checking whether your JSON is valid under the schema with the valico crate. – Caesar Jun 28 '22 at 12:26
  • 2
    If you are ok with unstable features, you can implement [`Try`](https://doc.rust-lang.org/1.54.0/std/ops/trait.Try.html) on your enums, allowing you to use `?` to unpack them. – Jmb Jun 28 '22 at 13:22
  • @Jmb do I need to create a custom implementation/function to use the 'Try' syntax or can I just use a trait attached to the enum? with an enum of more than 2 values, how does it know which value to unpack? – birdistheword99 Jun 28 '22 at 13:42
  • You need to implement the [`Try`](https://doc.rust-lang.org/1.54.0/std/ops/trait.Try.html) trait for your enums. This defines a [`branch`](https://doc.rust-lang.org/1.54.0/std/ops/trait.Try.html#tymethod.branch) method which tells it which value to unpack or return. – Jmb Jun 28 '22 at 13:48

2 Answers2

1

Implementing unwrap() is one possibility. Using Result is another, with a custom error type. You can deserialize directly into result with #[serde(deserialize_with = "...")], or using a newtype wrapper.

However, a not-enough-used power of pattern matching is nested patterns. For example, instead of if file.a.c.e.Parsed() && file.a.c.e == 32 you can write:

if let TryParse::Parsed(a) = &file.a {
    // Unfortunately we cannot combine this `if let` with the surrounding `if let`,
    // because `Vec` doesn't support pattern matching (currently).
    if let TryParsed::Parsed(
        [Foo {
            c:
                TryParse::Parsed(Bar {
                    e: TryParse::Parsed(32),
                    ..
                }),
            ..
        }, ..],
    ) = a.as_slice()
    {
        // ...
    }
}
Chayim Friedman
  • 47,971
  • 5
  • 48
  • 77
  • The second option seems very complicated and almost as much code as a full match syntax, but I'm interested in using the `Result`. How would that work? and would I have to attach `#[serde(deserialize_with = "...")]` for every parameter in each structure or can I just attach it to the structs? – birdistheword99 Jun 28 '22 at 13:39
  • @birdistheword99 I actually like the pattern matching more :) If you use `Result` directly, you have to attach it to each field. However, you can wrap `Result` with a newtype struct, and use a custom `Deserialize` impl for it. – Chayim Friedman Jun 28 '22 at 13:41
  • Sorry I'm a bit new to rust, how do I wrap result to achieve this? I have managed to implement a 'Is valid' function that returns a bool if the value is valid, but I tried implementing a 'get' function to get the value and I just cant find a way, compiler complains if I try to return a result – birdistheword99 Jun 28 '22 at 15:02
  • @birdistheword99 `pub struct MyResult(pub Result); impl<'de, T> serde::Deserialize<'de> for MyResult where T: serde::Deserialize<'de> { ... }`. – Chayim Friedman Jun 28 '22 at 15:07
  • inside those brackets I implemented deserialise, but when I try to return the value wrapped in `Ok(t)` or `Err(value)`, I get all kinds of errors about `mismatched types, expected struct MyResult found Type Parameter T`, and `expected associated type, found struct MyError`. Still not sure how to actually return the value, getting very frustrated with rust! – birdistheword99 Jun 29 '22 at 11:21
  • ```pub struct MyResult(pub Result); impl<'de, T> Deserialize<'de> for MyResult where T: Deserialize<'de> { fn deserialize>(deserializer: D) -> Result, ConfigError> { match Option::::deserialize(deserializer)? { None => Err(ConfigError::new("Missing Paramater")), Some(value) => match T::deserialize(&value) { Ok(t) => Ok(t), Err(_) => Err(ConfigError::new("Wrong Type")), }, } } } ``` – birdistheword99 Jun 29 '22 at 11:24
  • @birdistheword99 If you have problems, you can ask a new SO question. A question should be focused on one thing only. – Chayim Friedman Jun 29 '22 at 11:25
  • Ok fair enough, its still related to the same problem, I haven't solved this yet, but I will ask another question (or probably just write the parser in C and pass the resulting C-Struct to rust, as this is getting overly complicated for such a simple task) - Thank you for your help though – birdistheword99 Jun 29 '22 at 11:31
0

May not be the most Rust-y way of doing it, but for those like me moving from another language like C/Python/C++, this is the way I have done it that still allows me to quickly validate if I have an error and use the match syntax to handle it. Thanks to @Chayim Friedman for assisting with this, his way is probably better but this made the most sense for me:

#[derive(Debug)]
pub enum TryParse<T> {
    Parsed(T),
    Unparsed(Value),
    NotPresent
}

impl<'de, T: DeserializeOwned> Deserialize<'de> for TryParse<T> {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        match Option::<Value>::deserialize(deserializer)? {
            None => Ok(TryParse::NotPresent),
            Some(value) => match T::deserialize(&value) {
                Ok(t) => Ok(TryParse::Parsed(t)),
                Err(_) => Ok(TryParse::Unparsed(value)),
            },
        }
    }
}

impl <T> TryParse<T> {
    //pub fn is_ok(self) -> bool {   ---> Use get().is_ok(), built into result
    //  match self {
    //      Self::Parsed(_t) => true,
    //      _ => false,
    //  }
    //}
    pub fn is_absent(self) -> bool {
        match self {
            Self::NotPresent => true,
            _ => false,
        }
    }
    pub fn is_invalid(self) -> bool {
        match self {
            Self::Unparsed(_) => true,
            _ => false,
        }
    }
    pub fn get(&self) -> Result<&T, String> {
        match self {
            Self::Parsed(t) => Ok(t),
            Self::Unparsed(v) => Err(format!("Unable to Parse {:?}", v)),
            Self::NotPresent => Err("Parameter not Present".to_string())
        }
    }
    // pub fn get_direct(&self) -> &T {
    //     match self {
    //         Self::Parsed(t) => t,
    //         _ => panic!("Can't get this value!"),
    //     }
    // }
}

match &nested.a.get().unwrap()[1].c.get.expect("Missing C Parameter").e{
        Parsed(val) => {println!("Got value of E: {}", val)},
        Unparsed(val) => {println!("Invalid Type: {:?}", val)}
        NotPresent => {println!("Param E Not Found")},
    };

//Note the use of '&' at the beginning because we need to borrow a reference to it

I know I need to change my mindset to use the rust way of thinking, and I am completely open to other suggestions if they can demonstrate some working code.

Jmb
  • 18,893
  • 2
  • 28
  • 55
birdistheword99
  • 179
  • 1
  • 15