Skip to content

Commit b630cb3

Browse files
committed
Add rename support for derived atom names
1 parent f5f88a8 commit b630cb3

7 files changed

Lines changed: 149 additions & 28 deletions

File tree

rustler_codegen/src/context.rs

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use heck::ToSnakeCase;
22
use proc_macro2::{Span, TokenStream};
33
use quote::quote;
4-
use syn::{Data, Field, Fields, Ident, Lifetime, Lit, Meta, TypeParam, Variant};
4+
use syn::{Attribute, Data, Field, Fields, Ident, Lifetime, Lit, Meta, TypeParam, Variant};
55

66
use super::RustlerAttr;
77

@@ -110,29 +110,49 @@ impl<'a> Context<'a> {
110110
.iter()
111111
.map(|field| {
112112
let atom_fun = Self::field_to_atom_fun(field);
113-
114-
let ident = field.ident.as_ref().unwrap();
115-
let ident_str = ident.to_string();
116-
let ident_str = Self::remove_raw(&ident_str);
113+
let atom_name = Self::field_atom_name(field);
117114

118115
quote! {
119-
#atom_fun = #ident_str,
116+
#atom_fun = #atom_name,
120117
}
121118
})
122119
.collect()
123120
})
124121
}
125122

126123
pub fn field_to_atom_fun(field: &Field) -> Ident {
127-
let ident = field.ident.as_ref().unwrap();
128-
Self::ident_to_atom_fun(ident)
124+
Self::atom_fun(&Self::field_atom_name(field))
125+
}
126+
127+
pub fn field_atom_name(field: &Field) -> String {
128+
Self::rename_attr(&field.attrs).unwrap_or_else(|| {
129+
let ident = field.ident.as_ref().unwrap();
130+
Self::ident_to_atom_name(ident)
131+
})
132+
}
133+
134+
pub fn variant_to_atom_fun(variant: &Variant) -> Ident {
135+
Self::atom_fun(&Self::variant_atom_name(variant))
129136
}
130137

