use crate::core::ir::EnumDef;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SerdeEnumRepr {
External,
Internal { tag: String },
Adjacent { tag: String, content: String },
Untagged,
}
impl SerdeEnumRepr {
pub fn kind(&self) -> &'static str {
match self {
Self::External => "external",
Self::Internal { .. } => "internal",
Self::Adjacent { .. } => "adjacent",
Self::Untagged => "untagged",
}
}
pub fn tag(&self) -> Option<&str> {
match self {
Self::Internal { tag } | Self::Adjacent { tag, .. } => Some(tag),
Self::External | Self::Untagged => None,
}
}
pub fn content(&self) -> Option<&str> {
match self {
Self::Adjacent { content, .. } => Some(content),
Self::External | Self::Internal { .. } | Self::Untagged => None,
}
}
}
pub fn serde_enum_repr(enum_def: &EnumDef) -> SerdeEnumRepr {
if enum_def.serde_untagged {
return SerdeEnumRepr::Untagged;
}
match (enum_def.serde_tag.as_deref(), enum_def.serde_content.as_deref()) {
(Some(tag), Some(content)) => SerdeEnumRepr::Adjacent {
tag: tag.to_string(),
content: content.to_string(),
},
(Some(tag), None) => SerdeEnumRepr::Internal { tag: tag.to_string() },
(None, _) => SerdeEnumRepr::External,
}
}
#[must_use]
pub fn variant_name_for_wire<'a>(enum_def: &'a EnumDef, wire: &str) -> Option<&'a str> {
let rename_all = enum_def.serde_rename_all.as_deref();
enum_def
.variants
.iter()
.find(|variant| {
variant.name == wire
|| crate::codegen::naming::wire_variant_value(
&variant.name,
variant.serde_rename.as_deref(),
rename_all,
) == wire
})
.map(|variant| variant.name.as_str())
}
#[cfg(test)]
mod variant_name_for_wire_tests {
use super::*;
use crate::core::ir::EnumVariant;
fn output_format(rename_all: Option<&str>, variants: &[(&str, Option<&str>)]) -> EnumDef {
EnumDef {
name: "OutputFormat".to_string(),
serde_rename_all: rename_all.map(str::to_string),
variants: variants
.iter()
.map(|(name, rename)| EnumVariant {
name: (*name).to_string(),
serde_rename: rename.map(str::to_string),
..EnumVariant::default()
})
.collect(),
..EnumDef::default()
}
}
type RenameCase<'a> = (
&'a str,
Option<&'a str>,
&'a [(&'a str, Option<&'a str>)],
&'a str,
Option<&'a str>,
);
#[test]
fn should_resolve_every_rename_strategy_back_to_the_declared_variant() {
let cases: [RenameCase<'_>; 7] = [
(
"single word, no rename",
None,
&[("Markdown", None)],
"Markdown",
Some("Markdown"),
),
(
"single word under lowercase",
Some("lowercase"),
&[("Markdown", None)],
"markdown",
Some("Markdown"),
),
(
"multi word under snake_case",
Some("snake_case"),
&[("PlainText", None)],
"plain_text",
Some("PlainText"),
),
(
"multi word under lowercase collapses, so re-casing cannot recover it",
Some("lowercase"),
&[("PlainText", None)],
"plaintext",
Some("PlainText"),
),
(
"explicit rename wins over rename_all",
Some("snake_case"),
&[("Markdown", Some("md"))],
"md",
Some("Markdown"),
),
(
"acronym variant under lowercase",
Some("lowercase"),
&[("HTML", None)],
"html",
Some("HTML"),
),
("value no variant declares", None, &[("Markdown", None)], "pdf", None),
];
for (case, rename_all, variants, wire, expected) in cases {
let enum_def = output_format(rename_all, variants);
assert_eq!(variant_name_for_wire(&enum_def, wire), expected, "{case}");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn enum_def(tag: Option<&str>, content: Option<&str>, untagged: bool) -> EnumDef {
EnumDef {
name: "Sample".to_string(),
serde_tag: tag.map(str::to_string),
serde_content: content.map(str::to_string),
serde_untagged: untagged,
..EnumDef::default()
}
}
#[test]
fn should_be_external_when_no_serde_container_attribute_is_present() {
let repr = serde_enum_repr(&enum_def(None, None, false));
assert_eq!(repr, SerdeEnumRepr::External);
assert_eq!(repr.kind(), "external");
assert_eq!(repr.tag(), None);
assert_eq!(repr.content(), None);
}
#[test]
fn should_be_internal_when_only_tag_is_present() {
let repr = serde_enum_repr(&enum_def(Some("type"), None, false));
assert_eq!(
repr,
SerdeEnumRepr::Internal {
tag: "type".to_string()
}
);
assert_eq!(repr.kind(), "internal");
assert_eq!(repr.tag(), Some("type"));
assert_eq!(repr.content(), None);
}
#[test]
fn should_be_adjacent_when_tag_and_content_are_both_present() {
let repr = serde_enum_repr(&enum_def(Some("kind"), Some("payload"), false));
assert_eq!(
repr,
SerdeEnumRepr::Adjacent {
tag: "kind".to_string(),
content: "payload".to_string()
}
);
assert_eq!(repr.kind(), "adjacent");
assert_eq!(repr.tag(), Some("kind"));
assert_eq!(repr.content(), Some("payload"));
}
#[test]
fn should_be_untagged_when_untagged_is_set() {
let repr = serde_enum_repr(&enum_def(None, None, true));
assert_eq!(repr, SerdeEnumRepr::Untagged);
assert_eq!(repr.kind(), "untagged");
assert_eq!(repr.tag(), None);
}
#[test]
fn should_prefer_untagged_over_a_stray_tag() {
assert_eq!(
serde_enum_repr(&enum_def(Some("type"), None, true)),
SerdeEnumRepr::Untagged
);
}
#[test]
fn should_ignore_content_without_a_tag() {
assert_eq!(
serde_enum_repr(&enum_def(None, Some("payload"), false)),
SerdeEnumRepr::External
);
}
}