Skip to content

Commit

Permalink
feat: improve logic
Browse files Browse the repository at this point in the history
  • Loading branch information
murar8 committed Feb 3, 2024
1 parent d8c545b commit 8e7b344
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 181 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ keywords = ["serde"]
license = "MIT"
name = "serde_nested_with"
repository = "https://github.com/murar8/serde_nested_with"
version = "0.1.2"
version = "0.2.0"
19 changes: 5 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@
[![.github/workflows/audit.yml](https://github.com/murar8/serde_nested_with/actions/workflows/audit.yml/badge.svg)](https://github.com/murar8/serde_nested_with/actions/workflows/audit.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

This is a small procedural macro that allows you to use serde's `with`, `serialize_with` and
`deserialize_with` attributes with a nested module or function. This is useful when you want to
use a custom (de)serializer that is defined in a different module or crate.
This is a small procedural macro that allows you to use serde attributes with a nested module or function. This is useful when you want to use a custom (de)serializer that is defined in a different module or crate.

## Installation

Expand All @@ -22,29 +20,22 @@ cargo add serde_nested_with
mod example {
use serde::{Deserialize, Serialize};
use serde_test::{assert_tokens, Token};
use serde_nested_with::serde_nested_with;
use serde_nested_with::serde_nested;
use std::collections::BTreeMap;
use time::serde::rfc3339;
use time::OffsetDateTime;

#[serde_nested_with]
#[serde_nested]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Foo {
#[serde_nested_with(substitute = "Option<Option<_>>", with = "rfc3339")]
#[serde_nested(sub = "OffsetDateTime", serde(with = "rfc3339"))]
pub bar: Option<Option<OffsetDateTime>>,
#[serde_nested_with(substitute = "Option<BTreeMap<i32, _>>", with = "rfc3339")]
#[serde_nested(sub = "OffsetDateTime", serde(with = "rfc3339"))]
pub baz: Option<BTreeMap<i32, OffsetDateTime>>,
}
}
```

## Limitations

- This macro only works with the `with`, `serialize_with` and `deserialize_with` attributes. It
does not work with any other serde attribute.

- Only one of the fields can be substituted in case multiple generics are present.

## Release process

When a [SemVer](https://semver.org/) compatible git tag is pushed to the repo a new version of the package will be published to [crates.io](https://crates.io/crates/serde_nested_with).
Expand Down
186 changes: 43 additions & 143 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,170 +2,94 @@

use darling::FromField;
use proc_macro::TokenStream;
use proc_macro_error::{abort, proc_macro_error};
use proc_macro_error::proc_macro_error;
use quote::{quote, ToTokens};
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::hash::{Hash as _, Hasher};
use syn::spanned::Spanned;

const ATTRIBUTE_NAME: &str = "serde_nested_with";
const WRAPPER_NAME: &str = "__Wrapper";
const ATTRIBUTE_NAME: &str = "serde_nested";

#[derive(FromField)]
#[darling(attributes(serde_nested_with))]
#[derive(Debug, FromField)]
#[darling(attributes(serde_nested))]
struct Field {
ty: syn::Type,
substitute: syn::Path,
with: Option<String>,
serialize_with: Option<String>,
deserialize_with: Option<String>,
sub: syn::Path,
serde: syn::Meta,
}

impl Field {
/// Returns the name of the argument that should be passed to the serde operation.
fn serde_operation(&self) -> &str {
match self {
Field { serialize_with: Some(_), deserialize_with: Some(_), .. } => "with",
Field { with: Some(_), .. } => "with",
Field { serialize_with: Some(_), .. } => "serialize_with",
Field { deserialize_with: Some(_), .. } => "deserialize_with",
_ => abort!(self.substitute.span(), "missing serde operation"),
}
}

/// Returns the name of the serde operation as a syn identifier.
fn serde_operation_ident(&self) -> syn::Ident {
syn::Ident::new(self.serde_operation(), self.ty.span())
fn module_name(&self) -> String {
let hasher = &mut DefaultHasher::new();
self.ty.hash(hasher);
self.sub.hash(hasher);
self.serde.hash(hasher);
format!("__serde_nested_{}", hasher.finish())
}

fn helper_module_name(&self) -> String {
let base_name = self.outer_module_base_name();
base_name + "_helper"
fn wrapper_type(&self) -> String {
let ty = self.ty.to_token_stream().to_string();
let sub = self.sub.to_token_stream().to_string();
ty.replace(&sub, WRAPPER_NAME)
}

/// Returns the name of the generated module that will be used for (de)serialization.
fn outer_module_base_name(&self) -> String {
let mut hasher = DefaultHasher::new();
self.substitute.to_token_stream().to_string().hash(&mut hasher);
self.with.hash(&mut hasher);
self.serialize_with.hash(&mut hasher);
self.deserialize_with.hash(&mut hasher);
format!("__serde_nested_with_{}", hasher.finish())
fn wrapper_type_ident(&self) -> syn::Path {
let ty = self.wrapper_type();
syn::parse_str(&ty).unwrap()
}

/// Returns the path to the generated module or function that will be used for
/// (de)serialization.
fn outer_module_name_with_op(&self) -> String {
let base_name = self.outer_module_base_name();
match self {
Field { serialize_with: Some(_), deserialize_with: Some(_), .. } => base_name,
Field { with: Some(_), .. } => base_name,
Field { serialize_with: Some(_), .. } => base_name + "::serialize",
Field { deserialize_with: Some(_), .. } => base_name + "::deserialize",
_ => abort!(self.substitute.span(), "missing serde operation"),
}
}

/// Returns the path to the user provided module or function that will be used for
/// (de)serialization.
fn inner_module_name_with_op(&self) -> String {
let helper_name = self.helper_module_name();
match self {
Field { serialize_with: Some(_), deserialize_with: Some(_), .. } => helper_name,
Field { with: Some(path), .. } => path.clone(),
Field { serialize_with: Some(path), .. } => path.clone(),
Field { deserialize_with: Some(path), .. } => path.clone(),
_ => abort!(self.ty.span(), "missing serde operation"),
}
}

/// Extracts the generic argument that is indicated by the placeholder `_`.
fn generic_argument(&self) -> syn::Path {
let full_ty = self.ty.to_token_stream().to_string();
let sub_ty = self.substitute.to_token_stream().to_string();
let (prefix, suffix) = match sub_ty.split_once('_') {
Some(res) => res,
None => abort!(self.substitute.span(), "missing placeholder `_`"),
};
let generic = full_ty
.strip_prefix(prefix)
.and_then(|s| s.strip_suffix(suffix))
.and_then(|s| syn::parse_str(s).ok());
match generic {
Some(generic) => generic,
None => abort!(self.substitute.span(), "placeholder does not match the type"),
}
}

/// Plugs the provided generic argument in place of the placeholder `_`.
fn plug_generic_argument(&self, generic_argument: &str) -> syn::Path {
let ty = self.substitute.to_token_stream().to_string();
let ty = ty.replace('_', generic_argument);
match syn::parse_str(&ty) {
Ok(path) => path,
Err(_) => abort!(self.substitute.span(), "placeholder does not match the type"),
}
}

/// Plugs the provided generic argument in place of the placeholder `_` and adds turbofish
/// syntax.
fn plug_generic_argument_turbofish(&self, generic_argument: &str) -> syn::Path {
let ty = self.substitute.to_token_stream().to_string();
let ty = ty.replacen('<', ":: <", 1);
let ty = ty.replace('_', generic_argument);
match syn::parse_str(&ty) {
Ok(path) => path,
Err(_) => abort!(self.substitute.span(), "placeholder does not match the type"),
}
fn wrapper_type_turbofish(&self) -> syn::Path {
let ty = self.wrapper_type();
let ty = ty.replacen('<', " :: <", 1);
syn::parse_str(&ty).unwrap()
}
}

#[proc_macro_error]
#[proc_macro_attribute]
pub fn serde_nested_with(_: TokenStream, input: TokenStream) -> TokenStream {
pub fn serde_nested(_: TokenStream, input: TokenStream) -> TokenStream {
let mut input = syn::parse_macro_input!(input as syn::ItemStruct);
let mut modules = HashMap::new();
let mut fields = HashMap::new();

for field in input.fields.iter_mut() {
let attrs = match Field::from_field(field) {
Ok(attrs) => attrs,
let info = match Field::from_field(field) {
Ok(field) => field,
Err(_) => continue,
};
for attr in field.attrs.iter_mut() {
if let syn::Meta::List(ref mut list) = attr.meta {
if let Some(syn::PathSegment { ident, .. }) = list.path.segments.first_mut() {
if ident == ATTRIBUTE_NAME {
*ident = syn::parse_quote!(serde);
let serde_operation_ident = attrs.serde_operation_ident();
let outer_module_name_with_op = attrs.outer_module_name_with_op();
list.tokens =
quote! { #serde_operation_ident = #outer_module_name_with_op };
modules.insert(attrs.outer_module_base_name(), attrs);
let module_name = info.module_name();
list.tokens = quote! { with = #module_name };
fields.insert(module_name, info);
break;
}
}
}
}
}

let convert_modules = modules.iter().map(|(outer_module_name, attrs)| {
let outer_module_name = syn::Ident::new(outer_module_name, attrs.ty.span());
let inner_module_name = &attrs.inner_module_name_with_op();
let field_ty = &attrs.ty;
let operation = attrs.serde_operation_ident();
let generic_argument = attrs.generic_argument();
let wrapper_type = attrs.plug_generic_argument("__Wrapper");
let wrapper_type_turbofish = attrs.plug_generic_argument_turbofish("__Wrapper");
let modules = fields.into_iter().map(|(module_name, field)| {
let module_name = syn::Ident::new(&module_name, input.span());
let field_serde_attr = &field.serde;
let field_sub = &field.sub;
let field_ty = &field.ty;
let wrapper_type = field.wrapper_type_ident();
let wrapper_type_turbofish = field.wrapper_type_turbofish();

quote! {
mod #outer_module_name {
mod #module_name {
use super::*;
use serde::{Serialize as _, Deserialize as _};

#[derive(serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
#[repr(transparent)]
struct __Wrapper(#[serde(#operation=#inner_module_name)] #generic_argument);
struct __Wrapper(#[#field_serde_attr] #field_sub);

pub fn serialize<S: serde::Serializer>(
val: &#field_ty,
Expand All @@ -191,34 +115,10 @@ pub fn serde_nested_with(_: TokenStream, input: TokenStream) -> TokenStream {
}
});

let helper_modules = modules.values().map(|attrs| {
let helper_module_name = syn::Ident::new(&attrs.helper_module_name(), attrs.ty.span());
let serialize_with =
match attrs.serialize_with.as_ref().map(|s| syn::parse_str::<syn::Expr>(s)) {
Some(Ok(s)) => quote! { pub use super::#s; },
None => quote! {},
Some(Err(_)) => abort!(attrs.ty, "failed to parse serialize_with"),
};
let deserialize_with =
match attrs.deserialize_with.as_ref().map(|s| syn::parse_str::<syn::Expr>(s)) {
Some(Ok(s)) => quote! { pub use super::#s; },
None => quote! {},
Some(Err(_)) => abort!(attrs.ty, "failed to parse deserialize_with"),
};

quote! {
mod #helper_module_name {
use super::*;
#serialize_with
#deserialize_with
}
}
});

let output = quote! {
#(#helper_modules)*
#(#convert_modules)*
#( #modules )*
#input
};

output.into()
}
11 changes: 5 additions & 6 deletions tests/test_both.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
use serde::{Deserialize, Serialize};
use serde_nested_with::serde_nested_with;
use serde_nested_with::serde_nested;
use serde_test::{assert_tokens, Token};
use time::serde::rfc3339;
use time::OffsetDateTime;

#[serde_nested_with]
#[serde_nested]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Foo {
#[serde_nested_with(
substitute = "Option<_>",
serialize_with = "rfc3339::serialize",
deserialize_with = "rfc3339::deserialize"
#[serde_nested(
sub = "OffsetDateTime",
serde(serialize_with = "rfc3339::serialize", deserialize_with = "rfc3339::deserialize")
)]
pub bar: Option<OffsetDateTime>,
}
Expand Down
8 changes: 4 additions & 4 deletions tests/test_deserialize_with.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
use serde::Deserialize;
use serde_nested_with::serde_nested_with;
use serde_nested_with::serde_nested;
use serde_test::{assert_de_tokens, Token};
use time::serde::rfc3339;
use time::OffsetDateTime;

#[serde_nested_with]
#[serde_nested]
#[derive(Debug, PartialEq, Eq, Deserialize)]
pub struct Foo {
#[serde_nested_with(substitute = "Option<_>", deserialize_with = "rfc3339::deserialize")]
#[serde_nested(sub = "OffsetDateTime", serde(deserialize_with = "rfc3339::deserialize"))]
pub bar1: Option<OffsetDateTime>,
#[serde_nested_with(substitute = "Vec<Option<_>>", deserialize_with = "rfc3339::deserialize")]
#[serde_nested(sub = "OffsetDateTime", serde(deserialize_with = "rfc3339::deserialize"))]
pub bar2: Vec<Option<OffsetDateTime>>,
}

Expand Down
8 changes: 4 additions & 4 deletions tests/test_serialize_with.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
use serde::Serialize;
use serde_nested_with::serde_nested_with;
use serde_nested_with::serde_nested;
use serde_test::{assert_ser_tokens, Token};
use time::serde::rfc3339;
use time::OffsetDateTime;

#[serde_nested_with]
#[serde_nested]
#[derive(Debug, PartialEq, Eq, Serialize)]
pub struct Foo {
#[serde_nested_with(substitute = "Option<_>", serialize_with = "rfc3339::serialize")]
#[serde_nested(sub = "OffsetDateTime", serde(serialize_with = "rfc3339::serialize"))]
pub bar1: Option<OffsetDateTime>,
#[serde_nested_with(substitute = "Option<Option<_>>", serialize_with = "rfc3339::serialize")]
#[serde_nested(sub = "OffsetDateTime", serde(serialize_with = "rfc3339::serialize"))]
pub bar2: Option<Option<OffsetDateTime>>,
}

Expand Down

0 comments on commit 8e7b344

Please sign in to comment.