Is it possible to initialise tuple variants of an enum from an array

I am curious to know whether it is possible to write the body of try_from below for all enum tuple variants (where the parts of the tuple implement JsCast)?

enum JsMessageType {
    MessageA(js_sys::Date, js_sys::Number, js_sys::Set),
    MessageB(js_sys::Date, js_sys::BigInt)
}

impl TryFrom<js_sys::Array> for JsMessageType {
    type Error = JsValue;

    fn try_from(array: js_sys::Array) -> Result<Self, Self::Error> {
        let discriminant: String = array.get(0)
            .dyn_into::<js_sys::JsString>()?
            .into();
        match &discriminant[..] {
            "MessageA" => {
                Ok(JsMessageType::MessageA(
                    array.get(1).dyn_into::<js_sys::Date>()?,
                    array.get(2).dyn_into::<js_sys::Number>()?,
                    array.get(3).dyn_into::<js_sys::Set>()?,
                ))
            },
            "MessageB" => {
                Ok(JsMessageType::MessageB(
                    array.get(1).dyn_into::<js_sys::Date>()?,
                    array.get(2).dyn_into::<js_sys::BigInt>()?,
                ))
            }
            _ => Err(JsValue::from_str("Unknown variant"))
        }
    }
}

I am confused by this statement. What exactly do you want to match over?

The match statement is only supposed to demonstrate functionally that I would like to have in general. To clarify, I have an array like:

[JsString, Date, Number, Set]

Where the JsString describes the discriminant of the enum and the remaining elements in the array should be used to initialise the corresponding variant of that enum.

In a nutshell, I would like to write try_from for any enum -- with a few constraints (tuple variants with 0..N parts that implement JsCast).

Well, a single TryFrom impl is for a single enum. If you'd like to generate the body of the try_from method from the variants of your enum, you'd need a proc-macro for it.

Ah! I see why what I was asking was unclear now. What I would like to achieve is rather to be able to write the body of try_from and not have to change it even if I introduced a JsMessageType::MessageC variant.

You'd need a proc-macro for that as well. The macro will "iterate" over all enum variants and introduce arms in the match for each on of them.

This certainly can be solved with a proc-macro, although I was wondering whether there was an alternative to going down that route.

You can probably get pretty far with ordinary declarative macros, as well. The first step would be to transform the code into something like this, which has as much repetition as you can come up with:

(NB: This is all untested, as the playground doesn't have access to the js_sys crate)

impl TryFrom<js_sys::Array> for JsMessageType {
    type Error = JsValue;

    fn try_from(array: js_sys::Array) -> Result<Self, Self::Error> {
        let mut iter = array.into_iter();
        let discriminant: String = iter.next()?
            .dyn_into::<js_sys::JsString>()?
            .into();
        match discriminant.as_str() {
            "MessageA" => {
                Ok(JsMessageType::MessageA(
                    iter.next()?.dyn_into()?,
                    iter.next()?.dyn_into()?,
                    iter.next()?.dyn_into()?,
                ))
            },
            "MessageB" => {
                Ok(JsMessageType::MessageB(
                    iter.next()?.dyn_into()?,
                    iter.next()?.dyn_into()?,
                ))
            }
            _ => Err(JsValue::from_str("Unknown variant"))
        }
    }
}

Then, you can write a macro to simplify the construction of individual variants:

impl TryFrom<js_sys::Array> for JsMessageType {
    type Error = JsValue;

    fn try_from(array: js_sys::Array) -> Result<Self, Self::Error> {
        let mut iter = array.into_iter();
        let discriminant: String = iter.next()?
            .dyn_into::<js_sys::JsString>()?
            .into();
        
        macro_rules! parse_msg! {
            $(variant::ident, 0) => { JsMessageType::$variant() };
            $(variant::ident, 1) => { JsMessageType::$variant(
                iter.next()?.dyn_into()?
            ) };
            $(variant::ident, 2) => { JsMessageType::$variant(
                iter.next()?.dyn_into()?
                iter.next()?.dyn_into()?
            ) };
            $(variant::ident, 3) => { JsMessageType::$variant(
                iter.next()?.dyn_into()?
                iter.next()?.dyn_into()?
                iter.next()?.dyn_into()?
            ) };
            $(variant::ident, 4) => { JsMessageType::$variant(
                iter.next()?.dyn_into()?
                iter.next()?.dyn_into()?
                iter.next()?.dyn_into()?
                iter.next()?.dyn_into()?
            ) };
        }

        match discriminant.as_str() {
            "MessageA" => Ok(parse_msg!(MessageA, 3)),
            "MessageB" => Ok(parse_msg!(MessageB, 2)),
            _ => Err(JsValue::from_str("Unknown variant"))
        }
    }
}

From there, you can write another to implement the match statement and flag a compile error if you forget to add a new variant:

impl TryFrom<js_sys::Array> for JsMessageType {
    type Error = JsValue;

    fn try_from(array: js_sys::Array) -> Result<Self, Self::Error> {
        let mut iter = array.into_iter();
        let discriminant: String = iter.next()?
            .dyn_into::<js_sys::JsString>()?
            .into();
        
        macro_rules! parse_msg! {
            $(variant::ident, 0) => { JsMessageType::$variant() };
            $(variant::ident, 1) => { JsMessageType::$variant(
                iter.next()?.dyn_into()?
            ) };
            $(variant::ident, 2) => { JsMessageType::$variant(
                iter.next()?.dyn_into()?
                iter.next()?.dyn_into()?
            ) };
            $(variant::ident, 3) => { JsMessageType::$variant(
                iter.next()?.dyn_into()?
                iter.next()?.dyn_into()?
                iter.next()?.dyn_into()?
            ) };
            $(variant::ident, 4) => { JsMessageType::$variant(
                iter.next()?.dyn_into()?
                iter.next()?.dyn_into()?
                iter.next()?.dyn_into()?
                iter.next()?.dyn_into()?
            ) };
        }

        macro_rules! parse_enum {
            {$($variant:ident: $nargs:literal),*} => {{
                /// This will trigger a compiler error if any variant is missing
                let _ = |x:JsMessageType| match x {
                    $($variant(..) => ()),
                };
                match discriminant.as_str() {
                    $(stringify!($variant) => Ok(parse_msg!($variant, $nargs)),)
                    _ => Err(JsValue::from_str("Unknown variant"))
                }
            }}
        }

        parse_enum! {
            MessageA: 3,
            MessageB: 2
        }
    }
}
2 Likes