Skip to content

Commit 7663826

Browse files
committed
Add with attribute for field transformations in valuable derive macro
Introduces a new `#[valuable(with = "function")]` attribute that allows custom field transformations during valuable serialization. Functions take a reference to the field and return any type implementing Valuable.
1 parent e5135da commit 7663826

File tree

4 files changed

+278
-52
lines changed

4 files changed

+278
-52
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
use valuable::Valuable;
2+
3+
/// Stack copy: bool -> bool
4+
fn invert_flag(active: &bool) -> bool {
5+
!*active
6+
}
7+
8+
/// Heap allocation: u64 -> String
9+
fn format_id(id: &u64) -> String {
10+
format!("ID-{:06}", id)
11+
}
12+
13+
/// Complex transformation: Vec<String> -> Box<[String]>
14+
fn uppercase_list(items: &[String]) -> Box<[String]> {
15+
items
16+
.iter()
17+
.map(|s| s.to_uppercase())
18+
.collect::<Vec<_>>()
19+
.into_boxed_slice()
20+
}
21+
22+
#[derive(Valuable, Debug)]
23+
struct Example {
24+
id: u64,
25+
26+
#[valuable(with = "invert_flag")]
27+
active: bool,
28+
29+
#[valuable(with = "format_id")]
30+
user_id: u64,
31+
32+
#[valuable(with = "uppercase_list")]
33+
tags: Vec<String>,
34+
35+
#[valuable(skip)]
36+
#[allow(dead_code)]
37+
secret: String,
38+
}
39+
40+
fn main() {
41+
let example = Example {
42+
id: 42,
43+
active: false,
44+
user_id: 12345,
45+
tags: vec!["admin".to_string(), "user".to_string()],
46+
secret: "hidden".to_string(),
47+
};
48+
49+
println!("Debug: {example:?}");
50+
51+
struct TestVisitor {
52+
fields: Vec<(String, String)>,
53+
}
54+
55+
impl valuable::Visit for TestVisitor {
56+
fn visit_value(&mut self, _value: valuable::Value<'_>) {}
57+
58+
fn visit_named_fields(&mut self, named_values: &valuable::NamedValues<'_>) {
59+
for (field, value) in named_values {
60+
self.fields
61+
.push((field.name().to_string(), format!("{value:?}")));
62+
}
63+
}
64+
65+
fn visit_unnamed_fields(&mut self, values: &[valuable::Value<'_>]) {
66+
for (i, value) in values.iter().enumerate() {
67+
self.fields.push((i.to_string(), format!("{value:?}")));
68+
}
69+
}
70+
}
71+
72+
let mut visitor = TestVisitor { fields: Vec::new() };
73+
example.visit(&mut visitor);
74+
75+
println!("Valuable fields:");
76+
for (name, value) in visitor.fields {
77+
println!(" {}: {}", name, value);
78+
}
79+
}

valuable-derive/src/attr.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ static ATTRS: &[AttrDef] = &[
3333
// #[valuable(skip)]
3434
AttrDef {
3535
name: "skip",
36-
conflicts_with: &["rename"],
36+
conflicts_with: &["rename", "with"],
3737
position: &[
3838
// TODO: How do we implement Enumerable::variant and Valuable::as_value if a variant is skipped?
3939
// Position::Variant,
@@ -42,12 +42,20 @@ static ATTRS: &[AttrDef] = &[
4242
],
4343
style: &[MetaStyle::Ident],
4444
},
45+
// #[valuable(with = "...")]
46+
AttrDef {
47+
name: "with",
48+
conflicts_with: &["skip"],
49+
position: &[Position::NamedField, Position::UnnamedField],
50+
style: &[MetaStyle::NameValue],
51+
},
4552
];
4653

4754
pub(crate) struct Attrs {
4855
rename: Option<(syn::MetaNameValue, syn::LitStr)>,
4956
transparent: Option<Span>,
5057
skip: Option<Span>,
58+
with: Option<(syn::MetaNameValue, syn::LitStr)>,
5159
}
5260

5361
impl Attrs {
@@ -65,12 +73,20 @@ impl Attrs {
6573
pub(crate) fn skip(&self) -> bool {
6674
self.skip.is_some()
6775
}
76+
77+
pub(crate) fn with(&self) -> Option<syn::Expr> {
78+
self.with.as_ref().map(|(_, lit_str)| {
79+
let path_str = lit_str.value();
80+
syn::parse_str(&path_str).expect("failed to parse with function path")
81+
})
82+
}
6883
}
6984

7085
pub(crate) fn parse_attrs(cx: &Context, attrs: &[syn::Attribute], pos: Position) -> Attrs {
7186
let mut rename = None;
7287
let mut transparent = None;
7388
let mut skip = None;
89+
let mut with = None;
7490

7591
let attrs = filter_attrs(cx, attrs, pos);
7692
for (def, meta) in &attrs {
@@ -104,6 +120,8 @@ pub(crate) fn parse_attrs(cx: &Context, attrs: &[syn::Attribute], pos: Position)
104120
"transparent" => transparent = Some(meta.span()),
105121
// #[valuable(skip)]
106122
"skip" => skip = Some(meta.span()),
123+
// #[valuable(with = "...")]
124+
"with" => lit_str!(with),
107125

108126
_ => unreachable!("{}", def.name),
109127
}
@@ -113,6 +131,7 @@ pub(crate) fn parse_attrs(cx: &Context, attrs: &[syn::Attribute], pos: Position)
113131
rename,
114132
transparent,
115133
skip,
134+
with,
116135
}
117136
}
118137

0 commit comments

Comments
 (0)