Skip to content

Support renamed derive fields and a term_map! helper for atom-keyed maps #727

Description

@dannote

Rustler users often expose Elixir-facing APIs made of plain maps with atom keys. Those keys should follow Elixir naming conventions, not Rust identifier constraints.

A common issue is that natural Elixir map keys include names that are Rust keywords or otherwise awkward Rust identifiers:

%{
  type: :import,
  start: 0,
  end: 12,
  async: true
}

In Rust, these fields usually need different names:

struct ImportInfo {
    type_: rustler::Atom,
    start: u32,
    end_: u32,
    async_: bool,
}

Without a way to rename the derived atom names, users have to either change the Elixir API shape or write manual Encoder / Decoder implementations.

Two small additions would cover this pattern well:

  1. #[rustler(rename = "...")] support for derived field / variant atom names.
  2. A term_map! macro for constructing Erlang maps without parallel key/value arrays.

#[rustler(rename = "...")]

With rename support, users could keep idiomatic Elixir keys while still using valid Rust identifiers:

#[derive(NifMap)]
struct ImportInfo {
    #[rustler(rename = "type")]
    type_: rustler::Atom,

    start: u32,

    #[rustler(rename = "end")]
    end_: u32,

    #[rustler(rename = "async")]
    async_: bool,
}

This would encode/decode:

%{
  type: :import,
  start: 0,
  end: 12,
  async: true
}

This mirrors a familiar Rust derive convention from Serde:

#[serde(rename = "type")]
type_: String

Potential scope:

  • NifMap field names
  • NifTaggedEnum named variant fields
  • NifTaggedEnum variant tags
  • NifUnitEnum, NifStruct, and NifRecord for consistency

Starting with only NifMap fields would already solve the most common plain-map case.

term_map!

Rustler already exposes the necessary low-level API for map construction, but common map construction currently requires parallel key/value arrays:

rustler::Term::map_from_term_arrays(
    env,
    &[
        atoms::code().encode(env),
        atoms::css().encode(env),
        atoms::errors().encode(env),
        atoms::warnings().encode(env),
    ],
    &[
        code.encode(env),
        css.encode(env),
        errors.encode(env),
        warnings.encode(env),
    ],
)
.unwrap()

For larger maps, the key and value for a field are separated, which makes additions and reordering more error-prone.

A macro keeps each key next to its value:

rustler::term_map!(env, {
    atoms::code() => code,
    atoms::css() => css,
    atoms::errors() => errors,
    atoms::warnings() => warnings,
})

Expected behavior:

rustler::term_map!(env, {
    atoms::code() => "console.log(1)",
    atoms::css() => Option::<String>::None,
    atoms::errors() => Vec::<String>::new(),
})

returns the same term as:

rustler::Term::map_from_term_arrays(
    env,
    &[
        atoms::code().encode(env),
        atoms::css().encode(env),
        atoms::errors().encode(env),
    ],
    &[
        "console.log(1)".encode(env),
        Option::<String>::None.encode(env),
        Vec::<String>::new().encode(env),
    ],
)
.unwrap()

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions