mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 19:07:42 +02:00
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:
@ -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
|
||||
})
|
||||
}
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user