Add #[nu_value(rename = "...")] as helper attribute on members for derive macros (#13761)

# Description

This PR allows the helper attribute `nu_value(rename = "...")` to be
used on struct fields and enum variants. This allows renaming keys and
variants just like [`#[serde(rename =
"name")]`](https://serde.rs/field-attrs.html#rename). This has no
singular variants for `IntoValue` or `FromValue`, both need to use the
same (but I think this shouldn't be an issue for now).

# User-Facing Changes

Users of the derive macros `IntoValue` and `FromValue` may now use
`#[nu_value(rename = "...")]` to rename single fields, but no already
existing code will break.
This commit is contained in:
Piepmatz 2024-09-04 11:27:21 +02:00 committed by GitHub
parent fa4f9b083e
commit ebe42241fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 599 additions and 210 deletions

View file

@ -1,60 +1,92 @@
use syn::{spanned::Spanned, Attribute, Fields, LitStr};
use syn::{meta::ParseNestedMeta, spanned::Spanned, Attribute, Fields, LitStr};
use crate::{case::Case, error::DeriveError, HELPER_ATTRIBUTE};
pub trait ParseAttrs: Default {
fn parse_attrs<'a, M>(
iter: impl IntoIterator<Item = &'a Attribute>,
) -> Result<Self, DeriveError<M>> {
let mut attrs = Self::default();
for attr in filter(iter.into_iter()) {
// This is a container to allow returning derive errors inside the parse_nested_meta fn.
let mut err = Ok(());
let _ = attr.parse_nested_meta(|meta| {
attrs.parse_attr(meta).or_else(|e| {
err = Err(e);
Ok(()) // parse_nested_meta requires another error type, so we escape it here
})
});
err?; // Shortcircuit here if `err` is holding some error.
}
Ok(attrs)
}
fn parse_attr<M>(&mut self, attr_meta: ParseNestedMeta<'_>) -> Result<(), DeriveError<M>>;
}
#[derive(Debug, Default)]
pub struct ContainerAttributes {
pub rename_all: Option<Case>,
pub type_name: Option<String>,
}
impl ContainerAttributes {
pub fn parse_attrs<'a, M>(
iter: impl Iterator<Item = &'a Attribute>,
) -> Result<Self, DeriveError<M>> {
let mut container_attrs = ContainerAttributes::default();
for attr in filter(iter) {
// This is a container to allow returning derive errors inside the parse_nested_meta fn.
let mut err = Ok(());
attr.parse_nested_meta(|meta| {
let ident = meta.path.require_ident()?;
impl ParseAttrs for ContainerAttributes {
fn parse_attr<M>(&mut self, attr_meta: ParseNestedMeta<'_>) -> Result<(), DeriveError<M>> {
let ident = attr_meta.path.require_ident()?;
match ident.to_string().as_str() {
"rename_all" => {
let case: LitStr = meta.value()?.parse()?;
let case: LitStr = attr_meta.value()?.parse()?;
let value_span = case.span();
let case = case.value();
match Case::from_str(&case) {
Some(case) => container_attrs.rename_all = Some(case),
Some(case) => self.rename_all = Some(case),
None => {
err = Err(DeriveError::InvalidAttributeValue {
return Err(DeriveError::InvalidAttributeValue {
value_span,
value: Box::new(case),
});
return Ok(()); // We stored the err in `err`.
}
}
}
"type_name" => {
let type_name: LitStr = meta.value()?.parse()?;
let type_name: LitStr = attr_meta.value()?.parse()?;
let type_name = type_name.value();
container_attrs.type_name = Some(type_name);
self.type_name = Some(type_name);
}
ident => {
err = Err(DeriveError::UnexpectedAttribute {
return Err(DeriveError::UnexpectedAttribute {
meta_span: ident.span(),
});
}
}
Ok(())
})
.map_err(DeriveError::Syn)?;
err?; // Shortcircuit here if `err` is holding some error.
}
}
Ok(container_attrs)
#[derive(Debug, Default)]
pub struct MemberAttributes {
pub rename: Option<String>,
}
impl ParseAttrs for MemberAttributes {
fn parse_attr<M>(&mut self, attr_meta: ParseNestedMeta<'_>) -> Result<(), DeriveError<M>> {
let ident = attr_meta.path.require_ident()?;
match ident.to_string().as_str() {
"rename" => {
let rename: LitStr = attr_meta.value()?.parse()?;
let rename = rename.value();
self.rename = Some(rename);
}
ident => {
return Err(DeriveError::UnexpectedAttribute {
meta_span: ident.span(),
});
}
}
Ok(())
}
}

View file

@ -47,12 +47,16 @@ impl Case {
}
pub trait Casing {
fn to_case(&self, case: Case) -> String;
fn to_case(&self, case: impl Into<Option<Case>>) -> String;
}
impl<T: AsRef<str>> Casing for T {
fn to_case(&self, case: Case) -> String {
let s = self.as_ref();
impl<T: ToString> Casing for T {
fn to_case(&self, case: impl Into<Option<Case>>) -> String {
let s = self.to_string();
let Some(case) = case.into() else {
return s.to_string();
};
match case {
Case::Pascal => s.to_upper_camel_case(),
Case::Camel => s.to_lower_camel_case(),

View file

@ -28,6 +28,19 @@ pub enum DeriveError<M> {
value_span: Span,
value: Box<dyn Debug>,
},
/// Two keys or variants are called the same name breaking bidirectionality.
NonUniqueName {
name: String,
first: Span,
second: Span,
},
}
impl<M> From<syn::parse::Error> for DeriveError<M> {
fn from(value: syn::parse::Error) -> Self {
Self::Syn(value)
}
}
impl<M> From<DeriveError<M>> for Diagnostic {
@ -77,6 +90,15 @@ impl<M> From<DeriveError<M>> for Diagnostic {
"check documentation for `{derive_name}` for valid attribute values"
))
}
DeriveError::NonUniqueName {
name,
first,
second,
} => Diagnostic::new(Level::Error, format!("non-unique name {name:?} found"))
.span_error(first, "first occurrence found here".to_string())
.span_error(second, "second occurrence found here".to_string())
.help("use `#[nu_value(rename = \"...\")]` to ensure unique names".to_string()),
}
}
}

View file

@ -6,13 +6,15 @@ use syn::{
};
use crate::{
attributes::{self, ContainerAttributes},
case::{Case, Casing},
attributes::{self, ContainerAttributes, MemberAttributes, ParseAttrs},
case::Case,
names::NameResolver,
};
#[derive(Debug)]
pub struct FromValue;
type DeriveError = super::error::DeriveError<FromValue>;
type Result<T = TokenStream2> = std::result::Result<T, DeriveError>;
/// Inner implementation of the `#[derive(FromValue)]` macro for structs and enums.
///
@ -23,7 +25,7 @@ type DeriveError = super::error::DeriveError<FromValue>;
/// - For structs: [`derive_struct_from_value`]
/// - For enums: [`derive_enum_from_value`]
/// - Unions are not supported and will return an error.
pub fn derive_from_value(input: TokenStream2) -> Result<TokenStream2, DeriveError> {
pub fn derive_from_value(input: TokenStream2) -> Result {
let input: DeriveInput = syn::parse2(input).map_err(DeriveError::Syn)?;
match input.data {
Data::Struct(data_struct) => Ok(derive_struct_from_value(
@ -52,13 +54,15 @@ fn derive_struct_from_value(
data: DataStruct,
generics: Generics,
attrs: Vec<Attribute>,
) -> Result<TokenStream2, DeriveError> {
) -> Result {
let container_attrs = ContainerAttributes::parse_attrs(attrs.iter())?;
attributes::deny_fields(&data.fields)?;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let from_value_impl = struct_from_value(&data, container_attrs.rename_all);
let expected_type_impl =
struct_expected_type(&data.fields, container_attrs.type_name.as_deref());
let from_value_impl = struct_from_value(&data, &container_attrs)?;
let expected_type_impl = struct_expected_type(
&data.fields,
container_attrs.type_name.as_deref(),
&container_attrs,
)?;
Ok(quote! {
#[automatically_derived]
impl #impl_generics nu_protocol::FromValue for #ident #ty_generics #where_clause {
@ -200,28 +204,28 @@ fn derive_struct_from_value(
/// }
/// }
/// ```
fn struct_from_value(data: &DataStruct, rename_all: Option<Case>) -> TokenStream2 {
let body = parse_value_via_fields(&data.fields, quote!(Self), rename_all);
quote! {
fn struct_from_value(data: &DataStruct, container_attrs: &ContainerAttributes) -> Result {
let body = parse_value_via_fields(&data.fields, quote!(Self), container_attrs)?;
Ok(quote! {
fn from_value(
v: nu_protocol::Value
) -> std::result::Result<Self, nu_protocol::ShellError> {
#body
}
}
})
}
/// Implements `FromValue::expected_type` for structs.
///
/// This function constructs the `expected_type` function for structs.
/// The type depends on the `fields`: named fields construct a record type with every key and type
/// laid out.
/// Unnamed fields construct a custom type with the name in the format like
/// `list[type0, type1, type2]`.
/// No fields expect the `Type::Nothing`.
/// This function constructs the `expected_type` function for structs based on the provided fields.
/// The type depends on the `fields`:
/// - Named fields construct a record type where each key corresponds to a field name.
/// The specific keys are resolved by [`NameResolver::resolve_ident`].
/// - Unnamed fields construct a custom type with the format `list[type0, type1, type2]`.
/// - Unit structs expect `Type::Nothing`.
///
/// If `#[nu_value(type_name = "...")]` is used, the output type will be `Type::Custom` with that
/// passed name.
/// If the `#[nu_value(type_name = "...")]` attribute is used, the output type will be
/// `Type::Custom` with the provided name.
///
/// # Examples
///
@ -233,6 +237,7 @@ fn struct_from_value(data: &DataStruct, rename_all: Option<Case>) -> TokenStream
/// struct Pet {
/// name: String,
/// age: u8,
/// #[nu_value(rename = "toy")]
/// favorite_toy: Option<String>,
/// }
///
@ -249,7 +254,7 @@ fn struct_from_value(data: &DataStruct, rename_all: Option<Case>) -> TokenStream
/// <u8 as nu_protocol::FromValue>::expected_type(),
/// ),
/// (
/// std::string::ToString::to_string("favorite_toy"),
/// std::string::ToString::to_string("toy"),
/// <Option<String> as nu_protocol::FromValue>::expected_type(),
/// )
/// ].into_boxed_slice()
@ -305,7 +310,11 @@ fn struct_from_value(data: &DataStruct, rename_all: Option<Case>) -> TokenStream
/// }
/// }
/// ```
fn struct_expected_type(fields: &Fields, attr_type_name: Option<&str>) -> TokenStream2 {
fn struct_expected_type(
fields: &Fields,
attr_type_name: Option<&str>,
container_attrs: &ContainerAttributes,
) -> Result {
let ty = match (fields, attr_type_name) {
(_, Some(type_name)) => {
quote!(nu_protocol::Type::Custom(
@ -313,20 +322,25 @@ fn struct_expected_type(fields: &Fields, attr_type_name: Option<&str>) -> TokenS
))
}
(Fields::Named(fields), _) => {
let fields = fields.named.iter().map(|field| {
let mut name_resolver = NameResolver::new();
let mut fields_ts = Vec::with_capacity(fields.named.len());
for field in fields.named.iter() {
let member_attrs = MemberAttributes::parse_attrs(&field.attrs)?;
let ident = field.ident.as_ref().expect("named has idents");
let ident_s = ident.to_string();
let ident_s =
name_resolver.resolve_ident(ident, container_attrs, &member_attrs, None)?;
let ty = &field.ty;
quote! {(
fields_ts.push(quote! {(
std::string::ToString::to_string(#ident_s),
<#ty as nu_protocol::FromValue>::expected_type(),
)}
});
)});
}
quote!(nu_protocol::Type::Record(
std::vec![#(#fields),*].into_boxed_slice()
std::vec![#(#fields_ts),*].into_boxed_slice()
))
}
(Fields::Unnamed(fields), _) => {
(f @ Fields::Unnamed(fields), _) => {
attributes::deny_fields(f)?;
let mut iter = fields.unnamed.iter();
let fields = fields.unnamed.iter().map(|field| {
let ty = &field.ty;
@ -352,11 +366,11 @@ fn struct_expected_type(fields: &Fields, attr_type_name: Option<&str>) -> TokenS
(Fields::Unit, _) => quote!(nu_protocol::Type::Nothing),
};
quote! {
Ok(quote! {
fn expected_type() -> nu_protocol::Type {
#ty
}
}
})
}
/// Implements the `#[derive(FromValue)]` macro for enums.
@ -372,7 +386,7 @@ fn derive_enum_from_value(
data: DataEnum,
generics: Generics,
attrs: Vec<Attribute>,
) -> Result<TokenStream2, DeriveError> {
) -> Result {
let container_attrs = ContainerAttributes::parse_attrs(attrs.iter())?;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let from_value_impl = enum_from_value(&data, &attrs)?;
@ -393,9 +407,9 @@ fn derive_enum_from_value(
/// should be represented via a `Value`.
/// This function checks that every field is a unit variant and constructs a match statement over
/// all possible variants.
/// The input value is expected to be a `Value::String` containing the name of the variant formatted
/// as defined by the `#[nu_value(rename_all = "...")]` attribute.
/// If no attribute is given, [`snake_case`](heck::ToSnakeCase) is expected.
/// The input value is expected to be a `Value::String` containing the name of the variant.
/// That string is defined by the [`NameResolver::resolve_ident`] method with the `default` value
/// being [`Case::Snake`].
///
/// If no matching variant is found, `ShellError::CantConvert` is returned.
///
@ -405,6 +419,7 @@ fn derive_enum_from_value(
/// enum Weather {
/// Sunny,
/// Cloudy,
/// #[nu_value(rename = "rain")]
/// Raining
/// }
///
@ -417,7 +432,7 @@ fn derive_enum_from_value(
/// match s.as_str() {
/// "sunny" => std::result::Ok(Self::Sunny),
/// "cloudy" => std::result::Ok(Self::Cloudy),
/// "raining" => std::result::Ok(Self::Raining),
/// "rain" => std::result::Ok(Self::Raining),
/// _ => std::result::Result::Err(nu_protocol::ShellError::CantConvert {
/// to_type: std::string::ToString::to_string(
/// &<Self as nu_protocol::FromValue>::expected_type()
@ -429,17 +444,17 @@ fn derive_enum_from_value(
/// }
/// }
/// ```
fn enum_from_value(data: &DataEnum, attrs: &[Attribute]) -> Result<TokenStream2, DeriveError> {
fn enum_from_value(data: &DataEnum, attrs: &[Attribute]) -> Result {
let container_attrs = ContainerAttributes::parse_attrs(attrs.iter())?;
let mut name_resolver = NameResolver::new();
let arms: Vec<TokenStream2> = data
.variants
.iter()
.map(|variant| {
attributes::deny(&variant.attrs)?;
let member_attrs = MemberAttributes::parse_attrs(&variant.attrs)?;
let ident = &variant.ident;
let ident_s = format!("{ident}")
.as_str()
.to_case(container_attrs.rename_all.unwrap_or(Case::Snake));
let ident_s =
name_resolver.resolve_ident(ident, &container_attrs, &member_attrs, Case::Snake)?;
match &variant.fields {
Fields::Named(fields) => Err(DeriveError::UnsupportedEnums {
fields_span: fields.span(),
@ -450,7 +465,7 @@ fn enum_from_value(data: &DataEnum, attrs: &[Attribute]) -> Result<TokenStream2,
Fields::Unit => Ok(quote!(#ident_s => std::result::Result::Ok(Self::#ident))),
}
})
.collect::<Result<_, _>>()?;
.collect::<Result<_>>()?;
Ok(quote! {
fn from_value(
@ -512,48 +527,50 @@ fn enum_expected_type(attr_type_name: Option<&str>) -> Option<TokenStream2> {
/// Parses a `Value` into self.
///
/// This function handles the actual parsing of a `Value` into self.
/// It takes three parameters: `fields`, `self_ident` and `rename_all`.
/// The `fields` parameter determines the expected type of `Value`: named fields expect a
/// `Value::Record`, unnamed fields expect a `Value::List`, and a unit expects `Value::Nothing`.
/// This function handles parsing a `Value` into the corresponding struct or enum variant (`self`).
/// It takes three parameters: `fields`, `self_ident`, and `rename_all`.
///
/// For named fields, the `fields` parameter indicates which field in the record corresponds to
/// which struct field.
/// For both named and unnamed fields, it also helps cast the type into a `FromValue` type.
/// This approach maintains
/// [hygiene](https://doc.rust-lang.org/reference/macros-by-example.html#hygiene).
/// - The `fields` parameter specifies the expected structure of the `Value`:
/// - Named fields expect a `Value::Record`.
/// - Unnamed fields expect a `Value::List`.
/// - A unit struct expects `Value::Nothing`.
///
/// The `self_ident` parameter is used to describe the identifier of the returned value.
/// For structs, `Self` is usually sufficient, but for enums, `Self::Variant` may be needed in the
/// future.
/// For named fields, each field in the record is matched to a struct field.
/// The name matching uses the identifiers resolved by
/// [`NameResolver`](NameResolver::resolve_ident) with `default` being `None`.
///
/// The `rename_all` parameter is provided through `#[nu_value(rename_all = "...")]` and describes
/// how, if passed, the field keys in the `Value` should be named.
/// If this is `None`, we keep the names as they are in the struct.
/// The `self_ident` parameter is used to specify the identifier for the returned value.
/// For most structs, `Self` is sufficient, but `Self::Variant` may be needed for enum variants.
///
/// The `container_attrs` parameters, provided through `#[nu_value]` on the container, defines
/// global rules for the `FromValue` implementation.
/// This is used for the [`NameResolver`] to resolve the correct ident in the `Value`.
///
/// This function is more complex than the equivalent for `IntoValue` due to additional error
/// handling:
/// - If a named field is missing in the `Value`, `ShellError::CantFindColumn` is returned.
/// - For unit structs, if the value is not `Value::Nothing`, `ShellError::CantConvert` is returned.
///
/// This function is more complex than the equivalent for `IntoValue` due to error handling
/// requirements.
/// For missing fields, `ShellError::CantFindColumn` is used, and for unit structs,
/// `ShellError::CantConvert` is used.
/// The implementation avoids local variables for fields to prevent accidental shadowing, ensuring
/// that poorly named fields don't cause issues.
/// While this style is not typically recommended in handwritten Rust, it is acceptable for code
/// that fields with similar names do not cause unexpected behavior.
/// This approach is not typically recommended in handwritten Rust, but it is acceptable for code
/// generation.
fn parse_value_via_fields(
fields: &Fields,
self_ident: impl ToTokens,
rename_all: Option<Case>,
) -> TokenStream2 {
container_attrs: &ContainerAttributes,
) -> Result {
match fields {
Fields::Named(fields) => {
let fields = fields.named.iter().map(|field| {
let mut name_resolver = NameResolver::new();
let mut fields_ts: Vec<TokenStream2> = Vec::with_capacity(fields.named.len());
for field in fields.named.iter() {
let member_attrs = MemberAttributes::parse_attrs(&field.attrs)?;
let ident = field.ident.as_ref().expect("named has idents");
let mut ident_s = ident.to_string();
if let Some(rename_all) = rename_all {
ident_s = ident_s.to_case(rename_all);
}
let ident_s =
name_resolver.resolve_ident(ident, container_attrs, &member_attrs, None)?;
let ty = &field.ty;
match type_is_option(ty) {
fields_ts.push(match type_is_option(ty) {
true => quote! {
#ident: record
.remove(#ident_s)
@ -573,15 +590,16 @@ fn parse_value_via_fields(
})?,
)?
},
}
});
quote! {
}
Ok(quote! {
let span = v.span();
let mut record = v.into_record()?;
std::result::Result::Ok(#self_ident {#(#fields),*})
std::result::Result::Ok(#self_ident {#(#fields_ts),*})
})
}
}
Fields::Unnamed(fields) => {
f @ Fields::Unnamed(fields) => {
attributes::deny_fields(f)?;
let fields = fields.unnamed.iter().enumerate().map(|(i, field)| {
let ty = &field.ty;
quote! {{
@ -596,14 +614,14 @@ fn parse_value_via_fields(
)?
}}
});
quote! {
Ok(quote! {
let span = v.span();
let list = v.into_list()?;
let mut deque: std::collections::VecDeque<_> = std::convert::From::from(list);
std::result::Result::Ok(#self_ident(#(#fields),*))
})
}
}
Fields::Unit => quote! {
Fields::Unit => Ok(quote! {
match v {
nu_protocol::Value::Nothing {..} => Ok(#self_ident),
v => std::result::Result::Err(nu_protocol::ShellError::CantConvert {
@ -613,7 +631,7 @@ fn parse_value_via_fields(
help: std::option::Option::None
})
}
},
}),
}
}

View file

@ -6,13 +6,15 @@ use syn::{
};
use crate::{
attributes::{self, ContainerAttributes},
case::{Case, Casing},
attributes::{self, ContainerAttributes, MemberAttributes, ParseAttrs},
case::Case,
names::NameResolver,
};
#[derive(Debug)]
pub struct IntoValue;
type DeriveError = super::error::DeriveError<IntoValue>;
type Result<T = TokenStream2> = std::result::Result<T, DeriveError>;
/// Inner implementation of the `#[derive(IntoValue)]` macro for structs and enums.
///
@ -23,7 +25,7 @@ type DeriveError = super::error::DeriveError<IntoValue>;
/// - For structs: [`struct_into_value`]
/// - For enums: [`enum_into_value`]
/// - Unions are not supported and will return an error.
pub fn derive_into_value(input: TokenStream2) -> Result<TokenStream2, DeriveError> {
pub fn derive_into_value(input: TokenStream2) -> Result {
let input: DeriveInput = syn::parse2(input).map_err(DeriveError::Syn)?;
match input.data {
Data::Struct(data_struct) => Ok(struct_into_value(
@ -48,6 +50,7 @@ pub fn derive_into_value(input: TokenStream2) -> Result<TokenStream2, DeriveErro
/// `IntoValue`.
/// For structs with named fields, the derived implementation creates a `Value::Record` using the
/// struct fields as keys.
/// The specific keys are resolved by [`NameResolver`](NameResolver::resolve_ident).
/// Each field value is converted using the `IntoValue::into_value` method.
/// For structs with unnamed fields, this generates a `Value::List` with each field in the list.
/// For unit structs, this generates `Value::Nothing`, because there is no data.
@ -111,9 +114,8 @@ fn struct_into_value(
data: DataStruct,
generics: Generics,
attrs: Vec<Attribute>,
) -> Result<TokenStream2, DeriveError> {
let rename_all = ContainerAttributes::parse_attrs(attrs.iter())?.rename_all;
attributes::deny_fields(&data.fields)?;
) -> Result {
let container_attrs = ContainerAttributes::parse_attrs(attrs.iter())?;
let record = match &data.fields {
Fields::Named(fields) => {
let accessor = fields
@ -121,7 +123,7 @@ fn struct_into_value(
.iter()
.map(|field| field.ident.as_ref().expect("named has idents"))
.map(|ident| quote!(self.#ident));
fields_return_value(&data.fields, accessor, rename_all)
fields_return_value(&data.fields, accessor, &container_attrs)?
}
Fields::Unnamed(fields) => {
let accessor = fields
@ -130,7 +132,7 @@ fn struct_into_value(
.enumerate()
.map(|(n, _)| Index::from(n))
.map(|index| quote!(self.#index));
fields_return_value(&data.fields, accessor, rename_all)
fields_return_value(&data.fields, accessor, &container_attrs)?
}
Fields::Unit => quote!(nu_protocol::Value::nothing(span)),
};
@ -150,10 +152,13 @@ fn struct_into_value(
/// This function implements the derive macro `IntoValue` for enums.
/// Currently, only unit enum variants are supported as it is not clear how other types of enums
/// should be represented in a `Value`.
/// For simple enums, we represent the enum as a `Value::String`. For other types of variants, we return an error.
/// The variant name will be case-converted as described by the `#[nu_value(rename_all = "...")]` helper attribute.
/// If no attribute is used, the default is `case_convert::Case::Snake`.
/// The implementation matches over all variants, uses the appropriate variant name, and constructs a `Value::String`.
/// For simple enums, we represent the enum as a `Value::String`.
/// For other types of variants, we return an error.
///
/// The variant name used in the `Value::String` is resolved by the
/// [`NameResolver`](NameResolver::resolve_ident) with the `default` being [`Case::Snake`].
/// The implementation matches over all variants, uses the appropriate variant name, and constructs
/// a `Value::String`.
///
/// This is how such a derived implementation looks:
/// ```rust
@ -161,6 +166,7 @@ fn struct_into_value(
/// enum Weather {
/// Sunny,
/// Cloudy,
/// #[nu_value(rename = "rain")]
/// Raining
/// }
///
@ -169,7 +175,7 @@ fn struct_into_value(
/// match self {
/// Self::Sunny => nu_protocol::Value::string("sunny", span),
/// Self::Cloudy => nu_protocol::Value::string("cloudy", span),
/// Self::Raining => nu_protocol::Value::string("raining", span),
/// Self::Raining => nu_protocol::Value::string("rain", span),
/// }
/// }
/// }
@ -179,17 +185,21 @@ fn enum_into_value(
data: DataEnum,
generics: Generics,
attrs: Vec<Attribute>,
) -> Result<TokenStream2, DeriveError> {
) -> Result {
let container_attrs = ContainerAttributes::parse_attrs(attrs.iter())?;
let mut name_resolver = NameResolver::new();
let arms: Vec<TokenStream2> = data
.variants
.into_iter()
.map(|variant| {
attributes::deny(&variant.attrs)?;
let member_attrs = MemberAttributes::parse_attrs(variant.attrs.iter())?;
let ident = variant.ident;
let ident_s = format!("{ident}")
.as_str()
.to_case(container_attrs.rename_all.unwrap_or(Case::Snake));
let ident_s = name_resolver.resolve_ident(
&ident,
&container_attrs,
&member_attrs,
Case::Snake,
)?;
match &variant.fields {
// In the future we can implement more complex enums here.
Fields::Named(fields) => Err(DeriveError::UnsupportedEnums {
@ -203,7 +213,7 @@ fn enum_into_value(
}
}
})
.collect::<Result<_, _>>()?;
.collect::<Result<_>>()?;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
Ok(quote! {
@ -219,57 +229,63 @@ fn enum_into_value(
/// Constructs the final `Value` that the macro generates.
///
/// This function handles the construction of the final `Value` that the macro generates.
/// It is currently only used for structs but may be used for enums in the future.
/// The function takes three parameters: the `fields`, which allow iterating over each field of a
/// data type, the `accessor` and `rename_all`.
/// The fields determine whether we need to generate a `Value::Record`, `Value::List`, or
/// `Value::Nothing`.
/// For named fields, they are also directly used to generate the record key.
/// If `#[nu_value(rename_all = "...")]` is used and then passed in here via `rename_all`, the
/// named fields will be converted to the given case and then uses as the record key.
/// This function handles the construction of the final `Value` that the macro generates, primarily
/// for structs.
/// It takes three parameters: `fields`, which allows iterating over each field of a data type,
/// `accessor`, which generalizes data access, and `container_attrs`, which is used for the
/// [`NameResolver`].
///
/// The `accessor` parameter generalizes how the data is accessed.
/// For named fields, this is usually the name of the fields preceded by `self` in a struct, and
/// maybe something else for enums.
/// For unnamed fields, this should be an iterator similar to the one with named fields, but
/// accessing tuple fields, so we get `self.n`.
/// For unit structs, this parameter is ignored.
/// By using the accessor like this, we can have the same code for structs and enums with data
/// - **Field Keys**:
/// The field key is field name of the input struct and resolved the
/// [`NameResolver`](NameResolver::resolve_ident).
///
/// - **Fields Type**:
/// - Determines whether to generate a `Value::Record`, `Value::List`, or `Value::Nothing` based
/// on the nature of the fields.
/// - Named fields are directly used to generate the record key, as described above.
///
/// - **Accessor**:
/// - Generalizes how data is accessed for different data types.
/// - For named fields in structs, this is typically `self.field_name`.
/// - For unnamed fields (e.g., tuple structs), it should be an iterator similar to named fields
/// but accessing fields like `self.0`.
/// - For unit structs, this parameter is ignored.
///
/// This design allows the same function to potentially handle both structs and enums with data
/// variants in the future.
fn fields_return_value(
fields: &Fields,
accessor: impl Iterator<Item = impl ToTokens>,
rename_all: Option<Case>,
) -> TokenStream2 {
container_attrs: &ContainerAttributes,
) -> Result {
match fields {
Fields::Named(fields) => {
let items: Vec<TokenStream2> = fields
.named
.iter()
.zip(accessor)
.map(|(field, accessor)| {
let mut name_resolver = NameResolver::new();
let mut items: Vec<TokenStream2> = Vec::with_capacity(fields.named.len());
for (field, accessor) in fields.named.iter().zip(accessor) {
let member_attrs = MemberAttributes::parse_attrs(field.attrs.iter())?;
let ident = field.ident.as_ref().expect("named has idents");
let mut field = ident.to_string();
if let Some(rename_all) = rename_all {
field = field.to_case(rename_all);
let field =
name_resolver.resolve_ident(ident, container_attrs, &member_attrs, None)?;
items.push(quote!(#field => nu_protocol::IntoValue::into_value(#accessor, span)));
}
quote!(#field => nu_protocol::IntoValue::into_value(#accessor, span))
})
.collect();
quote! {
Ok(quote! {
nu_protocol::Value::record(nu_protocol::record! {
#(#items),*
}, span)
})
}
}
Fields::Unnamed(fields) => {
f @ Fields::Unnamed(fields) => {
attributes::deny_fields(f)?;
let items =
fields.unnamed.iter().zip(accessor).map(
|(_, accessor)| quote!(nu_protocol::IntoValue::into_value(#accessor, span)),
);
quote!(nu_protocol::Value::list(std::vec![#(#items),*], span))
Ok(quote!(nu_protocol::Value::list(
std::vec![#(#items),*],
span
)))
}
Fields::Unit => quote!(nu_protocol::Value::nothing(span)),
Fields::Unit => Ok(quote!(nu_protocol::Value::nothing(span))),
}
}

View file

@ -36,6 +36,7 @@ mod case;
mod error;
mod from;
mod into;
mod names;
#[cfg(test)]
mod tests;

View file

@ -0,0 +1,61 @@
use proc_macro2::Span;
use std::collections::HashMap;
use syn::Ident;
use crate::attributes::{ContainerAttributes, MemberAttributes};
use crate::case::{Case, Casing};
use crate::error::DeriveError;
#[derive(Debug, Default)]
pub struct NameResolver {
seen_names: HashMap<String, Span>,
}
impl NameResolver {
pub fn new() -> Self {
Self::default()
}
/// Resolves an identifier using attributes and ensures its uniqueness.
///
/// The identifier is transformed according to these rules:
/// - If [`MemberAttributes::rename`] is set, this explicitly renamed value is used.
/// The value is defined by the helper attribute `#[nu_value(rename = "...")]` on a member.
/// - If the above is not set but [`ContainerAttributes::rename_all`] is, the identifier
/// undergoes case conversion as specified by the helper attribute
/// `#[nu_value(rename_all = "...")]` on the container (struct or enum).
/// - If neither renaming attribute is set, the function applies the case conversion provided
/// by the `default` parameter.
/// If `default` is `None`, the identifier remains unchanged.
///
/// This function checks the transformed identifier against previously seen identifiers to
/// ensure it is unique.
/// If a duplicate identifier is detected, it returns [`DeriveError::NonUniqueName`].
pub fn resolve_ident<M>(
&mut self,
ident: &'_ Ident,
container_attrs: &'_ ContainerAttributes,
member_attrs: &'_ MemberAttributes,
default: impl Into<Option<Case>>,
) -> Result<String, DeriveError<M>> {
let span = ident.span();
let rename_all = container_attrs.rename_all;
let rename = member_attrs.rename.as_ref();
let ident = match (rename, rename_all) {
(Some(rename), _) => rename.to_string(),
(None, Some(case)) => ident.to_case(case),
(None, None) => ident.to_case(default),
};
if let Some(seen) = self.seen_names.get(&ident) {
return Err(DeriveError::NonUniqueName {
name: ident.to_string(),
first: *seen,
second: span,
});
}
self.seen_names.insert(ident.clone(), span);
Ok(ident)
}
}

View file

@ -86,24 +86,81 @@ fn unexpected_attribute() {
}
#[test]
fn deny_attribute_on_fields() {
fn unexpected_attribute_on_struct_field() {
let input = quote! {
struct SomeStruct {
#[nu_value]
field: ()
struct SimpleStruct {
#[nu_value(what)]
field_a: i32,
field_b: String,
}
};
let from_res = derive_from_value(input.clone());
assert!(
matches!(from_res, Err(DeriveError::InvalidAttributePosition { .. })),
matches!(from_res, Err(DeriveError::UnexpectedAttribute { .. })),
"expected `DeriveError::UnexpectedAttribute`, got {:?}",
from_res
);
let into_res = derive_into_value(input);
assert!(
matches!(into_res, Err(DeriveError::UnexpectedAttribute { .. })),
"expected `DeriveError::UnexpectedAttribute`, got {:?}",
into_res
);
}
#[test]
fn unexpected_attribute_on_enum_variant() {
let input = quote! {
enum SimpleEnum {
#[nu_value(what)]
A,
B,
}
};
let from_res = derive_from_value(input.clone());
assert!(
matches!(from_res, Err(DeriveError::UnexpectedAttribute { .. })),
"expected `DeriveError::UnexpectedAttribute`, got {:?}",
from_res
);
let into_res = derive_into_value(input);
assert!(
matches!(into_res, Err(DeriveError::UnexpectedAttribute { .. })),
"expected `DeriveError::UnexpectedAttribute`, got {:?}",
into_res
);
}
#[test]
fn invalid_attribute_position_in_tuple_struct() {
let input = quote! {
struct SimpleTupleStruct(
#[nu_value(what)]
i32,
String,
);
};
let from_res = derive_from_value(input.clone());
assert!(
matches!(
from_res,
Err(DeriveError::InvalidAttributePosition { attribute_span: _ })
),
"expected `DeriveError::InvalidAttributePosition`, got {:?}",
from_res
);
let into_res = derive_into_value(input);
assert!(
matches!(into_res, Err(DeriveError::InvalidAttributePosition { .. })),
matches!(
into_res,
Err(DeriveError::InvalidAttributePosition { attribute_span: _ })
),
"expected `DeriveError::InvalidAttributePosition`, got {:?}",
into_res
);
@ -133,3 +190,53 @@ fn invalid_attribute_value() {
into_res
);
}
#[test]
fn non_unique_struct_keys() {
let input = quote! {
struct DuplicateStruct {
#[nu_value(rename = "field")]
some_field: (),
field: (),
}
};
let from_res = derive_from_value(input.clone());
assert!(
matches!(from_res, Err(DeriveError::NonUniqueName { .. })),
"expected `DeriveError::NonUniqueName`, got {:?}",
from_res
);
let into_res = derive_into_value(input);
assert!(
matches!(into_res, Err(DeriveError::NonUniqueName { .. })),
"expected `DeriveError::NonUniqueName`, got {:?}",
into_res
);
}
#[test]
fn non_unique_enum_variants() {
let input = quote! {
enum DuplicateEnum {
#[nu_value(rename = "variant")]
SomeVariant,
Variant
}
};
let from_res = derive_from_value(input.clone());
assert!(
matches!(from_res, Err(DeriveError::NonUniqueName { .. })),
"expected `DeriveError::NonUniqueName`, got {:?}",
from_res
);
let into_res = derive_into_value(input);
assert!(
matches!(into_res, Err(DeriveError::NonUniqueName { .. })),
"expected `DeriveError::NonUniqueName`, got {:?}",
into_res
);
}

View file

@ -20,13 +20,17 @@ use std::{
///
/// When derived on structs with named fields, it expects a [`Value::Record`] where each field of
/// the struct maps to a corresponding field in the record.
/// You can customize the case conversion of these field names by using
/// `#[nu_value(rename_all = "...")]` on the struct.
///
/// - If `#[nu_value(rename = "...")]` is applied to a field, that name will be used as the key in
/// the record.
/// - If `#[nu_value(rename_all = "...")]` is applied on the container (struct) the key of the
/// field will be case-converted accordingly.
/// - If neither attribute is applied, the field name is used as is.
///
/// Supported case conversions include those provided by [`heck`], such as
/// "snake_case", "kebab-case", "PascalCase", and others.
/// Additionally, all values accepted by
/// [`#[serde(rename_all = "...")]`](https://serde.rs/container-attrs.html#rename_all) are valid here.
/// If not set, the field names will match the original Rust field names as-is.
///
/// For structs with unnamed fields, it expects a [`Value::List`], and the fields are populated in
/// the order they appear in the list.
@ -35,13 +39,18 @@ use std::{
///
/// Only enums with no fields may derive this trait.
/// The expected value representation will be the name of the variant as a [`Value::String`].
/// By default, variant names will be expected in ["snake_case"](heck::ToSnakeCase).
/// You can customize the case conversion using `#[nu_value(rename_all = "kebab-case")]` on the enum.
///
/// - If `#[nu_value(rename = "...")]` is applied to a variant, that name will be used.
/// - If `#[nu_value(rename_all = "...")]` is applied on the enum container, the name of variant
/// will be case-converted accordingly.
/// - If neither attribute is applied, the variant name will default to
/// ["snake_case"](heck::ToSnakeCase).
///
/// Additionally, you can use `#[nu_value(type_name = "...")]` in the derive macro to set a custom type name
/// for `FromValue::expected_type`. This will result in a `Type::Custom` with the specified type name.
/// This can be useful in situations where the default type name is not desired.
///
/// # Enum Example
/// ```
/// # use nu_protocol::{FromValue, Value, ShellError, record, Span};
/// #
@ -52,11 +61,17 @@ use std::{
/// enum Bird {
/// MountainEagle,
/// ForestOwl,
/// #[nu_value(rename = "RIVER-QUACK")]
/// RiverDuck,
/// }
///
/// assert_eq!(
/// Bird::from_value(Value::test_string("RIVER-DUCK")).unwrap(),
/// Bird::from_value(Value::string("FOREST-OWL", span)).unwrap(),
/// Bird::ForestOwl
/// );
///
/// assert_eq!(
/// Bird::from_value(Value::string("RIVER-QUACK", span)).unwrap(),
/// Bird::RiverDuck
/// );
///
@ -64,14 +79,21 @@ use std::{
/// &Bird::expected_type().to_string(),
/// "birb"
/// );
/// ```
///
///
/// # Struct Example
/// ```
/// # use nu_protocol::{FromValue, Value, ShellError, record, Span};
/// #
/// # let span = Span::unknown();
/// #
/// #[derive(FromValue, PartialEq, Eq, Debug)]
/// #[nu_value(rename_all = "kebab-case")]
/// struct Person {
/// first_name: String,
/// last_name: String,
/// age: u32,
/// #[nu_value(rename = "age")]
/// age_years: u32,
/// }
///
/// let value = Value::record(record! {
@ -85,7 +107,7 @@ use std::{
/// Person {
/// first_name: "John".into(),
/// last_name: "Doe".into(),
/// age: 42,
/// age_years: 42,
/// }
/// );
/// ```

View file

@ -10,20 +10,30 @@ use std::{borrow::Borrow, collections::HashMap};
/// This trait can be used with `#[derive]`.
/// When derived on structs with named fields, the resulting value representation will use
/// [`Value::Record`], where each field of the record corresponds to a field of the struct.
/// By default, field names will be kept as-is, but you can customize the case conversion
/// for all fields in a struct by using `#[nu_value(rename_all = "kebab-case")]` on the struct.
/// All case options from [`heck`] are supported, as well as the values allowed by
/// [`#[serde(rename_all)]`](https://serde.rs/container-attrs.html#rename_all).
///
/// By default, field names will be used as-is unless specified otherwise:
/// - If `#[nu_value(rename = "...")]` is applied to a specific field, that name is used.
/// - If `#[nu_value(rename_all = "...")]` is applied to the struct, field names will be
/// case-converted accordingly.
/// - If neither attribute is used, the original field name will be retained.
///
/// For structs with unnamed fields, the value representation will be [`Value::List`], with all
/// fields inserted into a list.
/// Unit structs will be represented as [`Value::Nothing`] since they contain no data.
///
/// Only enums with no fields may derive this trait.
/// The resulting value representation will be the name of the variant as a [`Value::String`].
/// By default, variant names will be converted to ["snake_case"](heck::ToSnakeCase).
/// You can customize the case conversion using `#[nu_value(rename_all = "kebab-case")]` on the enum.
/// For enums, the resulting value representation depends on the variant name:
/// - If `#[nu_value(rename = "...")]` is applied to a specific variant, that name is used.
/// - If `#[nu_value(rename_all = "...")]` is applied to the enum, variant names will be
/// case-converted accordingly.
/// - If neither attribute is used, variant names will default to snake_case.
///
/// Only enums with no fields may derive this trait.
/// The resulting value will be the name of the variant as a [`Value::String`].
///
/// All case options from [`heck`] are supported, as well as the values allowed by
/// [`#[serde(rename_all)]`](https://serde.rs/container-attrs.html#rename_all).
///
/// # Enum Example
/// ```
/// # use nu_protocol::{IntoValue, Value, Span, record};
/// #
@ -34,28 +44,41 @@ use std::{borrow::Borrow, collections::HashMap};
/// enum Bird {
/// MountainEagle,
/// ForestOwl,
/// #[nu_value(rename = "RIVER-QUACK")]
/// RiverDuck,
/// }
///
/// assert_eq!(
/// Bird::RiverDuck.into_value(span),
/// Value::test_string("RIVER-DUCK")
/// Bird::ForestOwl.into_value(span),
/// Value::string("FOREST-OWL", span)
/// );
///
/// assert_eq!(
/// Bird::RiverDuck.into_value(span),
/// Value::string("RIVER-QUACK", span)
/// );
/// ```
///
/// # Struct Example
/// ```
/// # use nu_protocol::{IntoValue, Value, Span, record};
/// #
/// # let span = Span::unknown();
/// #
/// #[derive(IntoValue)]
/// #[nu_value(rename_all = "kebab-case")]
/// struct Person {
/// first_name: String,
/// last_name: String,
/// age: u32,
/// #[nu_value(rename = "age")]
/// age_years: u32,
/// }
///
/// assert_eq!(
/// Person {
/// first_name: "John".into(),
/// last_name: "Doe".into(),
/// age: 42,
/// age_years: 42,
/// }.into_value(span),
/// Value::record(record! {
/// "first-name" => Value::string("John", span),

View file

@ -586,3 +586,86 @@ fn enum_type_name_attr() {
assert_eq!(TypeNameEnum::expected_type().to_string().as_str(), "enum");
}
#[derive(IntoValue, FromValue, Default, Debug, PartialEq)]
struct RenamedFieldStruct {
#[nu_value(rename = "renamed")]
field: (),
}
impl RenamedFieldStruct {
fn value() -> Value {
Value::test_record(record! {
"renamed" => Value::test_nothing(),
})
}
}
#[test]
fn renamed_field_struct_into_value() {
let expected = RenamedFieldStruct::value();
let actual = RenamedFieldStruct::default().into_test_value();
assert_eq!(expected, actual);
}
#[test]
fn renamed_field_struct_from_value() {
let expected = RenamedFieldStruct::default();
let actual = RenamedFieldStruct::from_value(RenamedFieldStruct::value()).unwrap();
assert_eq!(expected, actual);
}
#[test]
fn renamed_field_struct_roundtrip() {
let expected = RenamedFieldStruct::default();
let actual =
RenamedFieldStruct::from_value(RenamedFieldStruct::default().into_test_value()).unwrap();
assert_eq!(expected, actual);
let expected = RenamedFieldStruct::value();
let actual = RenamedFieldStruct::from_value(RenamedFieldStruct::value())
.unwrap()
.into_test_value();
assert_eq!(expected, actual);
}
#[derive(IntoValue, FromValue, Default, Debug, PartialEq)]
enum RenamedVariantEnum {
#[default]
#[nu_value(rename = "renamed")]
Variant,
}
impl RenamedVariantEnum {
fn value() -> Value {
Value::test_string("renamed")
}
}
#[test]
fn renamed_variant_enum_into_value() {
let expected = RenamedVariantEnum::value();
let actual = RenamedVariantEnum::default().into_test_value();
assert_eq!(expected, actual);
}
#[test]
fn renamed_variant_enum_from_value() {
let expected = RenamedVariantEnum::default();
let actual = RenamedVariantEnum::from_value(RenamedVariantEnum::value()).unwrap();
assert_eq!(expected, actual);
}
#[test]
fn renamed_variant_enum_roundtrip() {
let expected = RenamedVariantEnum::default();
let actual =
RenamedVariantEnum::from_value(RenamedVariantEnum::default().into_test_value()).unwrap();
assert_eq!(expected, actual);
let expected = RenamedVariantEnum::value();
let actual = RenamedVariantEnum::from_value(RenamedVariantEnum::value())
.unwrap()
.into_test_value();
assert_eq!(expected, actual);
}