Piepmatz 39b0f3bdda
Change expected type for derived FromValue implementations via attribute (#13647)
<!--
if this PR closes one or more issues, you can automatically link the PR
with
them by using one of the [*linking
keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword),
e.g.
- this PR should close #xxxx
- fixes #xxxx

you can also mention related issues, PRs or discussions!
-->

# Description
<!--
Thank you for improving Nushell. Please, check our [contributing
guide](../CONTRIBUTING.md) and talk to the core team before making major
changes.

Description of your pull request goes here. **Provide examples and/or
screenshots** if your changes affect the user experience.
-->
In this PR I expanded the helper attribute `#[nu_value]` on
`#[derive(FromValue)]`. It now allows the usage of `#[nu_value(type_name
= "...")]` to set a type name for the `FromValue::expected_type`
implementation. Currently it only uses the default implementation but
I'd like to change that without having to manually implement the entire
trait on my own.

# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->
Users that derive `FromValue` may now change the name of the expected
type.

# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the
tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->
I added some tests that check if this feature work and updated the
documentation about the derive macro.

- 🟢 `toolkit fmt`
- 🟢 `toolkit clippy`
- 🟢 `toolkit test`
- 🟢 `toolkit test stdlib`

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->
2024-08-23 06:47:15 -05:00

265 lines
9.7 KiB
Rust

use convert_case::Casing;
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens};
use syn::{
spanned::Spanned, Attribute, Data, DataEnum, DataStruct, DeriveInput, Fields, Generics, Ident,
Index,
};
use crate::attributes::{self, ContainerAttributes};
#[derive(Debug)]
pub struct IntoValue;
type DeriveError = super::error::DeriveError<IntoValue>;
/// Inner implementation of the `#[derive(IntoValue)]` macro for structs and enums.
///
/// Uses `proc_macro2::TokenStream` for better testing support, unlike `proc_macro::TokenStream`.
///
/// This function directs the `IntoValue` trait derivation to the correct implementation based on
/// the input type:
/// - 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> {
let input: DeriveInput = syn::parse2(input).map_err(DeriveError::Syn)?;
match input.data {
Data::Struct(data_struct) => Ok(struct_into_value(
input.ident,
data_struct,
input.generics,
input.attrs,
)?),
Data::Enum(data_enum) => Ok(enum_into_value(
input.ident,
data_enum,
input.generics,
input.attrs,
)?),
Data::Union(_) => Err(DeriveError::UnsupportedUnions),
}
}
/// Implements the `#[derive(IntoValue)]` macro for structs.
///
/// Automatically derives the `IntoValue` trait for any struct where each field implements
/// `IntoValue`.
/// For structs with named fields, the derived implementation creates a `Value::Record` using the
/// struct fields as keys.
/// 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.
///
/// # Examples
///
/// These examples show what the macro would generate.
///
/// Struct with named fields:
/// ```rust
/// #[derive(IntoValue)]
/// struct Pet {
/// name: String,
/// age: u8,
/// favorite_toy: Option<String>,
/// }
///
/// impl nu_protocol::IntoValue for Pet {
/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
/// nu_protocol::Value::record(nu_protocol::record! {
/// "name" => nu_protocol::IntoValue::into_value(self.name, span),
/// "age" => nu_protocol::IntoValue::into_value(self.age, span),
/// "favorite_toy" => nu_protocol::IntoValue::into_value(self.favorite_toy, span),
/// }, span)
/// }
/// }
/// ```
///
/// Struct with unnamed fields:
/// ```rust
/// #[derive(IntoValue)]
/// struct Color(u8, u8, u8);
///
/// impl nu_protocol::IntoValue for Color {
/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
/// nu_protocol::Value::list(vec![
/// nu_protocol::IntoValue::into_value(self.0, span),
/// nu_protocol::IntoValue::into_value(self.1, span),
/// nu_protocol::IntoValue::into_value(self.2, span),
/// ], span)
/// }
/// }
/// ```
///
/// Unit struct:
/// ```rust
/// #[derive(IntoValue)]
/// struct Unicorn;
///
/// impl nu_protocol::IntoValue for Unicorn {
/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
/// nu_protocol::Value::nothing(span)
/// }
/// }
/// ```
fn struct_into_value(
ident: Ident,
data: DataStruct,
generics: Generics,
attrs: Vec<Attribute>,
) -> Result<TokenStream2, DeriveError> {
let _ = ContainerAttributes::parse_attrs(attrs.iter())?;
attributes::deny_fields(&data.fields)?;
let record = match &data.fields {
Fields::Named(fields) => {
let accessor = fields
.named
.iter()
.map(|field| field.ident.as_ref().expect("named has idents"))
.map(|ident| quote!(self.#ident));
fields_return_value(&data.fields, accessor)
}
Fields::Unnamed(fields) => {
let accessor = fields
.unnamed
.iter()
.enumerate()
.map(|(n, _)| Index::from(n))
.map(|index| quote!(self.#index));
fields_return_value(&data.fields, accessor)
}
Fields::Unit => quote!(nu_protocol::Value::nothing(span)),
};
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
Ok(quote! {
#[automatically_derived]
impl #impl_generics nu_protocol::IntoValue for #ident #ty_generics #where_clause {
fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
#record
}
}
})
}
/// Implements the `#[derive(IntoValue)]` macro for enums.
///
/// 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`.
///
/// This is how such a derived implementation looks:
/// ```rust
/// #[derive(IntoValue)]
/// enum Weather {
/// Sunny,
/// Cloudy,
/// Raining
/// }
///
/// impl nu_protocol::IntoValue for Weather {
/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::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),
/// }
/// }
/// }
/// ```
fn enum_into_value(
ident: Ident,
data: DataEnum,
generics: Generics,
attrs: Vec<Attribute>,
) -> Result<TokenStream2, DeriveError> {
let container_attrs = ContainerAttributes::parse_attrs(attrs.iter())?;
let arms: Vec<TokenStream2> = data
.variants
.into_iter()
.map(|variant| {
attributes::deny(&variant.attrs)?;
let ident = variant.ident;
let ident_s = format!("{ident}")
.as_str()
.to_case(container_attrs.rename_all);
match &variant.fields {
// In the future we can implement more complexe enums here.
Fields::Named(fields) => Err(DeriveError::UnsupportedEnums {
fields_span: fields.span(),
}),
Fields::Unnamed(fields) => Err(DeriveError::UnsupportedEnums {
fields_span: fields.span(),
}),
Fields::Unit => {
Ok(quote!(Self::#ident => nu_protocol::Value::string(#ident_s, span)))
}
}
})
.collect::<Result<_, _>>()?;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
Ok(quote! {
impl #impl_generics nu_protocol::IntoValue for #ident #ty_generics #where_clause {
fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
match self {
#(#arms,)*
}
}
}
})
}
/// 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 two parameters: the `fields`, which allow iterating over each field of a data
/// type, and the `accessor`.
/// 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.
///
/// 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
/// variants in the future.
fn fields_return_value(
fields: &Fields,
accessor: impl Iterator<Item = impl ToTokens>,
) -> TokenStream2 {
match fields {
Fields::Named(fields) => {
let items: Vec<TokenStream2> = fields
.named
.iter()
.zip(accessor)
.map(|(field, accessor)| {
let ident = field.ident.as_ref().expect("named has idents");
let field = ident.to_string();
quote!(#field => nu_protocol::IntoValue::into_value(#accessor, span))
})
.collect();
quote! {
nu_protocol::Value::record(nu_protocol::record! {
#(#items),*
}, span)
}
}
Fields::Unnamed(fields) => {
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))
}
Fields::Unit => quote!(nu_protocol::Value::nothing(span)),
}
}