Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions rustler_codegen/src/attrs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use std::fmt::Display;

use syn::{Ident, Lit, Meta};

pub(crate) trait TryFromRustlerNestedAttr: Sized {

@DanielSidhion DanielSidhion Jun 12, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name for this trait should change to be a bit more global than just "rustler nested attributes", but I couldn't think of anything interesting. Appreciate any suggestions.

fn collect_attrs_for_ident(ident: &Ident, meta: &Meta) -> Option<Vec<Self>>;
fn try_from_rustler_nested_attr(ident: &Ident) -> Option<Self>;
fn parse_failure_message() -> impl Display;

fn parse_rustler<T: TryFromRustlerNestedAttr>(meta: &Meta) -> Vec<T> {
if let Meta::List(ref list) = meta {
let mut attrs: Vec<T> = vec![];
let _ = list.parse_nested_meta(|nested_meta| {
let parsed_attr = nested_meta
.path
.get_ident()
.and_then(T::try_from_rustler_nested_attr);

match parsed_attr {
None => Err(nested_meta.error(T::parse_failure_message())),
Some(attr) => {
attrs.push(attr);
Ok(())
}
}
});

return attrs;
}

panic!("Expected nested attributes inside the rustler attribute");
}
}

#[derive(Debug)]
pub(crate) enum RustlerAttr {
Encode,
Decode,
OptionalDecode,
Module(String),
Tag(String),
}

impl RustlerAttr {
fn try_parse_tag(meta: &Meta) -> Option<Vec<RustlerAttr>> {
if let Meta::NameValue(ref name_value) = meta {
let expr = &name_value.value;

if let syn::Expr::Lit(lit_expr) = expr {
if let Lit::Str(ref tag) = lit_expr.lit {
return Some(vec![RustlerAttr::Tag(tag.value())]);
}
}
}
panic!("Cannot parse tag")
}

fn try_parse_module(meta: &Meta) -> Option<Vec<RustlerAttr>> {
if let Meta::NameValue(name_value) = meta {
let expr = &name_value.value;

if let syn::Expr::Lit(lit_expr) = expr {
if let Lit::Str(ref module) = lit_expr.lit {
let ident = format!("Elixir.{}", module.value());
return Some(vec![RustlerAttr::Module(ident)]);
}
}
}
panic!("Cannot parse module")
}
}

impl TryFromRustlerNestedAttr for RustlerAttr {
fn parse_failure_message() -> impl Display {
"Expected encode, decode and/or optional_decode in rustler attribute"
}

fn collect_attrs_for_ident(ident: &Ident, meta: &Meta) -> Option<Vec<Self>> {
match ident.to_string().as_ref() {
"rustler" => Some(Self::parse_rustler(meta)),
"tag" => Self::try_parse_tag(meta),
"module" => Self::try_parse_module(meta),
_ => None,
}
}

fn try_from_rustler_nested_attr(ident: &Ident) -> Option<Self> {
match ident.to_string().as_ref() {
"encode" => Some(Self::Encode),
"decode" => Some(Self::Decode),
"optional_decode" => Some(Self::OptionalDecode),
_ => None,
}
}
}

#[derive(Debug)]
pub(crate) enum RustlerFieldAttr {
OptionalDecode,
}

impl TryFromRustlerNestedAttr for RustlerFieldAttr {
fn parse_failure_message() -> impl Display {
"Expected optional_decode in rustler field attribute"
}

fn collect_attrs_for_ident(ident: &Ident, meta: &Meta) -> Option<Vec<Self>> {
match ident.to_string().as_ref() {
"rustler" => Some(Self::parse_rustler(meta)),
_ => None,
}
}

fn try_from_rustler_nested_attr(ident: &Ident) -> Option<Self> {
match ident.to_string().as_ref() {
"optional_decode" => Some(Self::OptionalDecode),
_ => None,
}
}
}
134 changes: 74 additions & 60 deletions rustler_codegen/src/context.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
use heck::ToSnakeCase;
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{Data, Field, Fields, Ident, Lifetime, Lit, Meta, TypeParam, Variant};
use syn::{Data, Field, Fields, Ident, Lifetime, Type, TypeParam, Variant};

use super::RustlerAttr;
use crate::attrs::{RustlerAttr, RustlerFieldAttr, TryFromRustlerNestedAttr};