131-
pub fn ident_to_atom_fun(ident: &Ident) -> Ident {
132-
let ident_str = ident.to_string().to_snake_case();
133-
let ident_str = Self::remove_raw(&ident_str);
138+
pub fn variant_atom_name(variant: &Variant) -> String {
139+
Self::rename_attr(&variant.attrs)
140+
.unwrap_or_else(|| Self::ident_to_atom_name(&variant.ident))
141+
}
142+
143+
pub fn ident_to_atom_name(ident: &Ident) -> String {
144+
let ident_str = ident.to_string();
145+
Self::remove_raw(&ident_str).to_snake_case()
146+
}
147+
148+
fn atom_fun(atom_name: &str) -> Ident {
149+
let suffix = atom_name
150+
.as_bytes()
151+
.iter()
152+
.map(|byte| format!("{byte:02x}"))
153+
.collect::<String>();
134154

135-
Ident::new(&format!("atom_{ident_str}"), Span::call_site())
155+
Ident::new(&format!("atom_{}", suffix), Span::call_site())
136156
}
137157

138158
pub fn escape_ident_with_index(ident_str: &str, index: usize, infix: &str) -> Ident {
@@ -161,6 +181,33 @@ impl<'a> Context<'a> {
161181
.expect("split has always at least one element")
162182
}
163183

184+
fn rename_attr(attrs: &[Attribute]) -> Option<String> {
185+
attrs.iter().find_map(|attr| {
186+
if !attr.path().is_ident("rustler") {
187+
return None;
188+
}
189+
190+
let Meta::List(list) = &attr.meta else {
191+
return None;
192+
};
193+
194+
let mut rename = None;
195+
list.parse_nested_meta(|nested_meta| {
196+
if nested_meta.path.is_ident("rename") {
197+
let value = nested_meta.value()?;
198+
let lit: syn::LitStr = value.parse()?;
199+
rename = Some(lit.value());
200+
Ok(())
201+
} else {
202+
Err(nested_meta.error("Expected rename in rustler attribute"))
203+
}
204+
})
205+
.unwrap_or_else(|err| panic!("{}", err));
206+
207+
rename
208+
})
209+
}
210+
164211
fn encode_decode_attr_set(attrs: &[RustlerAttr]) -> bool {
165212
attrs
166213
.iter()

rustler_codegen/src/tagged_enum.rs

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use proc_macro2::{Span, TokenStream};
22
use quote::{quote, quote_spanned};
33

4-
use heck::ToSnakeCase;
54
use std::collections::HashMap;
65
use syn::{self, spanned::Spanned, Field, Fields, FieldsNamed, FieldsUnnamed, Ident, Variant};
76

@@ -18,28 +17,25 @@ pub fn transcoder_decorator(ast: &syn::DeriveInput) -> TokenStream {
1817
let atoms = variants
1918
.iter()
2019
.flat_map(|variant| {
21-
let mut ret = if let Fields::Named(fields) = &variant.fields {
20+
let fields = if let Fields::Named(fields) = &variant.fields {
2221
fields
2322
.named
2423
.iter()
2524
.map(|field| {
26-
field
27-
.ident
28-
.as_ref()
29-
.expect("Named fields must have an ident.")
25+
(
26+
Context::field_atom_name(field),
27+
Context::field_to_atom_fun(field),
28+
)
3029
})
3130
.collect::<Vec<_>>()
3231
} else {
3332
vec![]
3433
};
3534

36-
ret.push(&variant.ident);
37-
ret
38-
})
39-
.map(|atom_ident| {
40-
let atom_str = atom_ident.to_string().to_snake_case();
41-
let atom_fn = Context::ident_to_atom_fun(atom_ident);
42-
(atom_str, atom_fn)
35+
fields.into_iter().chain(std::iter::once((
36+
Context::variant_atom_name(variant),
37+
Context::variant_to_atom_fun(variant),
38+
)))
4339
})
4440
.collect::<HashMap<_, _>>()
4541
.into_iter()
@@ -89,7 +85,7 @@ fn gen_decoder(ctx: &Context, variants: &[&Variant], atoms_module_name: &Ident)
8985
.iter()
9086
.filter_map(|variant| {
9187
let variant_ident = &variant.ident;
92-
let atom_fn = Context::ident_to_atom_fun(variant_ident);
88+
let atom_fn = Context::variant_to_atom_fun(variant);
9389
match &variant.fields {
9490
Fields::Unit => Some(gen_unit_decoder(enum_name, variant_ident, atom_fn)),
9591
_ => None,
@@ -100,7 +96,7 @@ fn gen_decoder(ctx: &Context, variants: &[&Variant], atoms_module_name: &Ident)
10096
.iter()
10197
.filter_map(|variant| {
10298
let variant_ident = &variant.ident;
103-
let atom_fn = Context::ident_to_atom_fun(variant_ident);
99+
let atom_fn = Context::variant_to_atom_fun(variant);
104100
match &variant.fields {
105101
Fields::Unnamed(fields) => Some(gen_unnamed_decoder(
106102
enum_name,
@@ -161,7 +157,7 @@ fn gen_encoder(ctx: &Context, variants: &[&Variant], atoms_module_name: &Ident)
161157
.iter()
162158
.map(|variant| {
163159
let variant_ident = &variant.ident;
164-
let atom_fn = Context::ident_to_atom_fun(variant_ident);
160+
let atom_fn = Context::variant_to_atom_fun(variant);
165161

166162
match &variant.fields {
167163
Fields::Unit => gen_unit_encoder(enum_name, variant_ident, atom_fn),
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
use rustler_codegen::NifMap;
2+
3+
#[derive(NifMap)]
4+
struct InvalidRename {
5+
#[rustler(rename)]
6+
value: i32,
7+
}
8+
9+
fn main() {}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
error: proc-macro derive panicked
2+
--> tests/ui/derive-rename-invalid.rs:3:10
3+
|
4+
3 | #[derive(NifMap)]
5+
| ^^^^^^
6+
|
7+
= help: message: expected `=`

rustler_tests/lib/rustler_test.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,16 @@ defmodule RustlerTest do
110110
def tuple_echo(_), do: err()
111111
def record_echo(_), do: err()
112112
def map_echo(_), do: err()
113+
def renamed_map_echo(_), do: err()
114+
def unicode_renamed_map_echo(_), do: err()
113115
def exception_echo(_), do: err()
114116
def struct_echo(_), do: err()
115117
def unit_enum_echo(_), do: err()
116118
def tagged_enum_1_echo(_), do: err()
117119
def tagged_enum_2_echo(_), do: err()
118120
def tagged_enum_3_echo(_), do: err()
119121
def tagged_enum_4_echo(_), do: err()
122+
def renamed_tagged_enum_echo(_), do: err()
120123
def untagged_enum_echo(_), do: err()
121124
def untagged_enum_with_truthy(_), do: err()
122125
def untagged_enum_for_issue_370(_), do: err()

rustler_tests/native/rustler_test/src/test_codegen.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,35 @@ pub fn map_echo(map: AddMap) -> AddMap {
5454
map
5555
}
5656

57+
#[derive(NifMap)]
58+
pub struct RenamedMap {
59+
#[rustler(rename = "type")]
60+
type_: rustler::Atom,
61+
start: u32,
62+
#[rustler(rename = "end")]
63+
end_: u32,
64+
#[rustler(rename = "async")]
65+
async_: bool,
66+
}
67+
68+
#[rustler::nif]
69+
pub fn renamed_map_echo(map: RenamedMap) -> RenamedMap {
70+
map
71+
}
72+
73+
#[derive(NifMap)]
74+
pub struct UnicodeRenamedMap {
75+
#[rustler(rename = "name_ä")]
76+
name_a_umlaut: u32,
77+
#[rustler(rename = "name_ö")]
78+
name_o_umlaut: u32,
79+
}
80+
81+
#[rustler::nif]
82+
pub fn unicode_renamed_map_echo(map: UnicodeRenamedMap) -> UnicodeRenamedMap {
83+
map
84+
}
85+
5786
#[derive(Debug, NifStruct)]
5887
#[must_use] // Added to test Issue #152
5988
#[module = "AddStruct"]
@@ -156,6 +185,21 @@ pub fn tagged_enum_4_echo(tagged_enum: TaggedEnum4) -> TaggedEnum4 {
156185
tagged_enum
157186
}
158187

188+
#[derive(NifTaggedEnum)]
189+
pub enum RenamedTaggedEnum {
190+
#[rustler(rename = "renamed")]
191+
Named {
192+
#[rustler(rename = "end")]
193+
end_: i32,
194+
y: i32,
195+
},
196+
}
197+
198+
#[rustler::nif]
199+
pub fn renamed_tagged_enum_echo(tagged_enum: RenamedTaggedEnum) -> RenamedTaggedEnum {
200+
tagged_enum
201+
}
202+
159203
#[derive(NifUntaggedEnum)]
160204
pub enum UntaggedEnum {
161205
Foo(u32),

rustler_tests/test/codegen_test.exs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ defmodule RustlerTest.CodegenTest do
4545
assert value == RustlerTest.map_echo(value)
4646
end
4747

48+
test "renamed fields" do
49+
value = %{type: :import, start: 0, end: 12, async: true}
50+
assert value == RustlerTest.renamed_map_echo(value)
51+
end
52+
53+
test "renamed fields with unicode atoms" do
54+
value = %{"name_ä": 1, "name_ö": 2}
55+
assert value == RustlerTest.unicode_renamed_map_echo(value)
56+
end
57+
4858
test "with invalid map" do
4959
value = %{lhs: "invalid", rhs: 2, loc: {57, 15}}
5060

@@ -330,6 +340,11 @@ defmodule RustlerTest.CodegenTest do
330340
end)
331341
end
332342

343+
test "renamed tagged enum variants and fields" do
344+
value = {:renamed, %{end: 1, y: 2}}
345+
assert value == RustlerTest.renamed_tagged_enum_echo(value)
346+
end
347+
333348
test "untagged enum transcoder" do
334349
assert 123 == RustlerTest.untagged_enum_echo(123)
335350
assert "Hello" == RustlerTest.untagged_enum_echo("Hello")

0 commit comments

Comments
 (0)