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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions