有没有更好的方法来为没有标签的Rust枚举与单元变体生成serde impls?

huangapple go评论58阅读模式
英文:

Is there a better way to derive serde impls for untagged Rust enums with unit variants?

问题

我的团队一直在努力为应该被序列化为字符串的枚举派生DeserializeSerialize实现,其中包括一些预定义的字符串值以及一个"other"情况。问题的关键是,我们不知道如何在未标记的枚举中支持单元变体,而不需要大量的样板代码。

我想写一个像这样的枚举:

#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BinaryComparisonOperator {
    LessThan,
    GreaterThan,
    Equal,
    Custom(String),
}

我希望它序列化为以下方式:

Rust JSON
BinaryComparisonOperator::LessThan "less_than"
BinaryComparisonOperator::GreaterThan "greater_than"
BinaryComparisonOperator::Equal "equal"
BinaryComparisonOperator::Custom("other_op") "other_op"

但这不起作用。单元变体(LessThanGreaterThanEqual)序列化如预期,但Custom变体以以下外部标签进行序列化:

{ "custom": "other_op" } // 不是我们想要的!

我尝试在枚举上添加#[serde(untagged)]以删除标签。这修复了Custom情况,但然后所有单元变体都无法序列化或反序列化。所有测试都反序列化为Custom变体,而不应该,而所有单元变体都序列化为Null

我们想出了这个版本,它可以工作,但比我喜欢的要复杂得多:

use serde::{de, Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, Deserialize)]
#[serde(untagged)]
pub enum BinaryComparisonOperator {
    #[serde(deserialize_with = "parse_less_than")]
    LessThan,
    #[serde(deserialize_with = "parse_greater_than")]
    GreaterThan,
    #[serde(deserialize_with = "parse_equal")]
    Equal,
    Custom(String),
}

impl Serialize for BinaryComparisonOperator {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        match self {
            BinaryComparisonOperator::LessThan => serializer.serialize_str("less_than"),
            BinaryComparisonOperator::GreaterThan => serializer.serialize_str("greater_than"),
            BinaryComparisonOperator::Equal => serializer.serialize_str("equal"),
            BinaryComparisonOperator::Custom(s) => {
                serializer.serialize_str(&s)
            }
        }
    }
}

fn string_p<'de, D>(expected: String, input: String) -> Result<(), D::Error>
where
    D: de::Deserializer<'de>,
{
    if input == expected {
        Ok(())
    } else {
        Err(de::Error::custom("invalid value"))
    }
}

fn parse_less_than<'de, D>(deserializer: D) -> Result<(), D::Error>
where
    D: de::Deserializer<'de>,
{
    let s = String::deserialize(deserializer)?;
    string_p::<'de, D>(s, "less_than".to_owned())
}

// 还有更多的解析辅助函数...

我看到了https://github.com/serde-rs/serde/issues/1158,它似乎暗示了我们复杂的解决方案是必需的。但我希望也许有一个更好的解决方案,我们忽略了吗?

英文:

My team has been struggling to derive Deserialize and Serialize impls for an enum that should be serialized as a string, with some predefined string values and with an "other" case. The gist of the problem is that we don't know of a way to support unit variants in an untagged enum without lots of boilerplate.

I want to write an enum like this,

#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = &quot;snake_case&quot;)]
pub enum BinaryComparisonOperator {
    LessThan,
    GreaterThan,
    Equal,
    Custom(String),
}

And I want it to serialize like this:

Rust JSON
BinaryComparisonOperator::LessThan "less_than"
BinaryComparisonOperator::GreaterThan "greater_than"
BinaryComparisonOperator::Equal "equal"
BinaryComparisonOperator::Custom("other_op") "other_op"

But that doesn't work. The unit variants (LessThan, GreaterThan, Equal)
serialize as expected, but the Custom variant serializes with an external tag
like this:

{ &quot;custom&quot;: &quot;other_op&quot; } // not what we want!

I tried adding #[serde(untagged)] to the enum to remove the tag. That fixes
the Custom case, but then all of the unit variants fail to serialize or to
deserialize. All of our tests deserialize to the Custom variant when they
shouldn't, and all of the unit variants serialize to Null.

We came up with this version which works, but is way more complicated than
I like:

use serde::{de, Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, Deserialize)]
#[serde(untagged)]
pub enum BinaryComparisonOperator {
    #[serde(deserialize_with = &quot;parse_less_than&quot;)]
    LessThan,
    #[serde(deserialize_with = &quot;parse_greater_than&quot;)]
    GreaterThan,
    #[serde(deserialize_with = &quot;parse_equal&quot;)]
    Equal,
    Custom(String),
}

impl Serialize for BinaryComparisonOperator {
    fn serialize&lt;S&gt;(&amp;self, serializer: S) -&gt; Result&lt;S::Ok, S::Error&gt;
    where
        S: serde::Serializer,
    {
        match self {
            BinaryComparisonOperator::LessThan =&gt; serializer.serialize_str(&quot;less_than&quot;),
            BinaryComparisonOperator::GreaterThan =&gt; serializer.serialize_str(&quot;greater_than&quot;),
            BinaryComparisonOperator::Equal =&gt; serializer.serialize_str(&quot;equal&quot;),
            BinaryComparisonOperator::Custom(s) =&gt; {
                serializer.serialize_str(&amp;s)
            }
        }
    }
}

fn string_p&lt;&#39;de, D&gt;(expected: String, input: String) -&gt; Result&lt;(), D::Error&gt;
where
    D: de::Deserializer&lt;&#39;de&gt;,
{
    if input == expected {
        Ok(())
    } else {
        Err(de::Error::custom(&quot;invalid value&quot;))
    }
}

fn parse_less_than&lt;&#39;de, D&gt;(deserializer: D) -&gt; Result&lt;(), D::Error&gt;
where
    D: de::Deserializer&lt;&#39;de&gt;,
{
    let s = String::deserialize(deserializer)?;
    string_p::&lt;&#39;de, D&gt;(s, &quot;less_than&quot;.to_owned())
}

// lots more parse helpers...

I saw https://github.com/serde-rs/serde/issues/1158 which seems to imply that
our complicated solution is necessary. But I'm hoping that maybe there is
a better solution out there that we have overlooked?

答案1

得分: 2

你可以使用两个枚举。

#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(untagged)]
pub enum SerdeBinCompOp {
    Known(KnownOp),
    Custom(String),
}

#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum KnownOp {
    LessThan,
    GreaterThan,
    Equal,
}

这允许你保持父枚举不带标签,同时使用子枚举的变体名称。

这有一个缺点,即如果你希望最终的枚举是扁平的,你需要拥有两个包含所有已知变体的枚举,以及转换实现。这可以通过宏来减少。

#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(from = "SerdeBinCompOp", into = "SerdeBinCompOp")]
pub enum BinaryComparisonOperator {
    LessThan,
    GreaterThan,
    Equal,
    Custom(String),
}

impl From<BinaryComparisonOperator> for SerdeBinCompOp {
    fn from(bco: BinaryComparisonOperator) -> Self {
        match bco {
            BinaryComparisonOperator::LessThan => Self::Known(KnownOp::LessThan),
            BinaryComparisonOperator::GreaterThan => Self::Known(KnownOp::GreaterThan),
            BinaryComparisonOperator::Equal => Self::Known(KnownOp::Equal),
            BinaryComparisonOperator::Custom(s) => Self::Custom(s),
        }
    }
}

impl From<SerdeBinCompOp> for BinaryComparisonOperator {
    fn from(sbco: SerdeBinCompOp) -> Self {
        match sbco {
            SerdeBinCompOp::Known(KnownOp::LessThan) => Self::LessThan,
            SerdeBinCompOp::Known(KnownOp::GreaterThan) => Self::GreaterThan,
            SerdeBinCompOp::Known(KnownOp::Equal) => Self::Equal,
            SerdeBinCompOp::Custom(s) => Self::Custom(s),
        }
    }
}
英文:

You can use two enums.

#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(untagged)]
pub enum SerdeBinCompOp {
    Known(KnownOp),
    Custom(String),
}

#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = &quot;snake_case&quot;)]
pub enum KnownOp {
    LessThan,
    GreaterThan,
    Equal,
}

This allows you to leave the parent enum untagged while using the child enum's variant names.

This has the disadvantage that if you want the final enum to be flat, you need to have two enums with all the known variants, plus conversion implementations. This could be reduced with a macro.

#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(from = &quot;SerdeBinCompOp&quot;, into = &quot;SerdeBinCompOp&quot;)]
pub enum BinaryComparisonOperator {
    LessThan,
    GreaterThan,
    Equal,
    Custom(String),
}

impl From&lt;BinaryComparisonOperator&gt; for SerdeBinCompOp {
    fn from(bco: BinaryComparisonOperator) -&gt; Self {
        match bco {
            BinaryComparisonOperator::LessThan =&gt; Self::Known(KnownOp::LessThan),
            BinaryComparisonOperator::GreaterThan =&gt; Self::Known(KnownOp::GreaterThan),
            BinaryComparisonOperator::Equal =&gt; Self::Known(KnownOp::Equal),
            BinaryComparisonOperator::Custom(s) =&gt; Self::Custom(s),
        }
    }
}

impl From&lt;SerdeBinCompOp&gt; for BinaryComparisonOperator {
    fn from(sbco: SerdeBinCompOp) -&gt; Self {
        match sbco {
            SerdeBinCompOp::Known(KnownOp::LessThan) =&gt; Self::LessThan,
            SerdeBinCompOp::Known(KnownOp::GreaterThan) =&gt; Self::GreaterThan,
            SerdeBinCompOp::Known(KnownOp::Equal) =&gt; Self::Equal,
            SerdeBinCompOp::Custom(s) =&gt; Self::Custom(s),
        }
    }
}

huangapple
  • 本文由 发表于 2023年5月25日 04:38:40
  • 转载请务必保留本文链接:https://go.coder-hub.com/76327250.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定