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
11 changed files with 599 additions and 210 deletions

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
})
}
},
}),
}
}