///
/// A helper struct to make it easier to access field attributes.
///
/// `StructField` holds a reference to the field itself as well as any parsed attributes declared on that field.
///
pub(crate) struct StructField<'a> {
pub field: &'a Field,
pub attrs: Vec<RustlerFieldAttr>,

/// `true` if we identified that this field is of type `Option`.
pub is_option_type: bool,
}

impl<'a> StructField<'a> {
pub fn optional_decode(&self) -> bool {
self.attrs
.iter()
.any(|attr| matches!(attr, RustlerFieldAttr::OptionalDecode))
}
}

///
/// A parsing context struct.
Expand All @@ -17,7 +38,7 @@ pub(crate) struct Context<'a> {
pub lifetimes: Vec<Lifetime>,
pub type_parameters: Vec<TypeParam>,
pub variants: Option<Vec<&'a Variant>>,
pub struct_fields: Option<Vec<&'a Field>>,
pub struct_fields: Option<Vec<StructField<'a>>>,
pub is_tuple_struct: bool,
}

Expand All @@ -43,7 +64,13 @@ impl<'a> Context<'a> {
};

let struct_fields = match ast.data {
Data::Struct(ref data_struct) => Some(data_struct.fields.iter().collect()),
Data::Struct(ref data_struct) => Some(
data_struct
.fields
.iter()
.map(Self::struct_field_with_parsed_attrs)
.collect(),
),
_ => None,
};

Expand Down Expand Up @@ -104,14 +131,20 @@ impl<'a> Context<'a> {
.any(|attr| matches!(attr, RustlerAttr::Decode))
}

pub fn optional_decode(&self) -> bool {
self.attrs
.iter()
.any(|attr| matches!(attr, RustlerAttr::OptionalDecode))
}

