alef 0.22.27

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 serde_tag %}#[serde(tag = "{{ serde_tag }}")]
{% 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.is_tuple and serde_untagged %}    {{ variant.name }}({% for field in variant.fields %}{{ field.field_type }}{{ ", " if not loop.last }}{% endfor %}),
{%- else %}    {{ variant.name }} { {% for field in variant.fields %}{{ field.name }}: {{ field.field_type }}{{ ", " if not loop.last }}{% endfor %} },
{%- endif %}
{% else %}    {{ variant.name }},
{% endif %}{% endfor %}}

impl Default for {{ enum_name }} {
    fn default() -> Self { Self::{{ default_variant }}{{ first_variant_default }} }
}

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),
        }
    }
}

impl magnus::TryConvert for {{ enum_name }} {
    fn try_convert(val: magnus::Value) -> Result<Self, magnus::Error> {
        // 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\":\"...\"}}\"")
        // 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)
            .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 has_data %}
{# Variant accessor methods for internally-tagged data enums #}
{%- if serde_tag %}
impl {{ enum_name }} {
{%- for variant in variants %}
{%- if variant.fields | length == 1 and variant.fields[0].name == "_0" %}
    {# Internally-tagged tuple variant converted to struct variant with _0 field #}
    pub fn {{ variant.snake_name }}(&self) -> Option<&{{ variant.fields[0].field_type }}> {
        match self {
            Self::{{ variant.name }} { _0 } => Some(_0),
            _ => None,
        }
    }
{%- endif %}
{%- endfor %}
}
{%- endif %}
{%- endif %}

{# Unit enum definition with Symbol-based conversion #}
{% else %}
#[derive(Clone, Copy, PartialEq, Eq, 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.snake_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> {
        let s: String = magnus::TryConvert::try_convert(val)?;
        // Accept the serde wire name (snake_case), the PascalCase Rust variant name,
        // and a lowercase fallback so fixtures written in any of those styles work.
        match s.as_str() {
{% for variant in variants %}            "{{ variant.snake_name }}"{% if variant.name != variant.snake_name %} | "{{ variant.name }}"{% endif %} => 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 %}