use proc_macro::TokenStream;
use quote::quote;
use syn::{Data, DeriveInput, Fields, Lit, Type, parse_macro_input};
#[derive(Default)]
struct ContainerAttrs {
rename_all: Option<RenameAll>,
untagged: bool,
tag: Option<String>,
}
#[derive(Default)]
struct FieldAttrs {
rename: Option<String>,
skip: bool,
flatten: bool,
skip_serializing_if: Option<String>,
}
#[derive(Clone, Copy)]
enum RenameAll {
CamelCase,
SnakeCase,
PascalCase,
ScreamingSnakeCase,
KebabCase,
}
fn parse_container_attrs(attrs: &[syn::Attribute]) -> ContainerAttrs {
let mut result = ContainerAttrs::default();
for attr in attrs {
if !attr.path().is_ident("marfy") {
continue;
}
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("untagged") {
result.untagged = true;
} else if meta.path.is_ident("rename_all") {
let value = meta.value()?;
let lit: Lit = value.parse()?;
if let Lit::Str(s) = lit {
result.rename_all = parse_rename_all(&s.value());
}
} else if meta.path.is_ident("tag") {
let value = meta.value()?;
let lit: Lit = value.parse()?;
if let Lit::Str(s) = lit {
result.tag = Some(s.value());
}
}
Ok(())
});
}
result
}
fn parse_field_attrs(attrs: &[syn::Attribute]) -> FieldAttrs {
let mut result = FieldAttrs::default();
for attr in attrs {
if !attr.path().is_ident("marfy") {
continue;
}
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename") {
let value = meta.value()?;
let lit: Lit = value.parse()?;
if let Lit::Str(s) = lit {
result.rename = Some(s.value());
}
} else if meta.path.is_ident("skip") {
result.skip = true;
} else if meta.path.is_ident("flatten") {
result.flatten = true;
} else if meta.path.is_ident("skip_serializing_if") {
let value = meta.value()?;
let lit: Lit = value.parse()?;
if let Lit::Str(s) = lit {
result.skip_serializing_if = Some(s.value());
}
}
Ok(())
});
}
result
}
fn parse_rename_all(s: &str) -> Option<RenameAll> {
match s {
"camelCase" => Some(RenameAll::CamelCase),
"snake_case" => Some(RenameAll::SnakeCase),
"PascalCase" => Some(RenameAll::PascalCase),
"SCREAMING_SNAKE_CASE" => Some(RenameAll::ScreamingSnakeCase),
"kebab-case" => Some(RenameAll::KebabCase),
_ => {
eprintln!(
"marfy warning: unknown rename_all value \"{}\". \
Expected: camelCase, snake_case, PascalCase, SCREAMING_SNAKE_CASE, kebab-case. \
Falling back to no rename.",
s
);
None
}
}
}
fn apply_rename_all(name: &str, rule: RenameAll) -> String {
match rule {
RenameAll::CamelCase => to_camel_case(name),
RenameAll::SnakeCase => to_snake_case(name),
RenameAll::PascalCase => to_pascal_case(name),
RenameAll::ScreamingSnakeCase => to_screaming_snake_case(name),
RenameAll::KebabCase => to_kebab_case(name),
}
}
fn to_camel_case(s: &str) -> String {
let mut result = String::new();
let mut capitalize_next = false;
for (i, c) in s.chars().enumerate() {
if c == '_' {
capitalize_next = true;
} else if capitalize_next {
result.push(c.to_ascii_uppercase());
capitalize_next = false;
} else if i == 0 {
result.push(c.to_ascii_lowercase());
} else {
result.push(c);
}
}
result
}
fn to_snake_case(s: &str) -> String {
let mut result = String::new();
for (i, c) in s.chars().enumerate() {
if c.is_ascii_uppercase() {
if i > 0 {
result.push('_');
}
result.push(c.to_ascii_lowercase());
} else {
result.push(c);
}
}
result
}
fn to_pascal_case(s: &str) -> String {
let mut result = String::new();
let mut capitalize_next = true;
for c in s.chars() {
if c == '_' {
capitalize_next = true;
} else if capitalize_next {
result.push(c.to_ascii_uppercase());
capitalize_next = false;
} else {
result.push(c);
}
}
result
}
fn to_screaming_snake_case(s: &str) -> String {
let mut result = String::new();
for (i, c) in s.chars().enumerate() {
if c.is_ascii_uppercase() && i > 0 {
result.push('_');
}
result.push(c.to_ascii_uppercase());
}
result
}
fn to_kebab_case(s: &str) -> String {
let mut result = String::new();
for (i, c) in s.chars().enumerate() {
if c == '_' {
result.push('-');
} else if c.is_ascii_uppercase() {
if i > 0 {
result.push('-');
}
result.push(c.to_ascii_lowercase());
} else {
result.push(c);
}
}
result
}
fn resolve_key(original: &str, field_attrs: &FieldAttrs, container_attrs: &ContainerAttrs) -> String {
if let Some(ref renamed) = field_attrs.rename {
renamed.clone()
} else if let Some(rule) = container_attrs.rename_all {
apply_rename_all(original, rule)
} else {
original.to_string()
}
}
#[proc_macro_derive(Serialize, attributes(marfy))]
pub fn derive_serialize(input: TokenStream) -> TokenStream {
derive_serialize_impl(input)
}
fn derive_serialize_impl(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let container_attrs = parse_container_attrs(&input.attrs);
let expanded = match &input.data {
Data::Struct(data) => derive_serialize_struct(name, &impl_generics, &ty_generics, where_clause, &container_attrs, data),
Data::Enum(data) => {
if container_attrs.untagged && container_attrs.tag.is_some() {
return syn::Error::new_spanned(
name,
"cannot combine #[marfy(untagged)] and #[marfy(tag = \"...\")]",
)
.to_compile_error()
.into();
} else if let Some(ref tag_field) = container_attrs.tag {
derive_serialize_internally_tagged_enum(name, &impl_generics, &ty_generics, where_clause, &container_attrs, data, tag_field)
} else {
derive_serialize_enum(name, &impl_generics, &ty_generics, where_clause, &container_attrs, data)
}
}
_ => {
return syn::Error::new_spanned(name, "Serialize can only be derived for structs or enums")
.to_compile_error()
.into();
}
};
expanded.into()
}
fn derive_serialize_struct(
name: &syn::Ident,
impl_generics: &syn::ImplGenerics,
ty_generics: &syn::TypeGenerics,
where_clause: Option<&syn::WhereClause>,
container_attrs: &ContainerAttrs,
data: &syn::DataStruct,
) -> proc_macro2::TokenStream {
let fields = match &data.fields {
Fields::Named(fields) => &fields.named,
_ => {
return syn::Error::new_spanned(
name,
"Serialize can only be derived for structs with named fields",
)
.to_compile_error();
}
};
let mut destructure_bindings = Vec::new();
let mut entry_stmts = Vec::new();
for f in fields.iter() {
let field_name = f.ident.as_ref().unwrap();
let field_attrs = parse_field_attrs(&f.attrs);
if field_attrs.skip {
destructure_bindings.push(quote! { #field_name: _ });
continue;
}
destructure_bindings.push(quote! { #field_name });
let body = if field_attrs.flatten {
quote! {
{
let inner = marfy::Serialize::into_node(#field_name);
if let marfy::Node::Mapping(m) = inner {
entries.extend(m.entries);
}
}
}
} else {
let key = resolve_key(&field_name.to_string(), &field_attrs, container_attrs);
quote! {
entries.push((
Some(marfy::Node::String(marfy::StringNode {
value: #key.to_string(),
comment: None,
})),
marfy::Serialize::into_node(#field_name),
));
}
};
if let Some(ref predicate_str) = field_attrs.skip_serializing_if {
let predicate: syn::ExprPath = syn::parse_str(predicate_str)
.expect("skip_serializing_if: invalid path");
entry_stmts.push(quote! {
if !#predicate(&#field_name) {
#body
}
});
} else {
entry_stmts.push(body);
}
}
quote! {
impl #impl_generics marfy::Serialize for #name #ty_generics #where_clause {
fn into_node(self) -> marfy::Node {
let #name { #(#destructure_bindings),* } = self;
let mut entries: Vec<(Option<marfy::Node>, marfy::Node)> = Vec::new();
#(#entry_stmts)*
marfy::Node::Mapping(marfy::MappingNode {
entries,
comment: None,
})
}
}
}
}
fn derive_serialize_enum(
name: &syn::Ident,
impl_generics: &syn::ImplGenerics,
ty_generics: &syn::TypeGenerics,
where_clause: Option<&syn::WhereClause>,
container_attrs: &ContainerAttrs,
data: &syn::DataEnum,
) -> proc_macro2::TokenStream {
let variant_arms: Vec<_> = data
.variants
.iter()
.map(|variant| {
let variant_name = &variant.ident;
let variant_field_attrs = parse_field_attrs(&variant.attrs);
let key = resolve_key(&variant_name.to_string(), &variant_field_attrs, container_attrs);
match &variant.fields {
Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
quote! {
#name::#variant_name(inner) => {
marfy::Node::Mapping(marfy::MappingNode {
entries: vec![(
Some(marfy::Node::String(marfy::StringNode {
value: #key.to_string(),
comment: None,
})),
marfy::Serialize::into_node(inner),
)],
comment: None,
})
}
}
}
Fields::Unit => {
quote! {
#name::#variant_name => {
marfy::Node::String(marfy::StringNode {
value: #key.to_string(),
comment: None,
})
}
}
}
_ => {
syn::Error::new_spanned(variant, "Serialize for enums: unsupported variant type")
.to_compile_error()
}
}
})
.collect();
quote! {
impl #impl_generics marfy::Serialize for #name #ty_generics #where_clause {
fn into_node(self) -> marfy::Node {
match self {
#(#variant_arms),*
}
}
}
}
}
fn derive_serialize_internally_tagged_enum(
name: &syn::Ident,
impl_generics: &syn::ImplGenerics,
ty_generics: &syn::TypeGenerics,
where_clause: Option<&syn::WhereClause>,
container_attrs: &ContainerAttrs,
data: &syn::DataEnum,
tag_field: &str,
) -> proc_macro2::TokenStream {
let variant_arms: Vec<_> = data
.variants
.iter()
.map(|variant| {
let variant_name = &variant.ident;
let variant_field_attrs = parse_field_attrs(&variant.attrs);
let key = resolve_key(&variant_name.to_string(), &variant_field_attrs, container_attrs);
match &variant.fields {
Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
quote! {
#name::#variant_name(inner) => {
let inner_node = marfy::Serialize::into_node(inner);
match inner_node {
marfy::Node::Mapping(mut m) => {
m.entries.insert(0, (
Some(marfy::Node::String(marfy::StringNode {
value: #tag_field.to_string(),
comment: None,
})),
marfy::Node::String(marfy::StringNode {
value: #key.to_string(),
comment: None,
}),
));
marfy::Node::Mapping(m)
}
_ => panic!("internally tagged enum variant {} must serialize to a Mapping", #key),
}
}
}
}
Fields::Unit => {
quote! {
#name::#variant_name => {
marfy::Node::Mapping(marfy::MappingNode {
entries: vec![(
Some(marfy::Node::String(marfy::StringNode {
value: #tag_field.to_string(),
comment: None,
})),
marfy::Node::String(marfy::StringNode {
value: #key.to_string(),
comment: None,
}),
)],
comment: None,
})
}
}
}
_ => {
syn::Error::new_spanned(variant, "Serialize for internally tagged enums: unsupported variant type")
.to_compile_error()
}
}
})
.collect();
quote! {
impl #impl_generics marfy::Serialize for #name #ty_generics #where_clause {
fn into_node(self) -> marfy::Node {
match self {
#(#variant_arms),*
}
}
}
}
}
#[proc_macro_derive(Deserialize, attributes(marfy))]
pub fn derive_deserialize(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let container_attrs = parse_container_attrs(&input.attrs);
match &input.data {
Data::Struct(data) => {
derive_deserialize_struct(name, &impl_generics, &ty_generics, where_clause, &container_attrs, data)
}
Data::Enum(data) => {
if container_attrs.untagged && container_attrs.tag.is_some() {
return syn::Error::new_spanned(
name,
"cannot combine #[marfy(untagged)] and #[marfy(tag = \"...\")]",
)
.to_compile_error()
.into();
} else if container_attrs.untagged {
derive_deserialize_untagged_enum(name, &impl_generics, &ty_generics, where_clause, data)
} else if let Some(ref tag_field) = container_attrs.tag {
derive_deserialize_internally_tagged_enum(name, &impl_generics, &ty_generics, where_clause, &container_attrs, data, tag_field)
} else {
derive_deserialize_externally_tagged_enum(name, &impl_generics, &ty_generics, where_clause, &container_attrs, data)
}
}
_ => syn::Error::new_spanned(name, "Deserialize can only be derived for structs or enums")
.to_compile_error()
.into(),
}
}
fn derive_deserialize_struct(
name: &syn::Ident,
impl_generics: &syn::ImplGenerics,
ty_generics: &syn::TypeGenerics,
where_clause: Option<&syn::WhereClause>,
container_attrs: &ContainerAttrs,
data: &syn::DataStruct,
) -> TokenStream {
let fields = match &data.fields {
Fields::Named(fields) => &fields.named,
_ => {
return syn::Error::new_spanned(
name,
"Deserialize can only be derived for structs with named fields",
)
.to_compile_error()
.into();
}
};
let field_extractions: Vec<_> = fields
.iter()
.map(|f| {
let field_name = f.ident.as_ref().unwrap();
let field_attrs = parse_field_attrs(&f.attrs);
let field_ty = &f.ty;
if field_attrs.skip {
return quote! {
#field_name: Default::default()
};
}
if field_attrs.flatten {
return quote! {
#field_name: marfy::Deserialize::from_node(node)?
};
}
let key = resolve_key(&field_name.to_string(), &field_attrs, container_attrs);
if is_option_type(field_ty) {
quote! {
#field_name: match node.get(#key) {
Some(n) if !n.is_null() => Some(marfy::Deserialize::from_node(n)?),
_ => None,
}
}
} else {
quote! {
#field_name: {
let n = node.get(#key).ok_or_else(|| {
marfy::Error::Deserialize(
format!("missing field: {}", #key)
)
})?;
marfy::Deserialize::from_node(n)?
}
}
}
})
.collect();
let expanded = quote! {
impl #impl_generics marfy::Deserialize for #name #ty_generics #where_clause {
fn from_node(node: &marfy::Node) -> Result<Self, marfy::Error> {
let _ = node.as_mapping().ok_or_else(|| {
marfy::Error::Deserialize("expected mapping".into())
})?;
Ok(#name {
#(#field_extractions),*
})
}
}
};
expanded.into()
}
fn derive_deserialize_untagged_enum(
name: &syn::Ident,
impl_generics: &syn::ImplGenerics,
ty_generics: &syn::TypeGenerics,
where_clause: Option<&syn::WhereClause>,
data: &syn::DataEnum,
) -> TokenStream {
let variant_attempts: Vec<_> = data
.variants
.iter()
.map(|variant| {
let variant_name = &variant.ident;
match &variant.fields {
Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
let inner_ty = &fields.unnamed[0].ty;
quote! {
if let Ok(v) = <#inner_ty as marfy::Deserialize>::from_node(node) {
return Ok(#name::#variant_name(v));
}
}
}
_ => {
let err_msg = format!(
"Deserialize for untagged enums only supports newtype variants, \
but {} has unsupported fields",
variant_name
);
syn::Error::new_spanned(variant, err_msg)
.to_compile_error()
}
}
})
.collect();
let err_msg = format!("no variant matched for {}", name);
let expanded = quote! {
impl #impl_generics marfy::Deserialize for #name #ty_generics #where_clause {
fn from_node(node: &marfy::Node) -> Result<Self, marfy::Error> {
#(#variant_attempts)*
Err(marfy::Error::Deserialize(#err_msg.into()))
}
}
};
expanded.into()
}
fn derive_deserialize_externally_tagged_enum(
name: &syn::Ident,
impl_generics: &syn::ImplGenerics,
ty_generics: &syn::TypeGenerics,
where_clause: Option<&syn::WhereClause>,
container_attrs: &ContainerAttrs,
data: &syn::DataEnum,
) -> TokenStream {
let variant_arms: Vec<_> = data
.variants
.iter()
.map(|variant| {
let variant_name = &variant.ident;
let variant_field_attrs = parse_field_attrs(&variant.attrs);
let key = resolve_key(&variant_name.to_string(), &variant_field_attrs, container_attrs);
match &variant.fields {
Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
let inner_ty = &fields.unnamed[0].ty;
quote! {
#key => {
Ok(#name::#variant_name(
<#inner_ty as marfy::Deserialize>::from_node(value)?
))
}
}
}
Fields::Unit => {
quote! {
#key => {
Ok(#name::#variant_name)
}
}
}
_ => {
let err_msg = format!(
"Deserialize for externally tagged enums: variant {} has unsupported fields",
variant_name
);
syn::Error::new_spanned(variant, err_msg)
.to_compile_error()
}
}
})
.collect();
let err_msg = format!("unknown variant for {}", name);
let expanded = quote! {
impl #impl_generics marfy::Deserialize for #name #ty_generics #where_clause {
fn from_node(node: &marfy::Node) -> Result<Self, marfy::Error> {
let mapping = node.as_mapping().ok_or_else(|| {
marfy::Error::Deserialize("expected mapping for externally tagged enum".into())
})?;
let (key, value) = mapping.entries.iter()
.find(|(k, _)| k.is_some())
.ok_or_else(|| {
marfy::Error::Deserialize("empty mapping for externally tagged enum".into())
})?;
let tag = key.as_ref().unwrap().as_str().ok_or_else(|| {
marfy::Error::Deserialize("expected string key for enum variant".into())
})?;
match tag {
#(#variant_arms)*
_ => Err(marfy::Error::Deserialize(
format!("{}: {}", #err_msg, tag)
)),
}
}
}
};
expanded.into()
}
fn derive_deserialize_internally_tagged_enum(
name: &syn::Ident,
impl_generics: &syn::ImplGenerics,
ty_generics: &syn::TypeGenerics,
where_clause: Option<&syn::WhereClause>,
container_attrs: &ContainerAttrs,
data: &syn::DataEnum,
tag_field: &str,
) -> TokenStream {
let variant_arms: Vec<_> = data
.variants
.iter()
.map(|variant| {
let variant_name = &variant.ident;
let variant_field_attrs = parse_field_attrs(&variant.attrs);
let key = resolve_key(&variant_name.to_string(), &variant_field_attrs, container_attrs);
match &variant.fields {
Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
let inner_ty = &fields.unnamed[0].ty;
quote! {
#key => {
Ok(#name::#variant_name(
<#inner_ty as marfy::Deserialize>::from_node(node)?
))
}
}
}
Fields::Unit => {
quote! {
#key => {
Ok(#name::#variant_name)
}
}
}
_ => {
let err_msg = format!(
"Deserialize for internally tagged enums: variant {} has unsupported fields",
variant_name
);
syn::Error::new_spanned(variant, err_msg)
.to_compile_error()
}
}
})
.collect();
let err_msg = format!("unknown variant for {}", name);
let missing_tag_msg = format!("missing tag field: {}", tag_field);
let invalid_tag_msg = format!("tag field '{}' is not a string", tag_field);
let expanded = quote! {
impl #impl_generics marfy::Deserialize for #name #ty_generics #where_clause {
fn from_node(node: &marfy::Node) -> Result<Self, marfy::Error> {
let tag_node = node.get(#tag_field).ok_or_else(|| {
marfy::Error::Deserialize(#missing_tag_msg.into())
})?;
let tag = tag_node.as_str().ok_or_else(|| {
marfy::Error::Deserialize(#invalid_tag_msg.into())
})?;
match tag {
#(#variant_arms)*
_ => Err(marfy::Error::Deserialize(
format!("{}: {}", #err_msg, tag)
)),
}
}
}
};
expanded.into()
}
fn is_option_type(ty: &Type) -> bool {
if let Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.last() {
return segment.ident == "Option";
}
}
false
}