mirror of
https://github.com/bytedream/serde-inline-default.git
synced 2026-02-04 13:26:27 +01:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ba08395a42 | |||
| 5427bdffe7 | |||
| ce70ea4365 |
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "serde-inline-default"
|
name = "serde-inline-default"
|
||||||
version = "1.0.0"
|
version = "1.0.1"
|
||||||
authors = ["bytedream"]
|
authors = ["bytedream"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Serde default values via inline declaration"
|
description = "Serde default values via inline declaration"
|
||||||
|
|||||||
@@ -1,55 +1,70 @@
|
|||||||
use crate::utils::type_lifetimes_to_static;
|
|
||||||
use proc_macro2::{Ident, Span, TokenStream};
|
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::{parse_quote, ItemStruct};
|
use syn::{spanned::Spanned, Error, Fields, ItemEnum, ItemStruct};
|
||||||
|
|
||||||
|
use crate::utils::{check_field_for_default_expr, ATTR_NAME, DEFAULT_FN_PREFIX};
|
||||||
|
|
||||||
pub(crate) fn expand_struct(mut item: ItemStruct) -> proc_macro::TokenStream {
|
pub(crate) fn expand_struct(mut item: ItemStruct) -> proc_macro::TokenStream {
|
||||||
let mut inline_fns: Vec<TokenStream> = vec![];
|
let mut default_fns = vec![];
|
||||||
|
|
||||||
for (i, field) in item.fields.iter_mut().enumerate() {
|
for (i, field) in item.fields.iter_mut().enumerate() {
|
||||||
for (j, attr) in field.attrs.iter_mut().enumerate() {
|
default_fns.extend(check_field_for_default_expr(field, || {
|
||||||
if !attr.path().is_ident("serde_inline_default") {
|
format!("{}_{}_Field{}", DEFAULT_FN_PREFIX, item.ident, i)
|
||||||
continue;
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
#( #default_fns )*
|
||||||
|
|
||||||
|
#item
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn expand_enum(mut item: ItemEnum) -> proc_macro::TokenStream {
|
||||||
|
let mut default_fns = vec![];
|
||||||
|
|
||||||
|
for (i, variant) in item.variants.iter_mut().enumerate() {
|
||||||
|
if variant.attrs.iter().any(|a| a.path().is_ident(ATTR_NAME)) {
|
||||||
|
return Error::new(
|
||||||
|
variant.span(),
|
||||||
|
format!(
|
||||||
|
"#[{}] can only be used on named enum variant fields",
|
||||||
|
ATTR_NAME
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.to_compile_error()
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
let fields = match &mut variant.fields {
|
||||||
|
Fields::Named(fields) => fields,
|
||||||
|
_ => {
|
||||||
|
return Error::new(
|
||||||
|
variant.span(),
|
||||||
|
format!(
|
||||||
|
"#[{}] can only be used on named enum variant fields",
|
||||||
|
ATTR_NAME
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.to_compile_error()
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let default: TokenStream = attr.parse_args().unwrap();
|
for (j, field) in fields.named.iter_mut().enumerate() {
|
||||||
|
default_fns.extend(check_field_for_default_expr(field, || {
|
||||||
// copy all the same #[cfg] conditional compilations flags for the field onto our built
|
format!(
|
||||||
// default function.
|
"{}_{}_Variant{}_Field{}",
|
||||||
// otherwise, it's possible to create a constructor for a type that may be filtered by
|
DEFAULT_FN_PREFIX, item.ident, i, j
|
||||||
// the same #[cfg]'s, breaking compilation
|
)
|
||||||
let cfg_attrs = field.attrs.iter().filter(|a| a.path().is_ident("cfg"));
|
}));
|
||||||
|
|
||||||
let fn_name_lit = format!("__serde_inline_default_{}_{}", item.ident, i);
|
|
||||||
let fn_name_ident = Ident::new(&fn_name_lit, Span::call_site());
|
|
||||||
let mut return_type = field.ty.clone();
|
|
||||||
|
|
||||||
// replace lifetimes with 'static.
|
|
||||||
// the built default function / default values in general can only be static as they're
|
|
||||||
// generated without reference to the parent struct
|
|
||||||
type_lifetimes_to_static(&mut return_type);
|
|
||||||
|
|
||||||
inline_fns.push(quote! {
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
#( #cfg_attrs )*
|
|
||||||
fn #fn_name_ident () -> #return_type {
|
|
||||||
#default
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
field.attrs.remove(j);
|
|
||||||
field
|
|
||||||
.attrs
|
|
||||||
.insert(j, parse_quote!( #[serde(default = #fn_name_lit)] ));
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let expanded = quote! {
|
quote! {
|
||||||
#( #inline_fns )*
|
#( #default_fns )*
|
||||||
|
|
||||||
#item
|
#item
|
||||||
};
|
}
|
||||||
expanded.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/lib.rs
12
src/lib.rs
@@ -1,7 +1,7 @@
|
|||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
|
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use syn::{parse_macro_input, Item};
|
use syn::{parse_macro_input, spanned::Spanned, Error, Item};
|
||||||
|
|
||||||
mod expand;
|
mod expand;
|
||||||
mod utils;
|
mod utils;
|
||||||
@@ -32,11 +32,17 @@ mod utils;
|
|||||||
/// [`Serialize`]: https://docs.rs/serde/*/serde/trait.Serialize.html
|
/// [`Serialize`]: https://docs.rs/serde/*/serde/trait.Serialize.html
|
||||||
/// [`Deserialize`]: https://docs.rs/serde/*/serde/trait.Deserialize.html
|
/// [`Deserialize`]: https://docs.rs/serde/*/serde/trait.Deserialize.html
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn serde_inline_default(_attr: TokenStream, input: TokenStream) -> TokenStream {
|
pub fn serde_inline_default(attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||||
let item = parse_macro_input!(input as Item);
|
let item = parse_macro_input!(input as Item);
|
||||||
|
|
||||||
match item {
|
match item {
|
||||||
Item::Struct(s) => expand::expand_struct(s),
|
Item::Struct(s) => expand::expand_struct(s),
|
||||||
_ => panic!("can only be used on structs"),
|
Item::Enum(e) => expand::expand_enum(e),
|
||||||
|
_ => Error::new(
|
||||||
|
proc_macro2::TokenStream::from(attr).span(),
|
||||||
|
"can only be used on structs and enums",
|
||||||
|
)
|
||||||
|
.to_compile_error()
|
||||||
|
.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
49
src/utils.rs
49
src/utils.rs
@@ -1,4 +1,9 @@
|
|||||||
use syn::{parse_quote, GenericArgument, PathArguments, Type};
|
use proc_macro2::{Span, TokenStream};
|
||||||
|
use quote::quote;
|
||||||
|
use syn::{parse_quote, Field, GenericArgument, Ident, PathArguments, Type};
|
||||||
|
|
||||||
|
pub(crate) const ATTR_NAME: &str = "serde_inline_default";
|
||||||
|
pub(crate) const DEFAULT_FN_PREFIX: &str = "__serde_inline_default";
|
||||||
|
|
||||||
pub(crate) fn type_lifetimes_to_static(ty: &mut Type) {
|
pub(crate) fn type_lifetimes_to_static(ty: &mut Type) {
|
||||||
match ty {
|
match ty {
|
||||||
@@ -38,3 +43,45 @@ pub(crate) fn type_lifetimes_to_static(ty: &mut Type) {
|
|||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn check_field_for_default_expr(
|
||||||
|
field: &mut Field,
|
||||||
|
identifier_fn: impl FnOnce() -> String,
|
||||||
|
) -> Option<TokenStream> {
|
||||||
|
for (i, attr) in field.attrs.iter_mut().enumerate() {
|
||||||
|
if !attr.path().is_ident(ATTR_NAME) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let default_expr: TokenStream = attr.parse_args().unwrap();
|
||||||
|
|
||||||
|
// copy all the same #[cfg] conditional compilations flags for the field onto our built
|
||||||
|
// default function.
|
||||||
|
// otherwise, it's possible to create a constructor for a type that may be filtered by
|
||||||
|
// the same #[cfg]'s, breaking compilation
|
||||||
|
let cfg_attrs = field.attrs.iter().filter(|a| a.path().is_ident("cfg"));
|
||||||
|
|
||||||
|
let default_fn_lit = identifier_fn();
|
||||||
|
let default_fn_ident = Ident::new(&default_fn_lit, Span::call_site());
|
||||||
|
let mut return_type = field.ty.clone();
|
||||||
|
|
||||||
|
// replace lifetimes with 'static.
|
||||||
|
// the built default function / default values in general can only be static as they're
|
||||||
|
// generated without reference to the parent struct
|
||||||
|
type_lifetimes_to_static(&mut return_type);
|
||||||
|
|
||||||
|
let default_fn_expr = quote! {
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#( #cfg_attrs )*
|
||||||
|
fn #default_fn_ident () -> #return_type {
|
||||||
|
#default_expr
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
field.attrs[i] = parse_quote!( #[serde(default = #default_fn_lit)] );
|
||||||
|
return Some(default_fn_expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|||||||
20
tests/enum.rs
Normal file
20
tests/enum.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
use serde_inline_default::serde_inline_default;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn enum_default() {
|
||||||
|
#[serde_inline_default]
|
||||||
|
#[derive(Debug, PartialEq, Eq, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum Test {
|
||||||
|
VariantWithFields {
|
||||||
|
#[serde_inline_default(255)]
|
||||||
|
test_int: u8,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
let enum_test: Test = serde_json::from_value(json!({"VariantWithFields": {}})).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(enum_test, Test::VariantWithFields { test_int: 255 })
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_inline_default::serde_inline_default;
|
use serde_inline_default::serde_inline_default;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_serde_inline_default() {
|
fn struct_normal() {
|
||||||
fn native_default() -> u32 {
|
fn native_default() -> u32 {
|
||||||
69
|
69
|
||||||
}
|
}
|
||||||
@@ -31,7 +32,7 @@ fn test_serde_inline_default() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_lifetime() {
|
fn lifetime() {
|
||||||
#[serde_inline_default]
|
#[serde_inline_default]
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct LifetimeTest<'a> {
|
struct LifetimeTest<'a> {
|
||||||
@@ -46,7 +47,7 @@ fn test_lifetime() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn test_conditional_compilation() {
|
fn conditional_compilation() {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct TypeA(u8);
|
struct TypeA(u8);
|
||||||
Reference in New Issue
Block a user