22 Commits
Author SHA1 Message Date
bytedream 6eea08da2d build: update version to 1.0.3 2026-10-07 23:27:12 +02:00
bytedream 473210c388 feat: suggest inlining generated default functions 2026-10-07 23:24:39 +02:00
bytedream 0cc4052eaf build: update version to 1.0.2 2026-07-21 11:57:40 +02:00
bytedream d197326c32 chore: replaced deprecated cargo publish token flag with env variable 2026-07-21 11:57:25 +02:00
bytedream 71cdef9d6e chore: add msrv 2026-07-21 11:51:32 +02:00
bytedream 6132d5369f build: update syn to version 3.0 2026-07-21 11:38:17 +02:00
bytedream 2865e49da3 chore: remove dependabot 2026-07-21 11:31:05 +02:00
bytedream a44606e99c chore: exclude unnecessary files from packaged crate 2026-07-06 13:02:24 +02:00
bytedream 5cafcf3e3f chore: only run ci on branch push 2026-01-24 16:24:13 +01:00
bytedream ba08395a42 build: update version to 1.0.1 2026-01-24 16:13:20 +01:00
bytedream 5427bdffe7 feat: generate compile error instead of panicing when macro is used on
unsupported item
2026-01-24 15:56:18 +01:00
bytedream ce70ea4365 feat: add support for named enum variant fields (#15) 2026-01-24 15:56:14 +01:00
bytedream 017080244a build: update version to 1.0.0 2025-09-02 14:08:16 +02:00
bytedream 12b209ad49 docs: update code documentation 2025-09-02 14:06:37 +02:00
bytedream 8cac0af5c0 docs: simplify readme 2025-09-02 14:04:36 +02:00
nullstalgia 8a863f3b32 Copy conditional compilation flags onto created constructors (#13)
(Closes #12)
2025-09-02 13:43:29 +02:00
bytedream 6c014e41a6 docs: update placement visibility (#11) 2025-04-22 14:26:00 +02:00
bytedream 5fbcd2eb14 docs: remove code usage comment 2025-04-11 20:59:15 +02:00
bytedream baeb1969fe docs: add alternatives 2025-04-11 20:42:14 +02:00
bytedream 0a12ee6b3f docs: add crates.io total downloads badge 2025-03-25 12:22:58 +01:00
bytedream 6f0f9f9799 build: update version to 0.2.3 2024-12-02 21:59:27 +01:00
bytedream 985eca392c fix: remove default function reusing 2024-12-02 21:58:22 +01:00
10 changed files with 263 additions and 94 deletions

No files matched your search

-6
View File
@@ -1,6 +0,0 @@
version: 2
updates:
- package-ecosystem: cargo
directory: /
schedule:
interval: weekly
+27 -14
View File
@@ -2,30 +2,43 @@ name: ci
on: on:
push: push:
branches:
- "**"
tags-ignore:
- "**"
pull_request: pull_request:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v6
- name: Generate cargo lockfile - name: Setup Rust
run: cargo generate-lockfile uses: dtolnay/rust-toolchain@stable
- name: Cargo cache
uses: actions/cache@v3
with: with:
path: | toolchain: stable
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Test - name: Test
run: cargo test run: cargo test
verify-msrv:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
- name: Install cargo-msrv
uses: taiki-e/install-action@v2
with:
tool: cargo-msrv
- name: Verify msrv
run: cargo msrv verify --all-features
+9 -3
View File
@@ -8,10 +8,16 @@ on:
jobs: jobs:
publish: publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v6
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
- name: Publish crate - name: Publish crate
run: cargo publish --token ${{ secrets.CRATES_TOKEN }} env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_TOKEN }}
run: cargo publish
+5 -3
View File
@@ -1,23 +1,25 @@
[package] [package]
name = "serde-inline-default" name = "serde-inline-default"
version = "0.2.2" version = "1.0.3"
authors = ["bytedream"] authors = ["bytedream"]
edition = "2021" edition = "2021"
rust-version = "1.71.0"
description = "Serde default values via inline declaration" description = "Serde default values via inline declaration"
readme = "README.md" readme = "README.md"
repository = "https://github.com/bytedream/serde-inline-default" repository = "https://github.com/bytedream/serde-inline-default"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
keywords = ["serde", "serialization"] keywords = ["serde", "serialization"]
categories = ["encoding"] categories = ["encoding"]
include = ["/Cargo.toml", "/LICENSE-APACHE", "/LICENSE-MIT", "/README.md", "/src/**", "/tests/**"]
[lib] [lib]
proc-macro = true
doctest = false doctest = false
proc-macro = true
[dependencies] [dependencies]
proc-macro2 = "1.0" proc-macro2 = "1.0"
quote = "1.0" quote = "1.0"
syn = { version = "2.0", features = ["full"] } syn = { version = "3.0", features = ["full"] }
[dev-dependencies] [dev-dependencies]
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
+52 -11
View File
@@ -1,12 +1,12 @@
# serde-inline-default [![ci](https://github.com/ByteDream/serde-inline-default/actions/workflows/ci.yml/badge.svg)](https://github.com/ByteDream/serde-inline-default/actions/workflows/ci.yml) [![crates.io](https://img.shields.io/crates/v/serde-inline-default)](https://crates.io/crates/serde-inline-default) [![docs](https://img.shields.io/docsrs/serde-inline-default)](https://docs.rs/serde-inline-default/latest/serde_inline_default/) # serde-inline-default [![ci](https://github.com/ByteDream/serde-inline-default/actions/workflows/ci.yml/badge.svg)](https://github.com/ByteDream/serde-inline-default/actions/workflows/ci.yml) [![crates.io](https://img.shields.io/crates/v/serde-inline-default)](https://crates.io/crates/serde-inline-default) [![crates.io downloads](https://img.shields.io/crates/d/serde-inline-default)](https://crates.io/crates/serde-inline-default) [![docs](https://img.shields.io/docsrs/serde-inline-default)](https://docs.rs/serde-inline-default/latest/serde_inline_default/)
Tiny crate to set default values for serde fields via inline attribute declaration. A tiny crate to set default values for serde struct fields via inline attribute declaration.
## Overview ## Overview
This crate is an approach to do what [serde-rs/serde#368](https://github.com/serde-rs/serde/issues/368) purposes. This crate is an approach to do what [serde-rs/serde#368](https://github.com/serde-rs/serde/issues/368) purposes: Defining default values for struct fields via inline declaration instead of creating a separate function for it.
If you want to set default values in plain [`serde`](https://serde.rs/), you have to create a function and link to it with `#[serde(default = "...")`.
This may be good if you need to do calculations to get the default value, but often you just want a simple integer or string to be the default value and have to create a whole function to return a hard-coded value. So instead of writing something like this, which can get very verbose quickly with many fields:
```rust ```rust
#[derive(Deserialize)] #[derive(Deserialize)]
struct Test { struct Test {
@@ -16,11 +16,7 @@ struct Test {
fn value_default() -> u32 { 42 } fn value_default() -> u32 { 42 }
``` ```
you can just do this:
That can get quiet messy if you have many fields with many (different) default values.
This crate tries to solve this issue by providing the `#[serde_inline_default]` proc macro.
With this macro set at the struct level (_**before `#[derive(Deserialize)]`/`#[derive(Serialize)]`!, otherwise it's not working correctly**_), you can set default values via `#[serde_inline_default(...)]` for your serde fields inline, without creating an extra function.
```rust ```rust
#[serde_inline_default] #[serde_inline_default]
#[derive(Deserialize)] #[derive(Deserialize)]
@@ -30,8 +26,53 @@ struct Test {
} }
``` ```
> [!IMPORTANT]
> **`#[serde_inline_default]` must be set before `#[derive(Deserialize)]`/`#[derive(Serialize)]`!**
Internally, `#[serde_inline_default(...)]` gets expanded to a function which returns the set value and the attribute is replaced with `#[serde(default = "<function name>")]`. Internally, `#[serde_inline_default(...)]` gets expanded to a function which returns the set value and the attribute is replaced with `#[serde(default = "<function name>")]`.
So this macro is just some syntax sugar for you, but can get quiet handy if you want to keep your code clean or write declarative macros / `macro_rules!`. So this macro is just some syntax sugar for you, but can get quite handy if you want to keep your code clean or write declarative macros / `macro_rules!`.
## Alternatives
This crate isn't perfect. Thus, you might be more satisfied with alternatives `serde` provides.
With `#[serde(default)]` + `impl Default` on a struct, `serde` uses the default implementation of the struct to get default values for each field ([docs](https://serde.rs/container-attrs.html#default)):
```rust
#[derive(Deserialize)]
#[serde(default)]
struct Test {
value: u32
}
impl Default for Test {
fn default() -> Self {
Self {
value: 42
}
}
}
```
If you still need/want `serde-inline-default` features, you also can combine them with `#[serde(default))` and `impl Default`:
```rust
#[serde_inline_default]
#[derive(Deserialize)]
#[serde(default)]
struct Test {
value: u32,
#[serde_inline_default(0)]
other_value: u32,
}
impl Default for Test {
fn default() -> Self {
Self {
value: 42,
other_value: 42
}
}
}
```
## License ## License
+57 -45
View File
@@ -1,58 +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<(String, TokenStream, 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, || {
// we check here if a function with the exact same return value already exists. if so, format!(
// this function gets used. "{}_{}_Variant{}_Field{}",
let fn_name_lit = if let Some((fn_name_lit, _, _)) = inline_fns DEFAULT_FN_PREFIX, item.ident, i, j
.iter() )
.find(|(_, def, _)| def.to_string() == default.to_string()) }));
{
fn_name_lit.clone()
} else {
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();
// replaces most lifetimes with 'static
type_lifetimes_to_static(&mut return_type);
let inline_fn = quote! {
#[doc(hidden)]
#[allow(non_snake_case)]
fn #fn_name_ident () -> #return_type {
#default
}
};
inline_fns.push((fn_name_lit.clone(), default, inline_fn));
fn_name_lit
};
field.attrs.remove(j);
field
.attrs
.insert(j, parse_quote!( #[serde(default = #fn_name_lit)] ));
break;
} }
} }
let real_inline_fns: Vec<TokenStream> = quote! {
inline_fns.into_iter().map(|(_, _, func)| func).collect(); #( #default_fns )*
let expanded = quote! {
#( #real_inline_fns )*
#item #item
}; }
expanded.into() .into()
} }
+16 -7
View File
@@ -1,19 +1,22 @@
#![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;
/// The main macro of this crate. /// The main macro of this crate.
/// Use it to define default values of fields in structs you [`Serialize`] or [`Deserialize`]. /// Use it to define default values of fields in structs you [`Serialize`] or [`Deserialize`].
/// You do not need to create a extra function to provide the default value, as it is the case in serdes' implementation of default (`#[serde(default = "...")]`). /// You do not need to create an extra function to provide the default value, as it is the case in
/// serdes' implementation of default (`#[serde(default = "...")]`).
/// ///
/// Set this macro on a struct where you use [`Serialize`] or [`Deserialize`] and use `#[serde_inline_default(...)]` on the field you want to have a inline default value. /// Set this macro on a struct where you use [`Serialize`] or [`Deserialize`] and use
/// Replace the `...` with the value you want and it will be set as default if serde needs it. /// `#[serde_inline_default(...)]` on the field you want to have an inline default value.
/// Replace the `...` with the value you want, and it will be set as default if serde needs it.
/// ///
/// Note that you must set this macro _before_ `#[derive(Serialize)]` / `#[derive(Deserialize)]` as it wouldn't work properly if set after the derive. /// Note that you must set this macro _before_ `#[derive(Serialize)]` / `#[derive(Deserialize)]` as
/// it won't work properly if it's set after the derive.
/// ///
/// # Examples /// # Examples
/// ///
@@ -29,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(),
} }
} }
+50 -2
View File
@@ -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 {
@@ -21,7 +26,7 @@ pub(crate) fn type_lifetimes_to_static(ty: &mut Type) {
} }
PathArguments::Parenthesized(parenthesized) => { PathArguments::Parenthesized(parenthesized) => {
for input in &mut parenthesized.inputs { for input in &mut parenthesized.inputs {
type_lifetimes_to_static(input) type_lifetimes_to_static(&mut input.ty)
} }
} }
} }
@@ -38,3 +43,46 @@ 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)]
#[inline]
#( #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
View 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> {
@@ -43,3 +44,26 @@ fn test_lifetime() {
assert_eq!(lifetime_test.test_str, "test"); assert_eq!(lifetime_test.test_str, "test");
} }
#[test]
#[allow(dead_code)]
fn conditional_compilation() {
#[cfg(debug_assertions)]
#[derive(Deserialize)]
struct TypeA(u8);
#[cfg(not(debug_assertions))]
#[derive(Deserialize)]
struct TypeB(u8);
#[serde_inline_default]
#[derive(Deserialize)]
struct Test {
#[cfg(debug_assertions)]
#[serde_inline_default(TypeA(1))]
val_a: TypeA,
#[cfg(not(debug_assertions))]
#[serde_inline_default(TypeB(1))]
val_b: TypeB,
}
}