alef 0.103.6

Opinionated polyglot binding generator for Rust libraries
Documentation
{# 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 %}