diff --git a/tests/tests/ui/not_valuable.stderr b/tests/tests/ui/not_valuable.stderr index d21a171..4a88df6 100644 --- a/tests/tests/ui/not_valuable.stderr +++ b/tests/tests/ui/not_valuable.stderr @@ -1,86 +1,73 @@ -error[E0277]: the trait bound `S: Valuable` is not satisfied - --> tests/ui/not_valuable.rs:7:8 +error[E0599]: the method `as_value` exists for enum `Option`, but its trait bounds were not satisfied + --> tests/ui/not_valuable.rs:5:10 | +3 | struct S; + | -------- doesn't satisfy `S: Valuable` +4 | 5 | #[derive(Valuable)] - | -------- required by a bound introduced by this call -6 | struct Struct { -7 | f: Option, - | ^^^^^^^^^ the trait `Valuable` is not implemented for `S` + | ^^^^^^^^ method cannot be called on `Option` due to unsatisfied trait bounds | - = help: the following other types implement trait `Valuable`: - &T - &[T] - &mut T - &std::path::Path - &str - () - (T0, T1) - (T0, T1, T2) - and $N others - = note: required for `Option` to implement `Valuable` + ::: $RUST/core/src/option.rs + | + | pub enum Option { + | ------------------ doesn't satisfy `Option: Valuable` + | + = note: the following trait bounds were not satisfied: + `S: Valuable` + which is required by `Option: Valuable` +note: the trait `Valuable` must be implemented + --> $WORKSPACE/valuable/src/valuable.rs + | + | pub trait Valuable { + | ^^^^^^^^^^^^^^^^^^ + = note: this error originates in the derive macro `Valuable` (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0277]: the trait bound `S: Valuable` is not satisfied - --> tests/ui/not_valuable.rs:11:14 +error[E0599]: the method `as_value` exists for enum `Option`, but its trait bounds were not satisfied + --> tests/ui/not_valuable.rs:10:10 | +3 | struct S; + | -------- doesn't satisfy `S: Valuable` +... 10 | #[derive(Valuable)] - | -------- required by a bound introduced by this call -11 | struct Tuple(Option); - | ^^^^^^^^^ the trait `Valuable` is not implemented for `S` + | ^^^^^^^^ method cannot be called on `Option` due to unsatisfied trait bounds | - = help: the following other types implement trait `Valuable`: - &T - &[T] - &mut T - &std::path::Path - &str - () - (T0, T1) - (T0, T1, T2) - and $N others - = note: required for `Option` to implement `Valuable` - -error[E0277]: the trait bound `S: Valuable` is not satisfied - --> tests/ui/not_valuable.rs:15:25 + ::: $RUST/core/src/option.rs | -13 | #[derive(Valuable)] - | -------- required by a bound introduced by this call -14 | enum Enum { -15 | Struct { f: Option }, - | ^ the trait `Valuable` is not implemented for `S` + | pub enum Option { + | ------------------ doesn't satisfy `Option: Valuable` | - = help: the following other types implement trait `Valuable`: - &T - &[T] - &mut T - &std::path::Path - &str - () - (T0, T1) - (T0, T1, T2) - and $N others - = note: required for `Option` to implement `Valuable` - = note: 1 redundant requirement hidden - = note: required for `&Option` to implement `Valuable` + = note: the following trait bounds were not satisfied: + `S: Valuable` + which is required by `Option: Valuable` +note: the trait `Valuable` must be implemented + --> $WORKSPACE/valuable/src/valuable.rs + | + | pub trait Valuable { + | ^^^^^^^^^^^^^^^^^^ + = note: this error originates in the derive macro `Valuable` (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0277]: the trait bound `S: Valuable` is not satisfied - --> tests/ui/not_valuable.rs:16:19 +error[E0599]: the method `as_value` exists for reference `&Option`, but its trait bounds were not satisfied + --> tests/ui/not_valuable.rs:13:10 | -13 | #[derive(Valuable)] - | -------- required by a bound introduced by this call +3 | struct S; + | -------- doesn't satisfy `S: Valuable` ... -16 | Tuple(Option), - | ^ the trait `Valuable` is not implemented for `S` +13 | #[derive(Valuable)] + | ^^^^^^^^ method cannot be called on `&Option` due to unsatisfied trait bounds + | + ::: $RUST/core/src/option.rs + | + | pub enum Option { + | ------------------ doesn't satisfy `Option: Valuable` + | + = note: the following trait bounds were not satisfied: + `S: Valuable` + which is required by `Option: Valuable` + `Option: Valuable` + which is required by `&Option: Valuable` +note: the trait `Valuable` must be implemented + --> $WORKSPACE/valuable/src/valuable.rs | - = help: the following other types implement trait `Valuable`: - &T - &[T] - &mut T - &std::path::Path - &str - () - (T0, T1) - (T0, T1, T2) - and $N others - = note: required for `Option` to implement `Valuable` - = note: 1 redundant requirement hidden - = note: required for `&Option` to implement `Valuable` + | pub trait Valuable { + | ^^^^^^^^^^^^^^^^^^ + = note: this error originates in the derive macro `Valuable` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/valuable-derive/examples/comprehensive_demo.rs b/valuable-derive/examples/comprehensive_demo.rs new file mode 100644 index 0000000..4ffc4d1 --- /dev/null +++ b/valuable-derive/examples/comprehensive_demo.rs @@ -0,0 +1,79 @@ +use valuable::Valuable; + +/// Stack copy: bool -> bool +fn invert_flag(active: &bool) -> bool { + !*active +} + +/// Heap allocation: u64 -> String +fn format_id(id: &u64) -> String { + format!("ID-{:06}", id) +} + +/// Complex transformation: Vec -> Box<[String]> +fn uppercase_list(items: &[String]) -> Box<[String]> { + items + .iter() + .map(|s| s.to_uppercase()) + .collect::>() + .into_boxed_slice() +} + +#[derive(Valuable, Debug)] +struct Example { + id: u64, + + #[valuable(with = "invert_flag")] + active: bool, + + #[valuable(with = "format_id")] + user_id: u64, + + #[valuable(with = "uppercase_list")] + tags: Vec, + + #[valuable(skip)] + #[allow(dead_code)] + secret: String, +} + +fn main() { + let example = Example { + id: 42, + active: false, + user_id: 12345, + tags: vec!["admin".to_string(), "user".to_string()], + secret: "hidden".to_string(), + }; + + println!("Debug: {example:?}"); + + struct TestVisitor { + fields: Vec<(String, String)>, + } + + impl valuable::Visit for TestVisitor { + fn visit_value(&mut self, _value: valuable::Value<'_>) {} + + fn visit_named_fields(&mut self, named_values: &valuable::NamedValues<'_>) { + for (field, value) in named_values { + self.fields + .push((field.name().to_string(), format!("{value:?}"))); + } + } + + fn visit_unnamed_fields(&mut self, values: &[valuable::Value<'_>]) { + for (i, value) in values.iter().enumerate() { + self.fields.push((i.to_string(), format!("{value:?}"))); + } + } + } + + let mut visitor = TestVisitor { fields: Vec::new() }; + example.visit(&mut visitor); + + println!("Valuable fields:"); + for (name, value) in visitor.fields { + println!(" {}: {}", name, value); + } +} diff --git a/valuable-derive/src/attr.rs b/valuable-derive/src/attr.rs index 45a0e0b..b1f453d 100644 --- a/valuable-derive/src/attr.rs +++ b/valuable-derive/src/attr.rs @@ -33,7 +33,7 @@ static ATTRS: &[AttrDef] = &[ // #[valuable(skip)] AttrDef { name: "skip", - conflicts_with: &["rename"], + conflicts_with: &["rename", "with"], position: &[ // TODO: How do we implement Enumerable::variant and Valuable::as_value if a variant is skipped? // Position::Variant, @@ -42,12 +42,20 @@ static ATTRS: &[AttrDef] = &[ ], style: &[MetaStyle::Ident], }, + // #[valuable(with = "...")] + AttrDef { + name: "with", + conflicts_with: &["skip"], + position: &[Position::NamedField, Position::UnnamedField], + style: &[MetaStyle::NameValue], + }, ]; pub(crate) struct Attrs { rename: Option<(syn::MetaNameValue, syn::LitStr)>, transparent: Option, skip: Option, + with: Option<(syn::MetaNameValue, syn::LitStr)>, } impl Attrs { @@ -65,12 +73,20 @@ impl Attrs { pub(crate) fn skip(&self) -> bool { self.skip.is_some() } + + pub(crate) fn with(&self) -> Option { + self.with.as_ref().map(|(_, lit_str)| { + let path_str = lit_str.value(); + syn::parse_str(&path_str).expect("failed to parse with function path") + }) + } } pub(crate) fn parse_attrs(cx: &Context, attrs: &[syn::Attribute], pos: Position) -> Attrs { let mut rename = None; let mut transparent = None; let mut skip = None; + let mut with = None; let attrs = filter_attrs(cx, attrs, pos); for (def, meta) in &attrs { @@ -104,6 +120,8 @@ pub(crate) fn parse_attrs(cx: &Context, attrs: &[syn::Attribute], pos: Position) "transparent" => transparent = Some(meta.span()), // #[valuable(skip)] "skip" => skip = Some(meta.span()), + // #[valuable(with = "...")] + "with" => lit_str!(with), _ => unreachable!("{}", def.name), } @@ -113,6 +131,7 @@ pub(crate) fn parse_attrs(cx: &Context, attrs: &[syn::Attribute], pos: Position) rename, transparent, skip, + with, } } diff --git a/valuable-derive/src/expand.rs b/valuable-derive/src/expand.rs index 7fe879d..7dd4512 100644 --- a/valuable-derive/src/expand.rs +++ b/valuable-derive/src/expand.rs @@ -97,43 +97,114 @@ fn derive_struct( ) }; - let fields = data + let field_assignments: Vec<_> = data .fields .iter() .enumerate() .filter(|(i, _)| !field_attrs[*i].skip()) - .map(|(_, field)| { + .enumerate() + .map(|(value_idx, (field_idx, field))| { let f = field.ident.as_ref(); - let tokens = quote! { - &self.#f - }; - respan(tokens, &field.ty) - }); + let value_var = format_ident!("value_{}", value_idx); + + if let Some(with_expr) = field_attrs[field_idx].with() { + quote! { + let #value_var = #with_expr(&self.#f); + } + } else { + quote! { + let #value_var = { + use ::valuable::Valuable; + self.#f.as_value() + }; + } + } + }) + .collect(); + + let field_values: Vec<_> = data + .fields + .iter() + .enumerate() + .filter(|(i, _)| !field_attrs[*i].skip()) + .enumerate() + .map(|(value_idx, (field_idx, _field))| { + let value_var = format_ident!("value_{}", value_idx); + + if field_attrs[field_idx].with().is_some() { + quote! { + { + use ::valuable::Valuable; + #value_var.as_value() + } + } + } else { + quote! { #value_var } + } + }) + .collect(); + visit_fields = quote! { - visitor.visit_named_fields(&::valuable::NamedValues::new( - #named_fields_static_name, - &[ - #(::valuable::Valuable::as_value(#fields),)* - ], - )); + { + #(#field_assignments)* + let values = [#(#field_values),*]; + visitor.visit_named_fields(&::valuable::NamedValues::new( + #named_fields_static_name, + &values, + )); + } } } syn::Fields::Unnamed(_) | syn::Fields::Unit => { - let indices: Vec<_> = data + let field_assignments: Vec<_> = data .fields .iter() .enumerate() .filter(|(i, _)| !field_attrs[*i].skip()) - .map(|(i, field)| { - let index = syn::Index::from(i); - let tokens = quote! { - &self.#index - }; - respan(tokens, &field.ty) + .enumerate() + .map(|(value_idx, (field_idx, field))| { + let index = syn::Index::from(field_idx); + let value_var = format_ident!("value_{}", value_idx); + + if let Some(with_expr) = field_attrs[field_idx].with() { + quote! { + let #value_var = #with_expr(&self.#index); + } + } else { + let tokens = quote! { + let #value_var = { + use ::valuable::Valuable; + self.#index.as_value() + }; + }; + respan(tokens, &field.ty) + } }) .collect(); - let len = indices.len(); + let field_values: Vec<_> = data + .fields + .iter() + .enumerate() + .filter(|(i, _)| !field_attrs[*i].skip()) + .enumerate() + .map(|(value_idx, (field_idx, _field))| { + let value_var = format_ident!("value_{}", value_idx); + + if field_attrs[field_idx].with().is_some() { + quote! { + { + use ::valuable::Valuable; + #value_var.as_value() + } + } + } else { + quote! { #value_var } + } + }) + .collect(); + + let len = field_assignments.len(); struct_def = quote! { ::valuable::StructDef::new_static( #name_literal, @@ -142,11 +213,11 @@ fn derive_struct( }; visit_fields = quote! { - visitor.visit_unnamed_fields( - &[ - #(::valuable::Valuable::as_value(#indices),)* - ], - ); + { + #(#field_assignments)* + let values = [#(#field_values),*]; + visitor.visit_unnamed_fields(&values); + } }; } } @@ -242,35 +313,55 @@ fn derive_enum(cx: Context, input: &syn::DeriveInput, data: &syn::DataEnum) -> R }); let mut fields = Vec::with_capacity(variant.fields.len()); - let mut as_value = Vec::with_capacity(variant.fields.len()); - for (_, field) in variant + let mut field_assignments = Vec::new(); + let mut field_values = Vec::new(); + + for (value_idx, (field_idx, field)) in variant .fields .iter() .enumerate() .filter(|(i, _)| !field_attrs[variant_index][*i].skip()) + .enumerate() { let f = field.ident.as_ref(); fields.push(f); - let tokens = quote! { - // HACK(taiki-e): This `&` is not actually needed to calling as_value, - // but is needed to emulate multi-token span on stable Rust. - &#f - }; - as_value.push(respan(tokens, &field.ty)); + let value_var = format_ident!("value_{}", value_idx); + + if let Some(with_expr) = field_attrs[variant_index][field_idx].with() { + field_assignments.push(quote! { + let #value_var = #with_expr(#f); + }); + field_values.push(quote! { + { + use ::valuable::Valuable; + #value_var.as_value() + } + }); + } else { + field_assignments.push(quote! { + let #value_var = { + use ::valuable::Valuable; + #f.as_value() + }; + }); + field_values.push(quote! { #value_var }); + } } + let skipped = if fields.len() == variant.fields.len() { quote! {} } else { quote!(..) }; + visit_variants.push(quote! { Self::#variant_name { #(#fields,)* #skipped } => { + #(#field_assignments)* + let values = [#(#field_values),*]; visitor.visit_named_fields( &::valuable::NamedValues::new( #named_fields_static_name, - &[ - #(::valuable::Valuable::as_value(#as_value),)* - ], + &values, ), ); } @@ -286,22 +377,42 @@ fn derive_enum(cx: Context, input: &syn::DeriveInput, data: &syn::DataEnum) -> R let bindings: Vec<_> = (0..variant.fields.len()) .map(|i| format_ident!("__binding_{}", i)) .collect(); - let as_value: Vec<_> = bindings + + let mut field_assignments = Vec::new(); + let mut field_values = Vec::new(); + + for (value_idx, (field_idx, (binding, field))) in bindings .iter() .zip(&variant.fields) .enumerate() .filter(|(i, _)| !field_attrs[variant_index][*i].skip()) - .map(|(_, (binding, field))| { + .enumerate() + { + let value_var = format_ident!("value_{}", value_idx); + + if let Some(with_expr) = field_attrs[variant_index][field_idx].with() { + field_assignments.push(quote! { + let #value_var = #with_expr(#binding); + }); + field_values.push(quote! { + { + use ::valuable::Valuable; + #value_var.as_value() + } + }); + } else { let tokens = quote! { - // HACK(taiki-e): This `&` is not actually needed to calling as_value, - // but is needed to emulate multi-token span on stable Rust. - &#binding + let #value_var = { + use ::valuable::Valuable; + #binding.as_value() + }; }; - respan(tokens, &field.ty) - }) - .collect(); + field_assignments.push(respan(tokens, &field.ty)); + field_values.push(quote! { #value_var }); + } + } - let len = as_value.len(); + let len = field_assignments.len(); variant_defs.push(quote! { ::valuable::VariantDef::new( #variant_name_literal, @@ -311,11 +422,9 @@ fn derive_enum(cx: Context, input: &syn::DeriveInput, data: &syn::DataEnum) -> R visit_variants.push(quote! { Self::#variant_name(#(#bindings),*) => { - visitor.visit_unnamed_fields( - &[ - #(::valuable::Valuable::as_value(#as_value),)* - ], - ); + #(#field_assignments)* + let values = [#(#field_values),*]; + visitor.visit_unnamed_fields(&values); } }); } diff --git a/valuable-derive/src/lib.rs b/valuable-derive/src/lib.rs index 7da1257..15fcccb 100644 --- a/valuable-derive/src/lib.rs +++ b/valuable-derive/src/lib.rs @@ -27,6 +27,25 @@ use syn::parse_macro_input; /// /// Skip the field. /// +/// ## `#[valuable(with = "...")]` +/// +/// Use a custom function to transform the field value. The function must take a reference +/// to the field type and return a type that implements `Valuable`. +/// The returned value will be converted to a `Value` using `as_value()`. +/// +/// **Performance Note**: For minimal overhead, return primitive types (`u64`, `bool`, etc.) +/// which are cheap to copy, or reference from existing data. +/// String transformations will involve heap allocation. Example: +/// +/// ```rust,ignore +/// // Cheap copy: return primitives (stack copy, no allocation) +/// fn get_len(vec: &Vec) -> usize { vec.len() } +/// fn get_active(user: &User) -> bool { user.is_active } +/// +/// // Expensive: create new strings (heap allocation) +/// fn format_id(id: &u64) -> String { format!("ID-{}", id) } +/// ``` +/// /// # Examples /// /// ```