{# Data enum definition with IntoValue/TryConvert via JSON #}
{% if has_data %}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
{% if native_payload %}#[magnus::wrap(class = "{{ module_name }}::{{ enum_name }}")]
{% endif %}
{% if serde_tag %}#[serde(tag = "{{ serde_tag }}"{% if serde_content %}, content = "{{ serde_content }}"{% endif %})]
{% endif %}{% if serde_untagged %}#[serde(untagged)]
{% endif %}{% if serde_rename_all %}#[serde(rename_all = "{{ serde_rename_all }}")]
{% endif %}pub enum {{ enum_name }} {
{% for variant in variants %}
{% if variant.serde_rename %} #[serde(rename = "{{ variant.serde_rename }}")]
{% endif %}{% if variant.fields %}
{%- if variant.emits_as_tuple %} {{ variant.name }}({% for field in variant.fields %}{{ field.field_type }}{{ ", " if not loop.last }}{% endfor %}),
{%- else %} {{ variant.name }} { {% for field in variant.fields %}{% if field.flatten_newtype %}#[serde(flatten)] {% endif %}{% if field.serde_rename %}#[serde(rename = "{{ field.serde_rename }}")] {% endif %}{{ field.name }}: {{ field.field_type }}{{ ", " if not loop.last }}{% endfor %} },
{%- endif %}
{% else %} {{ variant.name }},
{% endif %}{% endfor %}}
{% if has_default %}impl Default for {{ enum_name }} {
fn default() -> Self { Self::{{ default_variant }}{{ first_variant_default }} }
}
{% endif %}
{% if not native_payload %}impl magnus::IntoValue for {{ enum_name }} {
fn into_value_with(self, handle: &Ruby) -> magnus::Value {
match serde_json::to_value(&self) {
Ok(v) => json_to_ruby(handle, v),
Err(_) => handle.qnil().into_value_with(handle),
}
}
}
{% endif %}
impl magnus::TryConvert for {{ enum_name }} {
fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {
{%- if native_payload %}
// The native class is the only unambiguous input for a discriminator-free enum, but it is
// not the only legitimate one: serde's own Deserialize accepts every variant's wire form,
// so a caller holding a decoded String or Hash must keep working. Take the wrapped instance
// when there is one, otherwise fall through to the serde reader below. ~keep
if let Ok(wrapped) = <&Self as magnus::TryConvert>::try_convert(val) {
return Ok(wrapped.clone());
}
{%- endif %}
{%- if serde_tag %}
// Generated Data variants carry native DTOs, not JSON-compatible display strings.
let class_name: Option<String> = val.class().funcall("name", ())?;
{%- for variant in variants %}
{%- if variant.typed_newtype %}
if class_name.as_deref() == Some("{{ module_name }}::{{ enum_name }}{{ variant.name }}") {
let payload: {{ variant.payload_type }} = val.funcall("value", ())?;
return Ok(Self::{{ variant.name }}{% if variant.emits_as_tuple %}({% if variant.payload_boxed %}Box::new(payload){% else %}payload{% endif %}){% else %} { _0: {% if variant.payload_boxed %}Box::new(payload){% else %}payload{% endif %} }{% endif %});
}
{%- endif %}
{%- endfor %}
{%- endif %}
// For data enums with fields (e.g., PageAction), try to deserialize from JSON first.
// For unit enums or when passed as a string, fall back to string-based conversion.
let json_str: String = if let Ok(s) = <String as magnus::TryConvert>::try_convert(val) {
s
} else {
val.funcall::<_, _, String>("to_json", ()).map_err(|e| {
magnus::Error::new(unsafe { Ruby::get_unchecked() }.exception_type_error(),
format!("no implicit conversion into {{ enum_name }}: {}", e))
})?
};
// Try deserializing as JSON first (handles JSON strings like "\"markdown\"" or "{\"click\":{\"selector\":\"...\"}}\"")
// For internally-tagged enums, a bare variant string is wrapped as {"<tag>": value}.
// If that fails, try treating it as a plain string value and wrap in quotes
// If both fail, try as Custom variant (for untagged enum support)
serde_json::from_str(&json_str)
{%- if serde_tag %}
.or_else(|_| serde_json::from_value(serde_json::json!({ "{{ serde_tag }}": json_str })))
{%- endif %}
.or_else(|_| serde_json::from_str(&format!("\"{json_str}\"")))
.or_else(|_| {
// Try as a JSON string for Custom variant (untagged enums accept any remaining value)
match serde_json::to_value(&json_str) {
Ok(val) => serde_json::from_value(val),
Err(e) => Err(e),
}
})
.map_err(|e| magnus::Error::new(unsafe { Ruby::get_unchecked() }.exception_type_error(), e.to_string()))
}
}
unsafe impl IntoValueFromNative for {{ enum_name }} {}
unsafe impl TryConvertOwned for {{ enum_name }} {}
{% if native_payload %}
impl {{ enum_name }} {
{% for variant in variants %}
pub fn from_{{ variant.snake_name }}(value: {{ variant.payload_type }}) -> Self {
Self::{{ variant.name }}({% if variant.payload_boxed %}Box::new(value){% else %}value{% endif %})
}
pub fn {{ variant.snake_name }}(&self) -> Option<{{ variant.payload_type }}> {
match self {
Self::{{ variant.name }}(value) => Some({% if variant.payload_boxed %}(**value).clone(){% else %}value.clone(){% endif %}),
_ => None,
}
}
{% endfor %}
}
{% endif %}
{%- if has_data %}
{# Variant accessor methods for internally-tagged data enums #}
{%- if serde_tag and not native_payload %}
impl {{ enum_name }} {
{%- for variant in variants %}
{%- if variant.fields | length == 1 and variant.fields[0].name == "_0" %}
pub fn {{ variant.snake_name }}(&self) -> Option<&{{ variant.payload_type }}> {
match self {
{% if variant.emits_as_tuple %} Self::{{ variant.name }}(_0) => Some({% if variant.payload_boxed %}&**_0{% else %}_0{% endif %}),
{% else %} Self::{{ variant.name }} { _0 } => Some({% if variant.payload_boxed %}&**_0{% else %}_0{% endif %}),
{% endif %}
_ => None,
}
}
{%- endif %}
{%- endfor %}
}
{%- endif %}
{%- endif %}
{# Unit enum definition with Symbol-based conversion #}
{% else %}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, serde::Serialize, serde::Deserialize)]
{% if serde_rename_all %}#[serde(rename_all = "{{ serde_rename_all }}")]
{% endif %}pub enum {{ enum_name }} {
{% for variant in variants %}
{% if variant.serde_rename %} #[serde(rename = "{{ variant.serde_rename }}")]
{% endif %} {{ variant.name }},
{% endfor %}}
impl Default for {{ enum_name }} {
fn default() -> Self { Self::{{ default_variant }} }
}
impl magnus::IntoValue for {{ enum_name }} {
fn into_value_with(self, handle: &Ruby) -> magnus::Value {
let sym = match self {
{% for variant in variants %} {{ enum_name }}::{{ variant.name }} => "{{ variant.wire_name }}",
{% endfor %} };
handle.to_symbol(sym).into_value_with(handle)
}
}
impl magnus::TryConvert for {{ enum_name }} {
fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {
// IntoValue emits Symbols, including when this enum is a Hash key.
// Accept that exact representation as well as String inputs; do not
// coerce arbitrary objects through to_s.
let s: String = if let Some(symbol) = magnus::Symbol::from_value(val) {
symbol.name()?.into_owned()
} else {
magnus::TryConvert::try_convert(val)?
};
// Accept the real serde wire value, the legacy always-snake_case symbol Magnus used to
// emit unconditionally, and the verbatim PascalCase Rust variant name, so fixtures and
// existing consumer code written in any of those styles keep working.
match s.as_str() {
{% for variant in variants %} {% for spelling in variant.accepted_input_values %}"{{ spelling }}"{{ " | " if not loop.last }}{% endfor %} => Ok({{ enum_name }}::{{ variant.name }}),
{% endfor %} other => Err(magnus::Error::new(
unsafe { Ruby::get_unchecked() }.exception_arg_error(),
format!("invalid {{ enum_name }} value: {other}"),
)),
}
}
}
unsafe impl IntoValueFromNative for {{ enum_name }} {}
unsafe impl TryConvertOwned for {{ enum_name }} {}
{% endif %}