mirror of
https://github.com/nushell/nushell
synced 2024-12-28 14:03:09 +00:00
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. -->
This commit is contained in:
parent
712fec166d
commit
39b0f3bdda
6 changed files with 117 additions and 44 deletions
|
@ -6,12 +6,14 @@ use crate::{error::DeriveError, HELPER_ATTRIBUTE};
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ContainerAttributes {
|
pub struct ContainerAttributes {
|
||||||
pub rename_all: Case,
|
pub rename_all: Case,
|
||||||
|
pub type_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ContainerAttributes {
|
impl Default for ContainerAttributes {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
rename_all: Case::Snake,
|
rename_all: Case::Snake,
|
||||||
|
type_name: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,6 +61,11 @@ impl ContainerAttributes {
|
||||||
};
|
};
|
||||||
container_attrs.rename_all = case;
|
container_attrs.rename_all = case;
|
||||||
}
|
}
|
||||||
|
"type_name" => {
|
||||||
|
let type_name: LitStr = meta.value()?.parse()?;
|
||||||
|
let type_name = type_name.value();
|
||||||
|
container_attrs.type_name = Some(type_name);
|
||||||
|
}
|
||||||
ident => {
|
ident => {
|
||||||
err = Err(DeriveError::UnexpectedAttribute {
|
err = Err(DeriveError::UnexpectedAttribute {
|
||||||
meta_span: ident.span(),
|
meta_span: ident.span(),
|
||||||
|
|
|
@ -42,9 +42,7 @@ pub fn derive_from_value(input: TokenStream2) -> Result<TokenStream2, DeriveErro
|
||||||
|
|
||||||
/// Implements the `#[derive(FromValue)]` macro for structs.
|
/// Implements the `#[derive(FromValue)]` macro for structs.
|
||||||
///
|
///
|
||||||
/// This function ensures that the helper attribute is not used anywhere, as it is not supported for
|
/// This function provides the impl signature for `FromValue`.
|
||||||
/// structs.
|
|
||||||
/// Other than this, this function provides the impl signature for `FromValue`.
|
|
||||||
/// The implementation for `FromValue::from_value` is handled by [`struct_from_value`] and the
|
/// The implementation for `FromValue::from_value` is handled by [`struct_from_value`] and the
|
||||||
/// `FromValue::expected_type` is handled by [`struct_expected_type`].
|
/// `FromValue::expected_type` is handled by [`struct_expected_type`].
|
||||||
fn derive_struct_from_value(
|
fn derive_struct_from_value(
|
||||||
|
@ -53,11 +51,12 @@ fn derive_struct_from_value(
|
||||||
generics: Generics,
|
generics: Generics,
|
||||||
attrs: Vec<Attribute>,
|
attrs: Vec<Attribute>,
|
||||||
) -> Result<TokenStream2, DeriveError> {
|
) -> Result<TokenStream2, DeriveError> {
|
||||||
attributes::deny(&attrs)?;
|
let container_attrs = ContainerAttributes::parse_attrs(attrs.iter())?;
|
||||||
attributes::deny_fields(&data.fields)?;
|
attributes::deny_fields(&data.fields)?;
|
||||||
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
||||||
let from_value_impl = struct_from_value(&data);
|
let from_value_impl = struct_from_value(&data);
|
||||||
let expected_type_impl = struct_expected_type(&data.fields);
|
let expected_type_impl =
|
||||||
|
struct_expected_type(&data.fields, container_attrs.type_name.as_deref());
|
||||||
Ok(quote! {
|
Ok(quote! {
|
||||||
#[automatically_derived]
|
#[automatically_derived]
|
||||||
impl #impl_generics nu_protocol::FromValue for #ident #ty_generics #where_clause {
|
impl #impl_generics nu_protocol::FromValue for #ident #ty_generics #where_clause {
|
||||||
|
@ -219,13 +218,16 @@ fn struct_from_value(data: &DataStruct) -> TokenStream2 {
|
||||||
/// `list[type0, type1, type2]`.
|
/// `list[type0, type1, type2]`.
|
||||||
/// No fields expect the `Type::Nothing`.
|
/// No fields expect the `Type::Nothing`.
|
||||||
///
|
///
|
||||||
|
/// If `#[nu_value(type_name = "...")]` is used, the output type will be `Type::Custom` with that
|
||||||
|
/// passed name.
|
||||||
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// These examples show what the macro would generate.
|
/// These examples show what the macro would generate.
|
||||||
///
|
///
|
||||||
/// Struct with named fields:
|
/// Struct with named fields:
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// #[derive(IntoValue)]
|
/// #[derive(FromValue)]
|
||||||
/// struct Pet {
|
/// struct Pet {
|
||||||
/// name: String,
|
/// name: String,
|
||||||
/// age: u8,
|
/// age: u8,
|
||||||
|
@ -256,7 +258,7 @@ fn struct_from_value(data: &DataStruct) -> TokenStream2 {
|
||||||
///
|
///
|
||||||
/// Struct with unnamed fields:
|
/// Struct with unnamed fields:
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// #[derive(IntoValue)]
|
/// #[derive(FromValue)]
|
||||||
/// struct Color(u8, u8, u8);
|
/// struct Color(u8, u8, u8);
|
||||||
///
|
///
|
||||||
/// impl nu_protocol::FromValue for Color {
|
/// impl nu_protocol::FromValue for Color {
|
||||||
|
@ -276,7 +278,7 @@ fn struct_from_value(data: &DataStruct) -> TokenStream2 {
|
||||||
///
|
///
|
||||||
/// Unit struct:
|
/// Unit struct:
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// #[derive(IntoValue)]
|
/// #[derive(FromValue)]
|
||||||
/// struct Unicorn;
|
/// struct Unicorn;
|
||||||
///
|
///
|
||||||
/// impl nu_protocol::FromValue for Color {
|
/// impl nu_protocol::FromValue for Color {
|
||||||
|
@ -285,9 +287,30 @@ fn struct_from_value(data: &DataStruct) -> TokenStream2 {
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
fn struct_expected_type(fields: &Fields) -> TokenStream2 {
|
///
|
||||||
let ty = match fields {
|
/// Struct with passed type name:
|
||||||
Fields::Named(fields) => {
|
/// ```rust
|
||||||
|
/// #[derive(FromValue)]
|
||||||
|
/// #[nu_value(type_name = "bird")]
|
||||||
|
/// struct Parrot;
|
||||||
|
///
|
||||||
|
/// impl nu_protocol::FromValue for Parrot {
|
||||||
|
/// fn expected_type() -> nu_protocol::Type {
|
||||||
|
/// nu_protocol::Type::Custom(
|
||||||
|
/// <std::string::String as std::convert::From::<&str>>::from("bird")
|
||||||
|
/// .into_boxed_str()
|
||||||
|
/// )
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
fn struct_expected_type(fields: &Fields, attr_type_name: Option<&str>) -> TokenStream2 {
|
||||||
|
let ty = match (fields, attr_type_name) {
|
||||||
|
(_, Some(type_name)) => {
|
||||||
|
quote!(nu_protocol::Type::Custom(
|
||||||
|
<std::string::String as std::convert::From::<&str>>::from(#type_name).into_boxed_str()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
(Fields::Named(fields), _) => {
|
||||||
let fields = fields.named.iter().map(|field| {
|
let fields = fields.named.iter().map(|field| {
|
||||||
let ident = field.ident.as_ref().expect("named has idents");
|
let ident = field.ident.as_ref().expect("named has idents");
|
||||||
let ident_s = ident.to_string();
|
let ident_s = ident.to_string();
|
||||||
|
@ -301,7 +324,7 @@ fn struct_expected_type(fields: &Fields) -> TokenStream2 {
|
||||||
std::vec![#(#fields),*].into_boxed_slice()
|
std::vec![#(#fields),*].into_boxed_slice()
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
Fields::Unnamed(fields) => {
|
(Fields::Unnamed(fields), _) => {
|
||||||
let mut iter = fields.unnamed.iter();
|
let mut iter = fields.unnamed.iter();
|
||||||
let fields = fields.unnamed.iter().map(|field| {
|
let fields = fields.unnamed.iter().map(|field| {
|
||||||
let ty = &field.ty;
|
let ty = &field.ty;
|
||||||
|
@ -324,7 +347,7 @@ fn struct_expected_type(fields: &Fields) -> TokenStream2 {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Fields::Unit => quote!(nu_protocol::Type::Nothing),
|
(Fields::Unit, _) => quote!(nu_protocol::Type::Nothing),
|
||||||
};
|
};
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
|
@ -338,23 +361,25 @@ fn struct_expected_type(fields: &Fields) -> TokenStream2 {
|
||||||
///
|
///
|
||||||
/// This function constructs the implementation of the `FromValue` trait for enums.
|
/// This function constructs the implementation of the `FromValue` trait for enums.
|
||||||
/// It is designed to be on the same level as [`derive_struct_from_value`], even though this
|
/// It is designed to be on the same level as [`derive_struct_from_value`], even though this
|
||||||
/// function only provides the impl signature for `FromValue`.
|
/// implementation is a lot simpler.
|
||||||
/// The actual implementation for `FromValue::from_value` is handled by [`enum_from_value`].
|
/// The main `FromValue::from_value` implementation is handled by [`enum_from_value`].
|
||||||
///
|
/// The `FromValue::expected_type` implementation is usually kept empty to use the default
|
||||||
/// Since variants are difficult to type with the current type system, this function uses the
|
/// implementation, but if `#[nu_value(type_name = "...")]` if given, we use that.
|
||||||
/// default implementation for `expected_type`.
|
|
||||||
fn derive_enum_from_value(
|
fn derive_enum_from_value(
|
||||||
ident: Ident,
|
ident: Ident,
|
||||||
data: DataEnum,
|
data: DataEnum,
|
||||||
generics: Generics,
|
generics: Generics,
|
||||||
attrs: Vec<Attribute>,
|
attrs: Vec<Attribute>,
|
||||||
) -> Result<TokenStream2, DeriveError> {
|
) -> Result<TokenStream2, DeriveError> {
|
||||||
|
let container_attrs = ContainerAttributes::parse_attrs(attrs.iter())?;
|
||||||
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
||||||
let from_value_impl = enum_from_value(&data, &attrs)?;
|
let from_value_impl = enum_from_value(&data, &attrs)?;
|
||||||
|
let expected_type_impl = enum_expected_type(container_attrs.type_name.as_deref());
|
||||||
Ok(quote! {
|
Ok(quote! {
|
||||||
#[automatically_derived]
|
#[automatically_derived]
|
||||||
impl #impl_generics nu_protocol::FromValue for #ident #ty_generics #where_clause {
|
impl #impl_generics nu_protocol::FromValue for #ident #ty_generics #where_clause {
|
||||||
#from_value_impl
|
#from_value_impl
|
||||||
|
#expected_type_impl
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -448,6 +473,41 @@ fn enum_from_value(data: &DataEnum, attrs: &[Attribute]) -> Result<TokenStream2,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Implements `FromValue::expected_type` for enums.
|
||||||
|
///
|
||||||
|
/// Since it's difficult to name the type of an enum in the current type system, we want to use the
|
||||||
|
/// default implementation if `#[nu_value(type_name = "...")]` was *not* given.
|
||||||
|
/// For that, a `None` value is returned, for a passed type name we return something like this:
|
||||||
|
/// ```rust
|
||||||
|
/// #[derive(IntoValue)]
|
||||||
|
/// #[nu_value(type_name = "sunny | cloudy | raining")]
|
||||||
|
/// enum Weather {
|
||||||
|
/// Sunny,
|
||||||
|
/// Cloudy,
|
||||||
|
/// Raining
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl nu_protocol::FromValue for Weather {
|
||||||
|
/// fn expected_type() -> nu_protocol::Type {
|
||||||
|
/// nu_protocol::Type::Custom(
|
||||||
|
/// <std::string::String as std::convert::From::<&str>>::from("sunny | cloudy | raining")
|
||||||
|
/// .into_boxed_str()
|
||||||
|
/// )
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
fn enum_expected_type(attr_type_name: Option<&str>) -> Option<TokenStream2> {
|
||||||
|
let type_name = attr_type_name?;
|
||||||
|
Some(quote! {
|
||||||
|
fn expected_type() -> nu_protocol::Type {
|
||||||
|
nu_protocol::Type::Custom(
|
||||||
|
<std::string::String as std::convert::From::<&str>>::from(#type_name)
|
||||||
|
.into_boxed_str()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Parses a `Value` into self.
|
/// Parses a `Value` into self.
|
||||||
///
|
///
|
||||||
/// This function handles the actual parsing of a `Value` into self.
|
/// This function handles the actual parsing of a `Value` into self.
|
||||||
|
|
|
@ -50,8 +50,6 @@ pub fn derive_into_value(input: TokenStream2) -> Result<TokenStream2, DeriveErro
|
||||||
/// For structs with unnamed fields, this generates a `Value::List` with each field in the list.
|
/// 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.
|
/// For unit structs, this generates `Value::Nothing`, because there is no data.
|
||||||
///
|
///
|
||||||
/// Note: The helper attribute `#[nu_value(...)]` is currently not allowed on structs.
|
|
||||||
///
|
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// These examples show what the macro would generate.
|
/// These examples show what the macro would generate.
|
||||||
|
@ -109,7 +107,7 @@ fn struct_into_value(
|
||||||
generics: Generics,
|
generics: Generics,
|
||||||
attrs: Vec<Attribute>,
|
attrs: Vec<Attribute>,
|
||||||
) -> Result<TokenStream2, DeriveError> {
|
) -> Result<TokenStream2, DeriveError> {
|
||||||
attributes::deny(&attrs)?;
|
let _ = ContainerAttributes::parse_attrs(attrs.iter())?;
|
||||||
attributes::deny_fields(&data.fields)?;
|
attributes::deny_fields(&data.fields)?;
|
||||||
let record = match &data.fields {
|
let record = match &data.fields {
|
||||||
Fields::Named(fields) => {
|
Fields::Named(fields) => {
|
||||||
|
|
|
@ -85,28 +85,6 @@ fn unexpected_attribute() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn deny_attribute_on_structs() {
|
|
||||||
let input = quote! {
|
|
||||||
#[nu_value]
|
|
||||||
struct SomeStruct;
|
|
||||||
};
|
|
||||||
|
|
||||||
let from_res = derive_from_value(input.clone());
|
|
||||||
assert!(
|
|
||||||
matches!(from_res, Err(DeriveError::InvalidAttributePosition { .. })),
|
|
||||||
"expected `DeriveError::InvalidAttributePosition`, got {:?}",
|
|
||||||
from_res
|
|
||||||
);
|
|
||||||
|
|
||||||
let into_res = derive_into_value(input);
|
|
||||||
assert!(
|
|
||||||
matches!(into_res, Err(DeriveError::InvalidAttributePosition { .. })),
|
|
||||||
"expected `DeriveError::InvalidAttributePosition`, got {:?}",
|
|
||||||
into_res
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn deny_attribute_on_fields() {
|
fn deny_attribute_on_fields() {
|
||||||
let input = quote! {
|
let input = quote! {
|
||||||
|
|
|
@ -34,10 +34,14 @@ use std::{
|
||||||
/// [`#[serde(rename_all = "...")]`](https://serde.rs/container-attrs.html#rename_all) are valid
|
/// [`#[serde(rename_all = "...")]`](https://serde.rs/container-attrs.html#rename_all) are valid
|
||||||
/// here.
|
/// here.
|
||||||
///
|
///
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use nu_protocol::{FromValue, Value, ShellError};
|
/// # use nu_protocol::{FromValue, Value, ShellError};
|
||||||
/// #[derive(FromValue, Debug, PartialEq)]
|
/// #[derive(FromValue, Debug, PartialEq)]
|
||||||
/// #[nu_value(rename_all = "COBOL-CASE")]
|
/// #[nu_value(rename_all = "COBOL-CASE", type_name = "birb")]
|
||||||
/// enum Bird {
|
/// enum Bird {
|
||||||
/// MountainEagle,
|
/// MountainEagle,
|
||||||
/// ForestOwl,
|
/// ForestOwl,
|
||||||
|
@ -48,6 +52,11 @@ use std::{
|
||||||
/// Bird::from_value(Value::test_string("RIVER-DUCK")).unwrap(),
|
/// Bird::from_value(Value::test_string("RIVER-DUCK")).unwrap(),
|
||||||
/// Bird::RiverDuck
|
/// Bird::RiverDuck
|
||||||
/// );
|
/// );
|
||||||
|
///
|
||||||
|
/// assert_eq!(
|
||||||
|
/// &Bird::expected_type().to_string(),
|
||||||
|
/// "birb"
|
||||||
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
pub trait FromValue: Sized {
|
pub trait FromValue: Sized {
|
||||||
// TODO: instead of ShellError, maybe we could have a FromValueError that implements Into<ShellError>
|
// TODO: instead of ShellError, maybe we could have a FromValueError that implements Into<ShellError>
|
||||||
|
|
|
@ -494,3 +494,24 @@ fn bytes_roundtrip() {
|
||||||
.into_test_value();
|
.into_test_value();
|
||||||
assert_eq!(expected, actual);
|
assert_eq!(expected, actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn struct_type_name_attr() {
|
||||||
|
#[derive(FromValue, Debug)]
|
||||||
|
#[nu_value(type_name = "struct")]
|
||||||
|
struct TypeNameStruct;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
TypeNameStruct::expected_type().to_string().as_str(),
|
||||||
|
"struct"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn enum_type_name_attr() {
|
||||||
|
#[derive(FromValue, Debug)]
|
||||||
|
#[nu_value(type_name = "enum")]
|
||||||
|
struct TypeNameEnum;
|
||||||
|
|
||||||
|
assert_eq!(TypeNameEnum::expected_type().to_string().as_str(), "enum");
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue