mirror of
https://github.com/nushell/nushell.git
synced 2025-05-30 06:39:33 +02:00
<!-- 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. -->
265 lines
9.7 KiB
Rust
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)),
|
|
}
|
|
}
|