recap icon indicating copy to clipboard operation
recap copied to clipboard

#[derive(Recap)] doesn't compose

Open ExpHP opened this issue 5 years ago • 3 comments

💡 Feature description

Currently the code generated by #[derive(Recap)] doesn't seem to be able to be composed in any manner. Ideally, I would more or less expect that fields of types with #[derive(Recap)] ought to be able to be parsed just like primitives and builtin types.

💻 Basic example

#[derive(Debug, Deserialize, Recap)]
#[recap(regex = r#"(?P<quantity>\d+) (?P<name>\w+)"#)]
struct Chemical {
    quantity: u32,
    name: String,
}

#[derive(Debug, Deserialize, Recap)]
#[recap(regex = r#"(?P<inner>\d+ \w+)"#)]
struct Wrapper {
    inner: Chemical,
}

fn main() {
    // ok
    "1 FUEL".parse::<Chemical>().unwrap();

    // Err(Custom("invalid type: string "1 FUEL", expected struct Chemical"))
    "1 FUEL".parse::<Wrapper>().unwrap();
}

ExpHP avatar Dec 16 '19 19:12 ExpHP

Hi @ExpHP.

I'm a little confused by your example and what you mean by "compose".

Your example with Chemical seems correct. You're derive a way to deserialize a chemical struct with two fields. That seems valid as you're describing the fields to expect in the regex.

With the Wrapper example, the error message reports what I'd expect. When you are declaring a Recap you're describing the fields of the thing you are parsing from a string. In this example you're declaring a description of a a fields for another struct, not Wrapper.

What's your concrete use case? The use cases recap targets is typical FromStr cases you might around std lib. for example.

"123"parse::<usize>() // => 123

but I wouldn't expect the following to work

struct Wrapper { inner: usize }
"123".parse::<Wrapper>() // => err

Perhaps I could extend this further if I had more context for the problem you're trying to solve.

Just out of curiousity. Why not just create the wrapper providing the chemical?

Wrapper { inner: "1 FUEL".parse::<Chemical>().unwrap() }

softprops avatar Dec 17 '19 04:12 softprops

The use case is described here:

https://users.rust-lang.org/t/deserializing-a-vector-with-recap/35726

1 LFDGN => 7 DMPX
1 PFNM, 14 MVSK => 3 VQCQ
14 HJLX, 3 KGKVK, 1 XQSVS => 6 HGSM
#[derive(Debug, Deserialize, Recap)]
#[recap(regex = r#"(?P<quantity>\d+) (?P<name>\w+)"#)]
struct Chemical {
    quantity: u32,
    name: String,
}

#[derive(Debug, Deserialize, Recap)]
#[recap(regex = r#"^(?P<inputs>\d+ \w+(, )?)+ => (?P<output>\d+ \w+)$"#)]
struct Reaction {
    inputs: Vec<Chemical>,
    output: Chemical,
}

ExpHP avatar Dec 17 '19 19:12 ExpHP

Luckily, envy just happens to do magical things with the comma character, so a simple wrapper-type is all that's needed to make this work:

use recap::Recap;
use serde::Deserialize;
use std::error::Error;

mod stringly_typed {
    use serde::Deserialize;
    use std::str::FromStr;

    pub(crate) struct StringlyTyped<T>(T);
    impl<T: std::fmt::Debug> std::fmt::Debug for StringlyTyped<T> {
        fn fmt(
            &self,
            f: &mut std::fmt::Formatter<'_>,
        ) -> std::fmt::Result {
            std::fmt::Debug::fmt(&self.0, f)
        }
    }
    impl<'de, T: Deserialize<'de> + FromStr> Deserialize<'de> for StringlyTyped<T>
    where
        <T as std::str::FromStr>::Err: std::fmt::Display,
    {
        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
        where
            D: serde::Deserializer<'de>,
        {
            let s: String = Deserialize::deserialize(deserializer)?;
            Ok(StringlyTyped(s.parse().map_err(serde::de::Error::custom)?))
        }
    }
}

use stringly_typed::StringlyTyped;

#[derive(Debug, Deserialize, Recap)]
#[recap(regex = r#"(?P<quantity>\d+) (?P<name>\w+)"#)]
struct Chemical {
    quantity: u32,
    name: String,
}

#[derive(Debug, Deserialize, Recap)]
#[recap(regex = r#"^(?P<inputs>(\d+ \w+(, )?)+) => (?P<output>\d+ \w+)$"#)]
struct Reaction {
    // when asked for a Vec-like thing, envy's deserializer format
    // deserializes a string and splits it on ","
    inputs: Vec<StringlyTyped<Chemical>>,
    output: StringlyTyped<Chemical>,
}

fn main() -> Result<(), Box<dyn Error>> {
    let chems = r#"
1 LFDGN => 7 DMPX
1 PFNM, 14 MVSK => 3 VQCQ
14 HJLX, 3 KGKVK, 1 XQSVS => 6 HGSM"#;

    for line in chems.lines().map(str::trim).filter(|s| !s.is_empty()) {
        let entry: Reaction = line.parse()?;
        eprintln!("{:?}", entry);
    }

    Ok(())
}

The only annoying part is that the intermediate deserialized value is a Vec<String> rather than a Vec<&str> (per #2), so it's going to be either inefficient or optimization-reliant.

mmirate avatar Jan 15 '21 02:01 mmirate