pub fn field_atoms(&self) -> Option<Vec<TokenStream>> {
self.struct_fields.as_ref().map(|struct_fields| {
struct_fields
.iter()
.map(|field| {
let atom_fun = Self::field_to_atom_fun(field);
let atom_fun = Self::field_to_atom_fun(field.field);

let ident = field.ident.as_ref().unwrap();
let ident = field.field.ident.as_ref().unwrap();
let ident_str = ident.to_string();
let ident_str = Self::remove_raw(&ident_str);

Expand All @@ -135,6 +168,32 @@ impl<'a> Context<'a> {
Ident::new(&format!("atom_{ident_str}"), Span::call_site())
}

fn struct_field_with_parsed_attrs(field: &'a Field) -> StructField<'a> {
let attrs: Vec<_> = field
.attrs
.iter()
.flat_map(Self::get_rustler_field_attrs)
.collect();

StructField {
field,
attrs,
is_option_type: Self::is_option_type(&field.ty),
}
}

fn is_option_type(t: &Type) -> bool {
match t {
Type::Path(type_path) => {
// Has a chance of returning false negatives (in case Option was aliased to another name and the field uses the other name as the type) and false positives (if some other module has an `Option` type - we only check that the name is `Option`).
let type_name = type_path.path.segments.last().unwrap();
type_name.ident == "Option"
}
Type::Paren(p) => Self::is_option_type(&p.elem),
_ => false,
}
}

pub fn escape_ident_with_index(ident_str: &str, index: usize, infix: &str) -> Ident {
Ident::new(
&format!(
Expand Down Expand Up @@ -168,67 +227,22 @@ impl<'a> Context<'a> {
}

fn get_rustler_attrs(attr: &syn::Attribute) -> Vec<RustlerAttr> {
Self::parse_attr::<RustlerAttr>(attr)
}

fn get_rustler_field_attrs(attr: &syn::Attribute) -> Vec<RustlerFieldAttr> {
Self::parse_attr::<RustlerFieldAttr>(attr)
}

fn parse_attr<T: TryFromRustlerNestedAttr>(attr: &syn::Attribute) -> Vec<T> {
attr.path()
.segments
.iter()
.filter_map(|segment| {
let meta = &attr.meta;
match segment.ident.to_string().as_ref() {
"rustler" => Some(Context::parse_rustler(meta)),
"tag" => Context::try_parse_tag(meta),
"module" => Context::try_parse_module(meta),
_ => None,
}
T::collect_attrs_for_ident(&segment.ident, meta)
})
.flatten()
.collect()
}

fn parse_rustler(meta: &Meta) -> Vec<RustlerAttr> {
if let Meta::List(ref list) = meta {
let mut attrs: Vec<RustlerAttr> = vec![];
let _ = list.parse_nested_meta(|nested_meta| {
if nested_meta.path.is_ident("encode") {
attrs.push(RustlerAttr::Encode);
Ok(())
} else if nested_meta.path.is_ident("decode") {
attrs.push(RustlerAttr::Decode);
Ok(())
} else {
Err(nested_meta.error("Expected encode and/or decode in rustler attribute"))
}
});

return attrs;
}

panic!("Expected encode and/or decode in rustler attribute");
}

fn try_parse_tag(meta: &Meta) -> Option<Vec<RustlerAttr>> {
if let Meta::NameValue(ref name_value) = meta {
let expr = &name_value.value;

if let syn::Expr::Lit(lit_expr) = expr {
if let Lit::Str(ref tag) = lit_expr.lit {
return Some(vec![RustlerAttr::Tag(tag.value())]);
}
}
}
panic!("Cannot parse tag")
}

fn try_parse_module(meta: &Meta) -> Option<Vec<RustlerAttr>> {
if let Meta::NameValue(name_value) = meta {
let expr = &name_value.value;

if let syn::Expr::Lit(lit_expr) = expr {
if let Lit::Str(ref module) = lit_expr.lit {
let ident = format!("Elixir.{}", module.value());
return Some(vec![RustlerAttr::Module(ident)]);
}
}
}
panic!("Cannot parse module")
}
}
19 changes: 10 additions & 9 deletions rustler_codegen/src/ex_struct.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned};

use syn::{self, spanned::Spanned, Field, Ident};
use syn::{self, spanned::Spanned, Ident};

use super::context::Context;
use super::RustlerAttr;
use crate::attrs::RustlerAttr;
use crate::context::StructField;

pub fn transcoder_decorator(ast: &syn::DeriveInput, add_exception: bool) -> TokenStream {
let ctx = Context::from_ast(ast);
Expand Down Expand Up @@ -65,24 +66,24 @@ pub fn transcoder_decorator(ast: &syn::DeriveInput, add_exception: bool) -> Toke
gen
}

fn gen_decoder(ctx: &Context, fields: &[&Field], atoms_module_name: &Ident) -> TokenStream {
fn gen_decoder(ctx: &Context, fields: &[StructField], atoms_module_name: &Ident) -> TokenStream {
let struct_name = ctx.ident;
let struct_name_str = struct_name.to_string();

let idents: Vec<_> = fields
.iter()
.map(|field| field.ident.as_ref().unwrap())
.map(|field| field.field.ident.as_ref().unwrap())
.collect();

let (assignments, field_defs): (Vec<TokenStream>, Vec<TokenStream>) = fields
.iter()
.zip(idents.iter())
.enumerate()
.map(|(index, (field, ident))| {
let atom_fun = Context::field_to_atom_fun(field);
let atom_fun = Context::field_to_atom_fun(field.field);
let variable = Context::escape_ident_with_index(&ident.to_string(), index, "struct");

let assignment = quote_spanned! { field.span() =>
let assignment = quote_spanned! { field.field.span() =>
let #variable = try_decode_field(term, #atom_fun())?;
};

Expand Down Expand Up @@ -131,7 +132,7 @@ fn gen_decoder(ctx: &Context, fields: &[&Field], atoms_module_name: &Ident) -> T

fn gen_encoder(
ctx: &Context,
fields: &[&Field],
fields: &[StructField],
atoms_module_name: &Ident,
add_exception: bool,
) -> TokenStream {
Expand All @@ -144,8 +145,8 @@ fn gen_encoder(
let (mut data_keys, mut data_values): (Vec<_>, Vec<_>) = fields
.iter()
.map(|field| {
let field_ident = field.ident.as_ref().unwrap();
let atom_fun = Context::field_to_atom_fun(field);
let field_ident = field.field.ident.as_ref().unwrap();
let atom_fun = Context::field_to_atom_fun(field.field);
(
quote! { ::rustler::Encoder::encode(&#atom_fun(), env) },
quote! { ::rustler::Encoder::encode(&self.#field_ident, env) },
Expand Down
Loading