diff --git a/crates/nu-derive-value/src/attributes.rs b/crates/nu-derive-value/src/attributes.rs index 6969b64287..428c5aaae6 100644 --- a/crates/nu-derive-value/src/attributes.rs +++ b/crates/nu-derive-value/src/attributes.rs @@ -6,12 +6,14 @@ use crate::{error::DeriveError, HELPER_ATTRIBUTE}; #[derive(Debug)] pub struct ContainerAttributes { pub rename_all: Case, + pub type_name: Option, } impl Default for ContainerAttributes { fn default() -> Self { Self { rename_all: Case::Snake, + type_name: None, } } } @@ -59,6 +61,11 @@ impl ContainerAttributes { }; 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 => { err = Err(DeriveError::UnexpectedAttribute { meta_span: ident.span(), diff --git a/crates/nu-derive-value/src/from.rs b/crates/nu-derive-value/src/from.rs index 783a22920e..901cc53d30 100644 --- a/crates/nu-derive-value/src/from.rs +++ b/crates/nu-derive-value/src/from.rs @@ -42,9 +42,7 @@ pub fn derive_from_value(input: TokenStream2) -> Result, ) -> Result { - attributes::deny(&attrs)?; + 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); - 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! { #[automatically_derived] 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]`. /// 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 /// /// These examples show what the macro would generate. /// /// Struct with named fields: /// ```rust -/// #[derive(IntoValue)] +/// #[derive(FromValue)] /// struct Pet { /// name: String, /// age: u8, @@ -256,7 +258,7 @@ fn struct_from_value(data: &DataStruct) -> TokenStream2 { /// /// Struct with unnamed fields: /// ```rust -/// #[derive(IntoValue)] +/// #[derive(FromValue)] /// struct Color(u8, u8, u8); /// /// impl nu_protocol::FromValue for Color { @@ -276,7 +278,7 @@ fn struct_from_value(data: &DataStruct) -> TokenStream2 { /// /// Unit struct: /// ```rust -/// #[derive(IntoValue)] +/// #[derive(FromValue)] /// struct Unicorn; /// /// 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 { - Fields::Named(fields) => { +/// +/// Struct with passed type name: +/// ```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( +/// >::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( + >::from(#type_name).into_boxed_str() + )) + } + (Fields::Named(fields), _) => { let fields = fields.named.iter().map(|field| { let ident = field.ident.as_ref().expect("named has idents"); let ident_s = ident.to_string(); @@ -301,7 +324,7 @@ fn struct_expected_type(fields: &Fields) -> TokenStream2 { std::vec![#(#fields),*].into_boxed_slice() )) } - Fields::Unnamed(fields) => { + (Fields::Unnamed(fields), _) => { let mut iter = fields.unnamed.iter(); let fields = fields.unnamed.iter().map(|field| { 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! { @@ -338,23 +361,25 @@ fn struct_expected_type(fields: &Fields) -> TokenStream2 { /// /// 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 -/// function only provides the impl signature for `FromValue`. -/// The actual implementation for `FromValue::from_value` is handled by [`enum_from_value`]. -/// -/// Since variants are difficult to type with the current type system, this function uses the -/// default implementation for `expected_type`. +/// implementation is a lot simpler. +/// 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 +/// implementation, but if `#[nu_value(type_name = "...")]` if given, we use that. fn derive_enum_from_value( ident: Ident, data: DataEnum, generics: Generics, attrs: Vec, ) -> 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)?; + let expected_type_impl = enum_expected_type(container_attrs.type_name.as_deref()); Ok(quote! { #[automatically_derived] impl #impl_generics nu_protocol::FromValue for #ident #ty_generics #where_clause { #from_value_impl + #expected_type_impl } }) } @@ -448,6 +473,41 @@ fn enum_from_value(data: &DataEnum, attrs: &[Attribute]) -> Result nu_protocol::Type { +/// nu_protocol::Type::Custom( +/// >::from("sunny | cloudy | raining") +/// .into_boxed_str() +/// ) +/// } +/// } +/// ``` +fn enum_expected_type(attr_type_name: Option<&str>) -> Option { + let type_name = attr_type_name?; + Some(quote! { + fn expected_type() -> nu_protocol::Type { + nu_protocol::Type::Custom( + >::from(#type_name) + .into_boxed_str() + ) + } + }) +} + /// Parses a `Value` into self. /// /// This function handles the actual parsing of a `Value` into self. diff --git a/crates/nu-derive-value/src/into.rs b/crates/nu-derive-value/src/into.rs index a7cec06378..cb1d82352d 100644 --- a/crates/nu-derive-value/src/into.rs +++ b/crates/nu-derive-value/src/into.rs @@ -50,8 +50,6 @@ pub fn derive_into_value(input: TokenStream2) -> Result, ) -> Result { - attributes::deny(&attrs)?; + let _ = ContainerAttributes::parse_attrs(attrs.iter())?; attributes::deny_fields(&data.fields)?; let record = match &data.fields { Fields::Named(fields) => { diff --git a/crates/nu-derive-value/src/tests.rs b/crates/nu-derive-value/src/tests.rs index b94d12a732..ca51ed5970 100644 --- a/crates/nu-derive-value/src/tests.rs +++ b/crates/nu-derive-value/src/tests.rs @@ -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] fn deny_attribute_on_fields() { let input = quote! { diff --git a/crates/nu-protocol/src/value/from_value.rs b/crates/nu-protocol/src/value/from_value.rs index d021b0b3a8..c5445446c9 100644 --- a/crates/nu-protocol/src/value/from_value.rs +++ b/crates/nu-protocol/src/value/from_value.rs @@ -34,10 +34,14 @@ use std::{ /// [`#[serde(rename_all = "...")]`](https://serde.rs/container-attrs.html#rename_all) are valid /// 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}; /// #[derive(FromValue, Debug, PartialEq)] -/// #[nu_value(rename_all = "COBOL-CASE")] +/// #[nu_value(rename_all = "COBOL-CASE", type_name = "birb")] /// enum Bird { /// MountainEagle, /// ForestOwl, @@ -48,6 +52,11 @@ use std::{ /// Bird::from_value(Value::test_string("RIVER-DUCK")).unwrap(), /// Bird::RiverDuck /// ); +/// +/// assert_eq!( +/// &Bird::expected_type().to_string(), +/// "birb" +/// ); /// ``` pub trait FromValue: Sized { // TODO: instead of ShellError, maybe we could have a FromValueError that implements Into diff --git a/crates/nu-protocol/src/value/test_derive.rs b/crates/nu-protocol/src/value/test_derive.rs index 263d5f3ed4..182d37f980 100644 --- a/crates/nu-protocol/src/value/test_derive.rs +++ b/crates/nu-protocol/src/value/test_derive.rs @@ -494,3 +494,24 @@ fn bytes_roundtrip() { .into_test_value(); 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"); +}