From b79a2255d2ab2bb93571df55371290814ca718e5 Mon Sep 17 00:00:00 2001 From: Piepmatz Date: Tue, 18 Jun 2024 01:05:11 +0200 Subject: [PATCH] Add derive macros for `FromValue` and `IntoValue` to ease the use of `Value`s in Rust code (#13031) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description After discussing with @sholderbach the cumbersome usage of `nu_protocol::Value` in Rust, I created a derive macro to simplify it. I’ve added a new crate called `nu-derive-value`, which includes two macros, `IntoValue` and `FromValue`. These are re-exported in `nu-protocol` and should be encouraged to be used via that re-export. The macros ensure that all types can easily convert from and into `Value`. For example, as a plugin author, you can define your plugin configuration using a Rust struct and easily convert it using `FromValue`. This makes plugin configuration less of a hassle. I introduced the `IntoValue` trait for a standardized approach to converting values into `Value` (and a fallible variant `TryIntoValue`). This trait could potentially replace existing `into_value` methods. Along with this, I've implemented `FromValue` for several standard types and refined other implementations to use blanket implementations where applicable. I made these design choices with input from @devyn. There are more improvements possible, but this is a solid start and the PR is already quite substantial. # User-Facing Changes For `nu-protocol` users, these changes simplify the handling of `Value`s. There are no changes for end-users of nushell itself. # Tests + Formatting Documenting the macros itself is not really possible, as they cannot really reference any other types since they are the root of the dependency graph. The standard library has the same problem ([std::Debug](https://doc.rust-lang.org/stable/std/fmt/derive.Debug.html)). However I documented the `FromValue` and `IntoValue` traits completely. For testing, I made of use `proc-macro2` in the derive macro code. This would allow testing the generated source code. Instead I just tested that the derived functionality is correct. This is done in `nu_protocol::value::test_derive`, as a consumer of `nu-derive-value` needs to do the testing of the macro usage. I think that these tests should provide a stable baseline so that users can be sure that the impl works. # After Submitting With these macros available, we can probably use them in some examples for plugins to showcase the use of them. --- Cargo.lock | 22 + Cargo.toml | 6 + crates/nu-cmd-base/src/hook.rs | 2 +- crates/nu-command/src/charting/histogram.rs | 2 +- .../src/conversions/into/cell_path.rs | 2 +- crates/nu-command/src/filters/split_by.rs | 2 +- crates/nu-command/src/filters/uniq_by.rs | 2 +- crates/nu-command/src/sort_utils.rs | 2 +- crates/nu-derive-value/Cargo.toml | 21 + crates/nu-derive-value/LICENSE | 21 + crates/nu-derive-value/src/attributes.rs | 116 +++ crates/nu-derive-value/src/error.rs | 82 ++ crates/nu-derive-value/src/from.rs | 539 ++++++++++++ crates/nu-derive-value/src/into.rs | 266 ++++++ crates/nu-derive-value/src/lib.rs | 69 ++ crates/nu-derive-value/src/tests.rs | 157 ++++ crates/nu-protocol/Cargo.toml | 2 + crates/nu-protocol/src/errors/shell_error.rs | 4 +- crates/nu-protocol/src/lib.rs | 2 + crates/nu-protocol/src/value/from_value.rs | 822 ++++++++++-------- crates/nu-protocol/src/value/into_value.rs | 196 +++++ crates/nu-protocol/src/value/mod.rs | 38 +- crates/nu-protocol/src/value/test_derive.rs | 386 ++++++++ .../src/cool_custom_value.rs | 2 +- 24 files changed, 2378 insertions(+), 385 deletions(-) create mode 100644 crates/nu-derive-value/Cargo.toml create mode 100644 crates/nu-derive-value/LICENSE create mode 100644 crates/nu-derive-value/src/attributes.rs create mode 100644 crates/nu-derive-value/src/error.rs create mode 100644 crates/nu-derive-value/src/from.rs create mode 100644 crates/nu-derive-value/src/into.rs create mode 100644 crates/nu-derive-value/src/lib.rs create mode 100644 crates/nu-derive-value/src/tests.rs create mode 100644 crates/nu-protocol/src/value/into_value.rs create mode 100644 crates/nu-protocol/src/value/test_derive.rs diff --git a/Cargo.lock b/Cargo.lock index e0c8b048ab..b88af73c25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -920,6 +920,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -3020,6 +3029,17 @@ dependencies = [ "winreg", ] +[[package]] +name = "nu-derive-value" +version = "0.94.3" +dependencies = [ + "convert_case", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.60", +] + [[package]] name = "nu-engine" version = "0.94.3" @@ -3209,11 +3229,13 @@ dependencies = [ "byte-unit", "chrono", "chrono-humanize", + "convert_case", "fancy-regex", "indexmap", "lru", "miette", "nix", + "nu-derive-value", "nu-path", "nu-system", "nu-test-support", diff --git a/Cargo.toml b/Cargo.toml index d59d7cdbff..18f45987e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ members = [ "crates/nu-lsp", "crates/nu-pretty-hex", "crates/nu-protocol", + "crates/nu-derive-value", "crates/nu-plugin", "crates/nu-plugin-core", "crates/nu-plugin-engine", @@ -74,6 +75,7 @@ chardetng = "0.1.17" chrono = { default-features = false, version = "0.4.34" } chrono-humanize = "0.2.3" chrono-tz = "0.8" +convert_case = "0.6" crossbeam-channel = "0.5.8" crossterm = "0.27" csv = "1.3" @@ -123,11 +125,14 @@ pathdiff = "0.2" percent-encoding = "2" pretty_assertions = "1.4" print-positions = "0.6" +proc-macro-error = { version = "1.0", default-features = false } +proc-macro2 = "1.0" procfs = "0.16.0" pwd = "1.3" quick-xml = "0.31.0" quickcheck = "1.0" quickcheck_macros = "1.0" +quote = "1.0" rand = "0.8" ratatui = "0.26" rayon = "1.10" @@ -147,6 +152,7 @@ serde_urlencoded = "0.7.1" serde_yaml = "0.9" sha2 = "0.10" strip-ansi-escapes = "0.2.0" +syn = "2.0" sysinfo = "0.30" tabled = { version = "0.14.0", default-features = false } tempfile = "3.10" diff --git a/crates/nu-cmd-base/src/hook.rs b/crates/nu-cmd-base/src/hook.rs index 76c13bd5c3..cef5348618 100644 --- a/crates/nu-cmd-base/src/hook.rs +++ b/crates/nu-cmd-base/src/hook.rs @@ -194,7 +194,7 @@ pub fn eval_hook( let Some(follow) = val.get("code") else { return Err(ShellError::CantFindColumn { col_name: "code".into(), - span, + span: Some(span), src_span: span, }); }; diff --git a/crates/nu-command/src/charting/histogram.rs b/crates/nu-command/src/charting/histogram.rs index 52964b087d..29515c96c5 100755 --- a/crates/nu-command/src/charting/histogram.rs +++ b/crates/nu-command/src/charting/histogram.rs @@ -194,7 +194,7 @@ fn run_histogram( if inputs.is_empty() { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: head_span, + span: Some(head_span), src_span: list_span, }); } diff --git a/crates/nu-command/src/conversions/into/cell_path.rs b/crates/nu-command/src/conversions/into/cell_path.rs index c05dad57ba..1342fb14c0 100644 --- a/crates/nu-command/src/conversions/into/cell_path.rs +++ b/crates/nu-command/src/conversions/into/cell_path.rs @@ -154,7 +154,7 @@ fn record_to_path_member( let Some(value) = record.get("value") else { return Err(ShellError::CantFindColumn { col_name: "value".into(), - span: val_span, + span: Some(val_span), src_span: span, }); }; diff --git a/crates/nu-command/src/filters/split_by.rs b/crates/nu-command/src/filters/split_by.rs index 0d3bf1cd30..419119fe0c 100644 --- a/crates/nu-command/src/filters/split_by.rs +++ b/crates/nu-command/src/filters/split_by.rs @@ -130,7 +130,7 @@ pub fn split( Some(group_key) => Ok(group_key.coerce_string()?), None => Err(ShellError::CantFindColumn { col_name: column_name.item.to_string(), - span: column_name.span, + span: Some(column_name.span), src_span: row.span(), }), } diff --git a/crates/nu-command/src/filters/uniq_by.rs b/crates/nu-command/src/filters/uniq_by.rs index 7bbeb0afe2..e1a502b06b 100644 --- a/crates/nu-command/src/filters/uniq_by.rs +++ b/crates/nu-command/src/filters/uniq_by.rs @@ -123,7 +123,7 @@ fn validate(vec: &[Value], columns: &[String], span: Span) -> Result<(), ShellEr if let Some(nonexistent) = nonexistent_column(columns, record.columns()) { return Err(ShellError::CantFindColumn { col_name: nonexistent, - span, + span: Some(span), src_span: val_span, }); } diff --git a/crates/nu-command/src/sort_utils.rs b/crates/nu-command/src/sort_utils.rs index 50d8256353..01bb604eac 100644 --- a/crates/nu-command/src/sort_utils.rs +++ b/crates/nu-command/src/sort_utils.rs @@ -81,7 +81,7 @@ pub fn sort( if let Some(nonexistent) = nonexistent_column(&sort_columns, record.columns()) { return Err(ShellError::CantFindColumn { col_name: nonexistent, - span, + span: Some(span), src_span: val_span, }); } diff --git a/crates/nu-derive-value/Cargo.toml b/crates/nu-derive-value/Cargo.toml new file mode 100644 index 0000000000..45395b2ddb --- /dev/null +++ b/crates/nu-derive-value/Cargo.toml @@ -0,0 +1,21 @@ +[package] +authors = ["The Nushell Project Developers"] +description = "Macros implementation of #[derive(FromValue, IntoValue)]" +edition = "2021" +license = "MIT" +name = "nu-derive-value" +repository = "https://github.com/nushell/nushell/tree/main/crates/nu-derive-value" +version = "0.94.3" + +[lib] +proc-macro = true +# we can only use exposed macros in doctests really, +# so we cannot test anything useful in a doctest +doctest = false + +[dependencies] +proc-macro2 = { workspace = true } +syn = { workspace = true } +quote = { workspace = true } +proc-macro-error = { workspace = true } +convert_case = { workspace = true } diff --git a/crates/nu-derive-value/LICENSE b/crates/nu-derive-value/LICENSE new file mode 100644 index 0000000000..ae174e8595 --- /dev/null +++ b/crates/nu-derive-value/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 - 2023 The Nushell Project Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/nu-derive-value/src/attributes.rs b/crates/nu-derive-value/src/attributes.rs new file mode 100644 index 0000000000..6969b64287 --- /dev/null +++ b/crates/nu-derive-value/src/attributes.rs @@ -0,0 +1,116 @@ +use convert_case::Case; +use syn::{spanned::Spanned, Attribute, Fields, LitStr}; + +use crate::{error::DeriveError, HELPER_ATTRIBUTE}; + +#[derive(Debug)] +pub struct ContainerAttributes { + pub rename_all: Case, +} + +impl Default for ContainerAttributes { + fn default() -> Self { + Self { + rename_all: Case::Snake, + } + } +} + +impl ContainerAttributes { + pub fn parse_attrs<'a, M>( + iter: impl Iterator, + ) -> Result> { + let mut container_attrs = ContainerAttributes::default(); + for attr in filter(iter) { + // This is a container to allow returning derive errors inside the parse_nested_meta fn. + let mut err = Ok(()); + + attr.parse_nested_meta(|meta| { + let ident = meta.path.require_ident()?; + match ident.to_string().as_str() { + "rename_all" => { + // The matched case are all useful variants from `convert_case` with aliases + // that `serde` uses. + let case: LitStr = meta.value()?.parse()?; + let case = match case.value().as_str() { + "UPPER CASE" | "UPPER WITH SPACES CASE" => Case::Upper, + "lower case" | "lower with spaces case" => Case::Lower, + "Title Case" => Case::Title, + "camelCase" => Case::Camel, + "PascalCase" | "UpperCamelCase" => Case::Pascal, + "snake_case" => Case::Snake, + "UPPER_SNAKE_CASE" | "SCREAMING_SNAKE_CASE" => Case::UpperSnake, + "kebab-case" => Case::Kebab, + "COBOL-CASE" | "UPPER-KEBAB-CASE" | "SCREAMING-KEBAB-CASE" => { + Case::Cobol + } + "Train-Case" => Case::Train, + "flatcase" | "lowercase" => Case::Flat, + "UPPERFLATCASE" | "UPPERCASE" => Case::UpperFlat, + // Although very funny, we don't support `Case::{Toggle, Alternating}`, + // as we see no real benefit. + c => { + err = Err(DeriveError::InvalidAttributeValue { + value_span: case.span(), + value: Box::new(c.to_string()), + }); + return Ok(()); // We stored the err in `err`. + } + }; + container_attrs.rename_all = case; + } + ident => { + err = Err(DeriveError::UnexpectedAttribute { + meta_span: ident.span(), + }); + } + } + + Ok(()) + }) + .map_err(DeriveError::Syn)?; + + err?; // Shortcircuit here if `err` is holding some error. + } + + Ok(container_attrs) + } +} + +pub fn filter<'a>( + iter: impl Iterator, +) -> impl Iterator { + iter.filter(|attr| attr.path().is_ident(HELPER_ATTRIBUTE)) +} + +// The deny functions are built to easily deny the use of the helper attribute if used incorrectly. +// As the usage of it gets more complex, these functions might be discarded or replaced. + +/// Deny any attribute that uses the helper attribute. +pub fn deny(attrs: &[Attribute]) -> Result<(), DeriveError> { + match filter(attrs.iter()).next() { + Some(attr) => Err(DeriveError::InvalidAttributePosition { + attribute_span: attr.span(), + }), + None => Ok(()), + } +} + +/// Deny any attributes that uses the helper attribute on any field. +pub fn deny_fields(fields: &Fields) -> Result<(), DeriveError> { + match fields { + Fields::Named(fields) => { + for field in fields.named.iter() { + deny(&field.attrs)?; + } + } + Fields::Unnamed(fields) => { + for field in fields.unnamed.iter() { + deny(&field.attrs)?; + } + } + Fields::Unit => (), + } + + Ok(()) +} diff --git a/crates/nu-derive-value/src/error.rs b/crates/nu-derive-value/src/error.rs new file mode 100644 index 0000000000..b7fd3e2f91 --- /dev/null +++ b/crates/nu-derive-value/src/error.rs @@ -0,0 +1,82 @@ +use std::{any, fmt::Debug, marker::PhantomData}; + +use proc_macro2::Span; +use proc_macro_error::{Diagnostic, Level}; + +#[derive(Debug)] +pub enum DeriveError { + /// Marker variant, makes the `M` generic parameter valid. + _Marker(PhantomData), + + /// Parsing errors thrown by `syn`. + Syn(syn::parse::Error), + + /// `syn::DeriveInput` was a union, currently not supported + UnsupportedUnions, + + /// Only plain enums are supported right now. + UnsupportedEnums { fields_span: Span }, + + /// Found a `#[nu_value(x)]` attribute where `x` is unexpected. + UnexpectedAttribute { meta_span: Span }, + + /// Found a `#[nu_value(x)]` attribute at a invalid position. + InvalidAttributePosition { attribute_span: Span }, + + /// Found a valid `#[nu_value(x)]` attribute but the passed values is invalid. + InvalidAttributeValue { + value_span: Span, + value: Box, + }, +} + +impl From> for Diagnostic { + fn from(value: DeriveError) -> Self { + let derive_name = any::type_name::().split("::").last().expect("not empty"); + match value { + DeriveError::_Marker(_) => panic!("used marker variant"), + + DeriveError::Syn(e) => Diagnostic::spanned(e.span(), Level::Error, e.to_string()), + + DeriveError::UnsupportedUnions => Diagnostic::new( + Level::Error, + format!("`{derive_name}` cannot be derived from unions"), + ) + .help("consider refactoring to a struct".to_string()) + .note("if you really need a union, consider opening an issue on Github".to_string()), + + DeriveError::UnsupportedEnums { fields_span } => Diagnostic::spanned( + fields_span, + Level::Error, + format!("`{derive_name}` can only be derived from plain enums"), + ) + .help( + "consider refactoring your data type to a struct with a plain enum as a field" + .to_string(), + ) + .note("more complex enums could be implemented in the future".to_string()), + + DeriveError::InvalidAttributePosition { attribute_span } => Diagnostic::spanned( + attribute_span, + Level::Error, + "invalid attribute position".to_string(), + ) + .help(format!( + "check documentation for `{derive_name}` for valid placements" + )), + + DeriveError::UnexpectedAttribute { meta_span } => { + Diagnostic::spanned(meta_span, Level::Error, "unknown attribute".to_string()).help( + format!("check documentation for `{derive_name}` for valid attributes"), + ) + } + + DeriveError::InvalidAttributeValue { value_span, value } => { + Diagnostic::spanned(value_span, Level::Error, format!("invalid value {value:?}")) + .help(format!( + "check documentation for `{derive_name}` for valid attribute values" + )) + } + } + } +} diff --git a/crates/nu-derive-value/src/from.rs b/crates/nu-derive-value/src/from.rs new file mode 100644 index 0000000000..033026c149 --- /dev/null +++ b/crates/nu-derive-value/src/from.rs @@ -0,0 +1,539 @@ +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, +}; + +use crate::attributes::{self, ContainerAttributes}; + +#[derive(Debug)] +pub struct FromValue; +type DeriveError = super::error::DeriveError; + +/// Inner implementation of the `#[derive(FromValue)]` macro for structs and enums. +/// +/// Uses `proc_macro2::TokenStream` for better testing support, unlike `proc_macro::TokenStream`. +/// +/// This function directs the `FromValue` trait derivation to the correct implementation based on +/// the input type: +/// - 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 { + let input: DeriveInput = syn::parse2(input).map_err(DeriveError::Syn)?; + match input.data { + Data::Struct(data_struct) => Ok(derive_struct_from_value( + input.ident, + data_struct, + input.generics, + input.attrs, + )?), + Data::Enum(data_enum) => Ok(derive_enum_from_value( + input.ident, + data_enum, + input.generics, + input.attrs, + )?), + Data::Union(_) => Err(DeriveError::UnsupportedUnions), + } +} + +/// Implements the `#[derive(FromValue)]` macro for structs. +/// +/// This function ensures that the helper attribute is not used anywhere, as it is not supported for +/// 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 +/// `FromValue::expected_type` is handled by [`struct_expected_type`]. +fn derive_struct_from_value( + ident: Ident, + data: DataStruct, + generics: Generics, + attrs: Vec, +) -> Result { + attributes::deny(&attrs)?; + 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); + Ok(quote! { + #[automatically_derived] + impl #impl_generics nu_protocol::FromValue for #ident #ty_generics #where_clause { + #from_value_impl + #expected_type_impl + } + }) +} + +/// Implements `FromValue::from_value` for structs. +/// +/// This function constructs the `from_value` function for structs. +/// The implementation is straightforward as most of the heavy lifting is handled by +/// `parse_value_via_fields`, and this function only needs to construct the signature around it. +/// +/// For structs with named fields, this constructs a large return type where each field +/// contains the implementation for that specific field. +/// In structs with unnamed fields, a [`VecDeque`](std::collections::VecDeque) is used to load each +/// field one after another, and the result is used to construct the tuple. +/// For unit structs, this only checks if the input value is `Value::Nothing`. +/// +/// # 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, +/// } +/// +/// impl nu_protocol::FromValue for Pet { +/// fn from_value( +/// v: nu_protocol::Value +/// ) -> std::result::Result { +/// let span = v.span(); +/// let mut record = v.into_record()?; +/// std::result::Result::Ok(Pet { +/// name: ::from_value( +/// record +/// .remove("name") +/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn { +/// col_name: std::string::ToString::to_string("name"), +/// span: std::option::Option::None, +/// src_span: span +/// })?, +/// )?, +/// age: ::from_value( +/// record +/// .remove("age") +/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn { +/// col_name: std::string::ToString::to_string("age"), +/// span: std::option::Option::None, +/// src_span: span +/// })?, +/// )?, +/// favorite_toy: as nu_protocol::FromValue>::from_value( +/// record +/// .remove("favorite_toy") +/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn { +/// col_name: std::string::ToString::to_string("favorite_toy"), +/// span: std::option::Option::None, +/// src_span: span +/// })?, +/// )?, +/// }) +/// } +/// } +/// ``` +/// +/// Struct with unnamed fields: +/// ```rust +/// #[derive(IntoValue)] +/// struct Color(u8, u8, u8); +/// +/// impl nu_protocol::FromValue for Color { +/// fn from_value( +/// v: nu_protocol::Value +/// ) -> std::result::Result { +/// 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( +/// { +/// ::from_value( +/// deque +/// .pop_front() +/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn { +/// col_name: std::string::ToString::to_string(&0), +/// span: std::option::Option::None, +/// src_span: span +/// })?, +/// )? +/// }, +/// { +/// ::from_value( +/// deque +/// .pop_front() +/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn { +/// col_name: std::string::ToString::to_string(&1), +/// span: std::option::Option::None, +/// src_span: span +/// })?, +/// )? +/// }, +/// { +/// ::from_value( +/// deque +/// .pop_front() +/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn { +/// col_name: std::string::ToString::to_string(&2), +/// span: std::option::Option::None, +/// src_span: span +/// })?, +/// )? +/// } +/// )) +/// } +/// } +/// ``` +/// +/// Unit struct: +/// ```rust +/// #[derive(IntoValue)] +/// struct Unicorn; +/// +/// impl nu_protocol::FromValue for Unicorn { +/// fn from_value( +/// v: nu_protocol::Value +/// ) -> std::result::Result { +/// match v { +/// nu_protocol::Value::Nothing {..} => Ok(Self), +/// v => std::result::Result::Err(nu_protocol::ShellError::CantConvert { +/// to_type: std::string::ToString::to_string(&::expected_type()), +/// from_type: std::string::ToString::to_string(&v.get_type()), +/// span: v.span(), +/// help: std::option::Option::None +/// }) +/// } +/// } +/// } +/// ``` +fn struct_from_value(data: &DataStruct) -> TokenStream2 { + let body = parse_value_via_fields(&data.fields, quote!(Self)); + quote! { + fn from_value( + v: nu_protocol::Value + ) -> std::result::Result { + #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`. +/// +/// # 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, +/// } +/// +/// impl nu_protocol::FromValue for Pet { +/// fn expected_type() -> nu_protocol::Type { +/// nu_protocol::Type::Record( +/// std::vec![ +/// ( +/// std::string::ToString::to_string("name"), +/// ::expected_type(), +/// ), +/// ( +/// std::string::ToString::to_string("age"), +/// ::expected_type(), +/// ), +/// ( +/// std::string::ToString::to_string("favorite_toy"), +/// as nu_protocol::FromValue>::expected_type(), +/// ) +/// ].into_boxed_slice() +/// ) +/// } +/// } +/// ``` +/// +/// Struct with unnamed fields: +/// ```rust +/// #[derive(IntoValue)] +/// struct Color(u8, u8, u8); +/// +/// impl nu_protocol::FromValue for Color { +/// fn expected_type() -> nu_protocol::Type { +/// nu_protocol::Type::Custom( +/// std::format!( +/// "[{}, {}, {}]", +/// ::expected_type(), +/// ::expected_type(), +/// ::expected_type() +/// ) +/// .into_boxed_str() +/// ) +/// } +/// } +/// ``` +/// +/// Unit struct: +/// ```rust +/// #[derive(IntoValue)] +/// struct Unicorn; +/// +/// impl nu_protocol::FromValue for Color { +/// fn expected_type() -> nu_protocol::Type { +/// nu_protocol::Type::Nothing +/// } +/// } +/// ``` +fn struct_expected_type(fields: &Fields) -> TokenStream2 { + let ty = match fields { + 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(); + let ty = &field.ty; + 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() + )) + } + Fields::Unnamed(fields) => { + let mut iter = fields.unnamed.iter(); + let fields = fields.unnamed.iter().map(|field| { + let ty = &field.ty; + quote!(<#ty as nu_protocol::FromValue>::expected_type()) + }); + let mut template = String::new(); + template.push('['); + if iter.next().is_some() { + template.push_str("{}") + } + iter.for_each(|_| template.push_str(", {}")); + template.push(']'); + quote! { + nu_protocol::Type::Custom( + std::format!( + #template, + #(#fields),* + ) + .into_boxed_str() + ) + } + } + Fields::Unit => quote!(nu_protocol::Type::Nothing), + }; + + quote! { + fn expected_type() -> nu_protocol::Type { + #ty + } + } +} + +/// Implements the `#[derive(FromValue)]` macro 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 +/// 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`. +fn derive_enum_from_value( + ident: Ident, + data: DataEnum, + generics: Generics, + attrs: Vec, +) -> Result { + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let from_value_impl = enum_from_value(&data, &attrs)?; + Ok(quote! { + #[automatically_derived] + impl #impl_generics nu_protocol::FromValue for #ident #ty_generics #where_clause { + #from_value_impl + } + }) +} + +/// Implements `FromValue::from_value` for enums. +/// +/// This function constructs the `from_value` implementation for enums. +/// It only accepts enums with unit variants, as it is currently unclear how other types of enums +/// 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, [`convert_case::Case::Snake`] is expected. +/// +/// If no matching variant is found, `ShellError::CantConvert` is returned. +/// +/// 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 { +/// let span = v.span(); +/// let ty = v.get_type(); +/// +/// let s = v.into_string()?; +/// match s.as_str() { +/// "sunny" => std::result::Ok(Self::Sunny), +/// "cloudy" => std::result::Ok(Self::Cloudy), +/// "raining" => std::result::Ok(Self::Raining), +/// _ => std::result::Result::Err(nu_protocol::ShellError::CantConvert { +/// to_type: std::string::ToString::to_string( +/// &::expected_type() +/// ), +/// from_type: std::string::ToString::to_string(&ty), +/// span: span,help: std::option::Option::None, +/// }), +/// } +/// } +/// } +/// ``` +fn enum_from_value(data: &DataEnum, attrs: &[Attribute]) -> Result { + let container_attrs = ContainerAttributes::parse_attrs(attrs.iter())?; + let arms: Vec = data + .variants + .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 { + Fields::Named(fields) => Err(DeriveError::UnsupportedEnums { + fields_span: fields.span(), + }), + Fields::Unnamed(fields) => Err(DeriveError::UnsupportedEnums { + fields_span: fields.span(), + }), + Fields::Unit => Ok(quote!(#ident_s => std::result::Result::Ok(Self::#ident))), + } + }) + .collect::>()?; + + Ok(quote! { + fn from_value( + v: nu_protocol::Value + ) -> std::result::Result { + let span = v.span(); + let ty = v.get_type(); + + let s = v.into_string()?; + match s.as_str() { + #(#arms,)* + _ => std::result::Result::Err(nu_protocol::ShellError::CantConvert { + to_type: std::string::ToString::to_string( + &::expected_type() + ), + from_type: std::string::ToString::to_string(&ty), + span: span, + help: std::option::Option::None, + }), + } + } + }) +} + +/// Parses a `Value` into self. +/// +/// This function handles the actual parsing of a `Value` into self. +/// It takes two parameters: `fields` and `self_ident`. +/// 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`. +/// +/// 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 `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. +/// +/// 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 +/// generation. +fn parse_value_via_fields(fields: &Fields, self_ident: impl ToTokens) -> TokenStream2 { + match fields { + Fields::Named(fields) => { + let fields = fields.named.iter().map(|field| { + // TODO: handle missing fields for Options as None + let ident = field.ident.as_ref().expect("named has idents"); + let ident_s = ident.to_string(); + let ty = &field.ty; + quote! { + #ident: <#ty as nu_protocol::FromValue>::from_value( + record + .remove(#ident_s) + .ok_or_else(|| nu_protocol::ShellError::CantFindColumn { + col_name: std::string::ToString::to_string(#ident_s), + span: std::option::Option::None, + src_span: span + })?, + )? + } + }); + quote! { + let span = v.span(); + let mut record = v.into_record()?; + std::result::Result::Ok(#self_ident {#(#fields),*}) + } + } + Fields::Unnamed(fields) => { + let fields = fields.unnamed.iter().enumerate().map(|(i, field)| { + let ty = &field.ty; + quote! {{ + <#ty as nu_protocol::FromValue>::from_value( + deque + .pop_front() + .ok_or_else(|| nu_protocol::ShellError::CantFindColumn { + col_name: std::string::ToString::to_string(&#i), + span: std::option::Option::None, + src_span: span + })?, + )? + }} + }); + 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! { + match v { + nu_protocol::Value::Nothing {..} => Ok(#self_ident), + v => std::result::Result::Err(nu_protocol::ShellError::CantConvert { + to_type: std::string::ToString::to_string(&::expected_type()), + from_type: std::string::ToString::to_string(&v.get_type()), + span: v.span(), + help: std::option::Option::None + }) + } + }, + } +} diff --git a/crates/nu-derive-value/src/into.rs b/crates/nu-derive-value/src/into.rs new file mode 100644 index 0000000000..a7cec06378 --- /dev/null +++ b/crates/nu-derive-value/src/into.rs @@ -0,0 +1,266 @@ +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; + +/// 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 { + 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. +/// +/// Note: The helper attribute `#[nu_value(...)]` is currently not allowed on structs. +/// +/// # 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, +/// } +/// +/// 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, +) -> Result { + attributes::deny(&attrs)?; + 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, +) -> Result { + let container_attrs = ContainerAttributes::parse_attrs(attrs.iter())?; + let arms: Vec = 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::>()?; + + 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, +) -> TokenStream2 { + match fields { + Fields::Named(fields) => { + let items: Vec = 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)), + } +} diff --git a/crates/nu-derive-value/src/lib.rs b/crates/nu-derive-value/src/lib.rs new file mode 100644 index 0000000000..269566fc88 --- /dev/null +++ b/crates/nu-derive-value/src/lib.rs @@ -0,0 +1,69 @@ +//! Macro implementations of `#[derive(FromValue, IntoValue)]`. +//! +//! As this crate is a [`proc_macro`] crate, it is only allowed to export +//! [procedural macros](https://doc.rust-lang.org/reference/procedural-macros.html). +//! Therefore, it only exports [`IntoValue`] and [`FromValue`]. +//! +//! To get documentation for other functions and types used in this crate, run +//! `cargo doc -p nu-derive-value --document-private-items`. +//! +//! This crate uses a lot of +//! [`proc_macro2::TokenStream`](https://docs.rs/proc-macro2/1.0.24/proc_macro2/struct.TokenStream.html) +//! as `TokenStream2` to allow testing the behavior of the macros directly, including the output +//! token stream or if the macro errors as expected. +//! The tests for functionality can be found in `nu_protocol::value::test_derive`. +//! +//! This documentation is often less reference-heavy than typical Rust documentation. +//! This is because this crate is a dependency for `nu_protocol`, and linking to it would create a +//! cyclic dependency. +//! Also all examples in the documentation aren't tested as this crate cannot be compiled as a +//! normal library very easily. +//! This might change in the future if cargo allows building a proc-macro crate differently for +//! `cfg(doctest)` as they are already doing for `cfg(test)`. +//! +//! The generated code from the derive macros tries to be as +//! [hygienic](https://doc.rust-lang.org/reference/macros-by-example.html#hygiene) as possible. +//! This ensures that the macro can be called anywhere without requiring specific imports. +//! This results in obtuse code, which isn't recommended for manual, handwritten Rust +//! but ensures that no other code may influence this generated code or vice versa. + +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use proc_macro_error::{proc_macro_error, Diagnostic}; + +mod attributes; +mod error; +mod from; +mod into; +#[cfg(test)] +mod tests; + +const HELPER_ATTRIBUTE: &str = "nu_value"; + +/// Derive macro generating an impl of the trait `IntoValue`. +/// +/// For further information, see the docs on the trait itself. +#[proc_macro_derive(IntoValue, attributes(nu_value))] +#[proc_macro_error] +pub fn derive_into_value(input: TokenStream) -> TokenStream { + let input = TokenStream2::from(input); + let output = match into::derive_into_value(input) { + Ok(output) => output, + Err(e) => Diagnostic::from(e).abort(), + }; + TokenStream::from(output) +} + +/// Derive macro generating an impl of the trait `FromValue`. +/// +/// For further information, see the docs on the trait itself. +#[proc_macro_derive(FromValue, attributes(nu_value))] +#[proc_macro_error] +pub fn derive_from_value(input: TokenStream) -> TokenStream { + let input = TokenStream2::from(input); + let output = match from::derive_from_value(input) { + Ok(output) => output, + Err(e) => Diagnostic::from(e).abort(), + }; + TokenStream::from(output) +} diff --git a/crates/nu-derive-value/src/tests.rs b/crates/nu-derive-value/src/tests.rs new file mode 100644 index 0000000000..b94d12a732 --- /dev/null +++ b/crates/nu-derive-value/src/tests.rs @@ -0,0 +1,157 @@ +// These tests only check that the derive macros throw the relevant errors. +// Functionality of the derived types is tested in nu_protocol::value::test_derive. + +use crate::error::DeriveError; +use crate::from::derive_from_value; +use crate::into::derive_into_value; +use quote::quote; + +#[test] +fn unsupported_unions() { + let input = quote! { + #[nu_value] + union SomeUnion { + f1: u32, + f2: f32, + } + }; + + let from_res = derive_from_value(input.clone()); + assert!( + matches!(from_res, Err(DeriveError::UnsupportedUnions)), + "expected `DeriveError::UnsupportedUnions`, got {:?}", + from_res + ); + + let into_res = derive_into_value(input); + assert!( + matches!(into_res, Err(DeriveError::UnsupportedUnions)), + "expected `DeriveError::UnsupportedUnions`, got {:?}", + into_res + ); +} + +#[test] +fn unsupported_enums() { + let input = quote! { + #[nu_value(rename_all = "SCREAMING_SNAKE_CASE")] + enum ComplexEnum { + Unit, + Unnamed(u32, f32), + Named { + u: u32, + f: f32, + } + } + }; + + let from_res = derive_from_value(input.clone()); + assert!( + matches!(from_res, Err(DeriveError::UnsupportedEnums { .. })), + "expected `DeriveError::UnsupportedEnums`, got {:?}", + from_res + ); + + let into_res = derive_into_value(input); + assert!( + matches!(into_res, Err(DeriveError::UnsupportedEnums { .. })), + "expected `DeriveError::UnsupportedEnums`, got {:?}", + into_res + ); +} + +#[test] +fn unexpected_attribute() { + let input = quote! { + #[nu_value(what)] + enum SimpleEnum { + A, + B, + } + }; + + let from_res = derive_from_value(input.clone()); + assert!( + matches!(from_res, Err(DeriveError::UnexpectedAttribute { .. })), + "expected `DeriveError::UnexpectedAttribute`, got {:?}", + from_res + ); + + let into_res = derive_into_value(input); + assert!( + matches!(into_res, Err(DeriveError::UnexpectedAttribute { .. })), + "expected `DeriveError::UnexpectedAttribute`, got {:?}", + into_res + ); +} + +#[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! { + struct SomeStruct { + #[nu_value] + field: () + } + }; + + 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 invalid_attribute_value() { + let input = quote! { + #[nu_value(rename_all = "CrazY-CasE")] + enum SimpleEnum { + A, + B + } + }; + + let from_res = derive_from_value(input.clone()); + assert!( + matches!(from_res, Err(DeriveError::InvalidAttributeValue { .. })), + "expected `DeriveError::InvalidAttributeValue`, got {:?}", + from_res + ); + + let into_res = derive_into_value(input); + assert!( + matches!(into_res, Err(DeriveError::InvalidAttributeValue { .. })), + "expected `DeriveError::InvalidAttributeValue`, got {:?}", + into_res + ); +} diff --git a/crates/nu-protocol/Cargo.toml b/crates/nu-protocol/Cargo.toml index de73ba1a2c..849a968eb8 100644 --- a/crates/nu-protocol/Cargo.toml +++ b/crates/nu-protocol/Cargo.toml @@ -16,11 +16,13 @@ bench = false nu-utils = { path = "../nu-utils", version = "0.94.3" } nu-path = { path = "../nu-path", version = "0.94.3" } nu-system = { path = "../nu-system", version = "0.94.3" } +nu-derive-value = { path = "../nu-derive-value", version = "0.94.3" } brotli = { workspace = true, optional = true } byte-unit = { version = "5.1", features = [ "serde" ] } chrono = { workspace = true, features = [ "serde", "std", "unstable-locales" ], default-features = false } chrono-humanize = { workspace = true } +convert_case = { workspace = true } fancy-regex = { workspace = true } indexmap = { workspace = true } lru = { workspace = true } diff --git a/crates/nu-protocol/src/errors/shell_error.rs b/crates/nu-protocol/src/errors/shell_error.rs index e201915fcf..30752d5c9e 100644 --- a/crates/nu-protocol/src/errors/shell_error.rs +++ b/crates/nu-protocol/src/errors/shell_error.rs @@ -557,12 +557,12 @@ pub enum ShellError { /// ## Resolution /// /// Check the spelling of your column name. Did you forget to rename a column somewhere? - #[error("Cannot find column")] + #[error("Cannot find column '{col_name}'")] #[diagnostic(code(nu::shell::column_not_found))] CantFindColumn { col_name: String, #[label = "cannot find column '{col_name}'"] - span: Span, + span: Option, #[label = "value originates here"] src_span: Span, }, diff --git a/crates/nu-protocol/src/lib.rs b/crates/nu-protocol/src/lib.rs index d09186cf46..78e7d40680 100644 --- a/crates/nu-protocol/src/lib.rs +++ b/crates/nu-protocol/src/lib.rs @@ -39,3 +39,5 @@ pub use span::*; pub use syntax_shape::*; pub use ty::*; pub use value::*; + +pub use nu_derive_value::*; diff --git a/crates/nu-protocol/src/value/from_value.rs b/crates/nu-protocol/src/value/from_value.rs index 97fb95d482..1033eb03b2 100644 --- a/crates/nu-protocol/src/value/from_value.rs +++ b/crates/nu-protocol/src/value/from_value.rs @@ -1,37 +1,188 @@ use crate::{ ast::{CellPath, PathMember}, engine::Closure, - NuGlob, Range, Record, ShellError, Spanned, Value, + NuGlob, Range, Record, ShellError, Spanned, Type, Value, }; use chrono::{DateTime, FixedOffset}; -use std::path::PathBuf; +use std::{ + any, + cmp::Ordering, + collections::{HashMap, VecDeque}, + path::PathBuf, + str::FromStr, +}; +/// A trait for loading a value from a [`Value`]. +/// +/// # Derivable +/// This trait can be used with `#[derive]`. +/// When derived on structs with named fields, it expects a [`Value::Record`] where each field of +/// the struct maps to a corresponding field in the record. +/// For structs with unnamed fields, it expects a [`Value::List`], and the fields are populated in +/// the order they appear in the list. +/// Unit structs expect a [`Value::Nothing`], as they contain no data. +/// Attempting to convert from a non-matching `Value` type will result in an error. +/// +/// Only enums with no fields may derive this trait. +/// The expected value representation will be the name of the variant as a [`Value::String`]. +/// By default, variant names will be expected in ["snake_case"](convert_case::Case::Snake). +/// You can customize the case conversion using `#[nu_value(rename_all = "kebab-case")]` on the enum. +/// All deterministic and useful case conversions provided by [`convert_case::Case`] are supported +/// by specifying the case name followed by "case". +/// Also all values for +/// [`#[serde(rename_all = "...")]`](https://serde.rs/container-attrs.html#rename_all) are valid +/// here. +/// +/// ``` +/// # use nu_protocol::{FromValue, Value, ShellError}; +/// #[derive(FromValue, Debug, PartialEq)] +/// #[nu_value(rename_all = "COBOL-CASE")] +/// enum Bird { +/// MountainEagle, +/// ForestOwl, +/// RiverDuck, +/// } +/// +/// assert_eq!( +/// Bird::from_value(Value::test_string("RIVER-DUCK")).unwrap(), +/// Bird::RiverDuck +/// ); +/// ``` pub trait FromValue: Sized { + // TODO: instead of ShellError, maybe we could have a FromValueError that implements Into + /// Loads a value from a [`Value`]. + /// + /// This method retrieves a value similarly to how strings are parsed using [`FromStr`]. + /// The operation might fail if the `Value` contains unexpected types or structures. fn from_value(v: Value) -> Result; -} -impl FromValue for Value { - fn from_value(v: Value) -> Result { - Ok(v) + /// Expected `Value` type. + /// + /// This is used to print out errors of what type of value is expected for conversion. + /// Even if not used in [`from_value`](FromValue::from_value) this should still be implemented + /// so that other implementations like `Option` or `Vec` can make use of it. + /// It is advised to call this method in `from_value` to ensure that expected type in the error + /// is consistent. + /// + /// Unlike the default implementation, derived implementations explicitly reveal the concrete + /// type, such as [`Type::Record`] or [`Type::List`], instead of an opaque type. + fn expected_type() -> Type { + Type::Custom( + any::type_name::() + .split(':') + .last() + .expect("str::split returns an iterator with at least one element") + .to_string() + .into_boxed_str(), + ) } } -impl FromValue for Spanned { +// Primitive Types + +impl FromValue for [T; N] +where + T: FromValue, +{ fn from_value(v: Value) -> Result { let span = v.span(); - match v { - Value::Int { val, .. } => Ok(Spanned { item: val, span }), - Value::Filesize { val, .. } => Ok(Spanned { item: val, span }), - Value::Duration { val, .. } => Ok(Spanned { item: val, span }), + let v_ty = v.get_type(); + let vec = Vec::::from_value(v)?; + vec.try_into() + .map_err(|err_vec: Vec| ShellError::CantConvert { + to_type: Self::expected_type().to_string(), + from_type: v_ty.to_string(), + span, + help: Some(match err_vec.len().cmp(&N) { + Ordering::Less => format!( + "input list too short ({}), expected length of {N}, add missing values", + err_vec.len() + ), + Ordering::Equal => { + unreachable!("conversion would have worked if the length would be the same") + } + Ordering::Greater => format!( + "input list too long ({}), expected length of {N}, remove trailing values", + err_vec.len() + ), + }), + }) + } + fn expected_type() -> Type { + Type::Custom(format!("list<{};{N}>", T::expected_type()).into_boxed_str()) + } +} + +impl FromValue for bool { + fn from_value(v: Value) -> Result { + match v { + Value::Bool { val, .. } => Ok(val), v => Err(ShellError::CantConvert { - to_type: "int".into(), + to_type: Self::expected_type().to_string(), from_type: v.get_type().to_string(), span: v.span(), help: None, }), } } + + fn expected_type() -> Type { + Type::Bool + } +} + +impl FromValue for char { + fn from_value(v: Value) -> Result { + let span = v.span(); + let v_ty = v.get_type(); + match v { + Value::String { ref val, .. } => match char::from_str(val) { + Ok(c) => Ok(c), + Err(_) => Err(ShellError::CantConvert { + to_type: Self::expected_type().to_string(), + from_type: v_ty.to_string(), + span, + help: Some("make the string only one char long".to_string()), + }), + }, + _ => Err(ShellError::CantConvert { + to_type: Self::expected_type().to_string(), + from_type: v_ty.to_string(), + span, + help: None, + }), + } + } + + fn expected_type() -> Type { + Type::String + } +} + +impl FromValue for f32 { + fn from_value(v: Value) -> Result { + f64::from_value(v).map(|float| float as f32) + } +} + +impl FromValue for f64 { + fn from_value(v: Value) -> Result { + match v { + Value::Float { val, .. } => Ok(val), + Value::Int { val, .. } => Ok(val as f64), + v => Err(ShellError::CantConvert { + to_type: Self::expected_type().to_string(), + from_type: v.get_type().to_string(), + span: v.span(), + help: None, + }), + } + } + + fn expected_type() -> Type { + Type::Float + } } impl FromValue for i64 { @@ -42,128 +193,207 @@ impl FromValue for i64 { Value::Duration { val, .. } => Ok(val), v => Err(ShellError::CantConvert { - to_type: "int".into(), + to_type: Self::expected_type().to_string(), from_type: v.get_type().to_string(), span: v.span(), help: None, }), } } + + fn expected_type() -> Type { + Type::Int + } } -impl FromValue for Spanned { - fn from_value(v: Value) -> Result { - let span = v.span(); - match v { - Value::Int { val, .. } => Ok(Spanned { - item: val as f64, - span, - }), - Value::Float { val, .. } => Ok(Spanned { item: val, span }), +macro_rules! impl_from_value_for_int { + ($type:ty) => { + impl FromValue for $type { + fn from_value(v: Value) -> Result { + let span = v.span(); + let int = i64::from_value(v)?; + const MIN: i64 = <$type>::MIN as i64; + const MAX: i64 = <$type>::MAX as i64; + #[allow(overlapping_range_endpoints)] // calculating MIN-1 is not possible for i64::MIN + #[allow(unreachable_patterns)] // isize might max out i64 number range + <$type>::try_from(int).map_err(|_| match int { + MIN..=MAX => unreachable!( + "int should be within the valid range for {}", + stringify!($type) + ), + i64::MIN..=MIN => ShellError::GenericError { + error: "Integer too small".to_string(), + msg: format!("{int} is smaller than {}", <$type>::MIN), + span: Some(span), + help: None, + inner: vec![], + }, + MAX..=i64::MAX => ShellError::GenericError { + error: "Integer too large".to_string(), + msg: format!("{int} is larger than {}", <$type>::MAX), + span: Some(span), + help: None, + inner: vec![], + }, + }) + } + fn expected_type() -> Type { + i64::expected_type() + } + } + }; +} + +impl_from_value_for_int!(i8); +impl_from_value_for_int!(i16); +impl_from_value_for_int!(i32); +impl_from_value_for_int!(isize); + +macro_rules! impl_from_value_for_uint { + ($type:ty, $max:expr) => { + impl FromValue for $type { + fn from_value(v: Value) -> Result { + let span = v.span(); + const MAX: i64 = $max; + match v { + Value::Int { val, .. } + | Value::Filesize { val, .. } + | Value::Duration { val, .. } => { + match val { + i64::MIN..=-1 => Err(ShellError::NeedsPositiveValue { span }), + 0..=MAX => Ok(val as $type), + #[allow(unreachable_patterns)] // u64 will max out the i64 number range + n => Err(ShellError::GenericError { + error: "Integer too large".to_string(), + msg: format!("{n} is larger than {MAX}"), + span: Some(span), + help: None, + inner: vec![], + }), + } + } + v => Err(ShellError::CantConvert { + to_type: Self::expected_type().to_string(), + from_type: v.get_type().to_string(), + span: v.span(), + help: None, + }), + } + } + + fn expected_type() -> Type { + Type::Custom("non-negative int".to_string().into_boxed_str()) + } + } + }; +} + +// Sadly we cannot implement FromValue for u8 without losing the impl of Vec, +// Rust would find two possible implementations then, Vec and Vec, +// and wouldn't compile. +// The blanket implementation for Vec is probably more useful than +// implementing FromValue for u8. + +impl_from_value_for_uint!(u16, u16::MAX as i64); +impl_from_value_for_uint!(u32, u32::MAX as i64); +impl_from_value_for_uint!(u64, i64::MAX); // u64::Max would be -1 as i64 +#[cfg(target_pointer_width = "64")] +impl_from_value_for_uint!(usize, i64::MAX); +#[cfg(target_pointer_width = "32")] +impl_from_value_for_uint!(usize, usize::MAX); + +impl FromValue for () { + fn from_value(v: Value) -> Result { + match v { + Value::Nothing { .. } => Ok(()), v => Err(ShellError::CantConvert { - to_type: "float".into(), + to_type: Self::expected_type().to_string(), from_type: v.get_type().to_string(), span: v.span(), help: None, }), } } + + fn expected_type() -> Type { + Type::Nothing + } } -impl FromValue for f64 { +macro_rules! tuple_from_value { + ($template:literal, $($t:ident:$n:tt),+) => { + impl<$($t),+> FromValue for ($($t,)+) where $($t: FromValue,)+ { + fn from_value(v: Value) -> Result { + let span = v.span(); + match v { + Value::List { vals, .. } => { + let mut deque = VecDeque::from(vals); + + Ok(($( + { + let v = deque.pop_front().ok_or_else(|| ShellError::CantFindColumn { + col_name: $n.to_string(), + span: None, + src_span: span + })?; + $t::from_value(v)? + }, + )*)) + }, + v => Err(ShellError::CantConvert { + to_type: Self::expected_type().to_string(), + from_type: v.get_type().to_string(), + span: v.span(), + help: None, + }), + } + } + + fn expected_type() -> Type { + Type::Custom( + format!( + $template, + $($t::expected_type()),* + ) + .into_boxed_str(), + ) + } + } + }; +} + +// Tuples in std are implemented for up to 12 elements, so we do it here too. +tuple_from_value!("[{}]", T0:0); +tuple_from_value!("[{}, {}]", T0:0, T1:1); +tuple_from_value!("[{}, {}, {}]", T0:0, T1:1, T2:2); +tuple_from_value!("[{}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3); +tuple_from_value!("[{}, {}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3, T4:4); +tuple_from_value!("[{}, {}, {}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3, T4:4, T5:5); +tuple_from_value!("[{}, {}, {}, {}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6); +tuple_from_value!("[{}, {}, {}, {}, {}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7); +tuple_from_value!("[{}, {}, {}, {}, {}, {}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8); +tuple_from_value!("[{}, {}, {}, {}, {}, {}, {}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8, T9:9); +tuple_from_value!("[{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8, T9:9, T10:10); +tuple_from_value!("[{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8, T9:9, T10:10, T11:11); + +// Other std Types + +impl FromValue for PathBuf { fn from_value(v: Value) -> Result { match v { - Value::Float { val, .. } => Ok(val), - Value::Int { val, .. } => Ok(val as f64), + Value::String { val, .. } => Ok(val.into()), v => Err(ShellError::CantConvert { - to_type: "float".into(), + to_type: Self::expected_type().to_string(), from_type: v.get_type().to_string(), span: v.span(), help: None, }), } } -} -impl FromValue for Spanned { - fn from_value(v: Value) -> Result { - let span = v.span(); - match v { - Value::Int { val, .. } => { - if val.is_negative() { - Err(ShellError::NeedsPositiveValue { span }) - } else { - Ok(Spanned { - item: val as usize, - span, - }) - } - } - Value::Filesize { val, .. } => { - if val.is_negative() { - Err(ShellError::NeedsPositiveValue { span }) - } else { - Ok(Spanned { - item: val as usize, - span, - }) - } - } - Value::Duration { val, .. } => { - if val.is_negative() { - Err(ShellError::NeedsPositiveValue { span }) - } else { - Ok(Spanned { - item: val as usize, - span, - }) - } - } - - v => Err(ShellError::CantConvert { - to_type: "non-negative int".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }), - } - } -} - -impl FromValue for usize { - fn from_value(v: Value) -> Result { - let span = v.span(); - match v { - Value::Int { val, .. } => { - if val.is_negative() { - Err(ShellError::NeedsPositiveValue { span }) - } else { - Ok(val as usize) - } - } - Value::Filesize { val, .. } => { - if val.is_negative() { - Err(ShellError::NeedsPositiveValue { span }) - } else { - Ok(val as usize) - } - } - Value::Duration { val, .. } => { - if val.is_negative() { - Err(ShellError::NeedsPositiveValue { span }) - } else { - Ok(val as usize) - } - } - - v => Err(ShellError::CantConvert { - to_type: "non-negative int".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }), - } + fn expected_type() -> Type { + Type::String } } @@ -174,176 +404,111 @@ impl FromValue for String { Value::CellPath { val, .. } => Ok(val.to_string()), Value::String { val, .. } => Ok(val), v => Err(ShellError::CantConvert { - to_type: "string".into(), + to_type: Self::expected_type().to_string(), from_type: v.get_type().to_string(), span: v.span(), help: None, }), } } -} -impl FromValue for Spanned { - fn from_value(v: Value) -> Result { - let span = v.span(); - Ok(Spanned { - item: match v { - Value::CellPath { val, .. } => val.to_string(), - Value::String { val, .. } => val, - v => { - return Err(ShellError::CantConvert { - to_type: "string".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }) - } - }, - span, - }) + fn expected_type() -> Type { + Type::String } } -impl FromValue for NuGlob { +// This impl is different from Vec as it reads from Value::Binary and +// Value::String instead of Value::List. +// This also denies implementing FromValue for u8. +impl FromValue for Vec { fn from_value(v: Value) -> Result { - // FIXME: we may want to fail a little nicer here match v { - Value::CellPath { val, .. } => Ok(NuGlob::Expand(val.to_string())), - Value::String { val, .. } => Ok(NuGlob::DoNotExpand(val)), - Value::Glob { - val, - no_expand: quoted, - .. - } => { - if quoted { - Ok(NuGlob::DoNotExpand(val)) - } else { - Ok(NuGlob::Expand(val)) - } - } + Value::Binary { val, .. } => Ok(val), + Value::String { val, .. } => Ok(val.into_bytes()), v => Err(ShellError::CantConvert { - to_type: "string".into(), + to_type: Self::expected_type().to_string(), from_type: v.get_type().to_string(), span: v.span(), help: None, }), } } -} -impl FromValue for Spanned { - fn from_value(v: Value) -> Result { - let span = v.span(); - Ok(Spanned { - item: match v { - Value::CellPath { val, .. } => NuGlob::Expand(val.to_string()), - Value::String { val, .. } => NuGlob::DoNotExpand(val), - Value::Glob { - val, - no_expand: quoted, - .. - } => { - if quoted { - NuGlob::DoNotExpand(val) - } else { - NuGlob::Expand(val) - } - } - v => { - return Err(ShellError::CantConvert { - to_type: "string".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }) - } - }, - span, - }) + fn expected_type() -> Type { + Type::Binary } } -impl FromValue for Vec { +// Blanket std Implementations + +impl FromValue for Option +where + T: FromValue, +{ fn from_value(v: Value) -> Result { - // FIXME: we may want to fail a little nicer here match v { - Value::List { vals, .. } => vals - .into_iter() - .map(|val| match val { - Value::String { val, .. } => Ok(val), - c => Err(ShellError::CantConvert { - to_type: "string".into(), - from_type: c.get_type().to_string(), - span: c.span(), - help: None, - }), - }) - .collect::, ShellError>>(), - v => Err(ShellError::CantConvert { - to_type: "string".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }), + Value::Nothing { .. } => Ok(None), + v => T::from_value(v).map(Option::Some), } } + + fn expected_type() -> Type { + T::expected_type() + } } -impl FromValue for Vec> { +impl FromValue for HashMap +where + V: FromValue, +{ fn from_value(v: Value) -> Result { - // FIXME: we may want to fail a little nicer here - match v { - Value::List { vals, .. } => vals - .into_iter() - .map(|val| { - let val_span = val.span(); - match val { - Value::String { val, .. } => Ok(Spanned { - item: val, - span: val_span, - }), - c => Err(ShellError::CantConvert { - to_type: "string".into(), - from_type: c.get_type().to_string(), - span: c.span(), - help: None, - }), - } - }) - .collect::>, ShellError>>(), - v => Err(ShellError::CantConvert { - to_type: "string".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }), - } + let record = v.into_record()?; + let items: Result, ShellError> = record + .into_iter() + .map(|(k, v)| Ok((k, V::from_value(v)?))) + .collect(); + Ok(HashMap::from_iter(items?)) + } + + fn expected_type() -> Type { + Type::Record(vec![].into_boxed_slice()) } } -impl FromValue for Vec { +impl FromValue for Vec +where + T: FromValue, +{ fn from_value(v: Value) -> Result { match v { Value::List { vals, .. } => vals .into_iter() - .map(|val| match val { - Value::Bool { val, .. } => Ok(val), - c => Err(ShellError::CantConvert { - to_type: "bool".into(), - from_type: c.get_type().to_string(), - span: c.span(), - help: None, - }), - }) - .collect::, ShellError>>(), + .map(|v| T::from_value(v)) + .collect::, ShellError>>(), v => Err(ShellError::CantConvert { - to_type: "bool".into(), + to_type: Self::expected_type().to_string(), from_type: v.get_type().to_string(), span: v.span(), help: None, }), } } + + fn expected_type() -> Type { + Type::List(Box::new(T::expected_type())) + } +} + +// Nu Types + +impl FromValue for Value { + fn from_value(v: Value) -> Result { + Ok(v) + } + + fn expected_type() -> Type { + Type::Any + } } impl FromValue for CellPath { @@ -372,36 +537,25 @@ impl FromValue for CellPath { } } x => Err(ShellError::CantConvert { - to_type: "cell path".into(), + to_type: Self::expected_type().to_string(), from_type: x.get_type().to_string(), span, help: None, }), } } -} -impl FromValue for bool { - fn from_value(v: Value) -> Result { - match v { - Value::Bool { val, .. } => Ok(val), - v => Err(ShellError::CantConvert { - to_type: "bool".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }), - } + fn expected_type() -> Type { + Type::CellPath } } -impl FromValue for Spanned { +impl FromValue for Closure { fn from_value(v: Value) -> Result { - let span = v.span(); match v { - Value::Bool { val, .. } => Ok(Spanned { item: val, span }), + Value::Closure { val, .. } => Ok(*val), v => Err(ShellError::CantConvert { - to_type: "bool".into(), + to_type: Self::expected_type().to_string(), from_type: v.get_type().to_string(), span: v.span(), help: None, @@ -415,28 +569,48 @@ impl FromValue for DateTime { match v { Value::Date { val, .. } => Ok(val), v => Err(ShellError::CantConvert { - to_type: "date".into(), + to_type: Self::expected_type().to_string(), from_type: v.get_type().to_string(), span: v.span(), help: None, }), } } + + fn expected_type() -> Type { + Type::Date + } } -impl FromValue for Spanned> { +impl FromValue for NuGlob { fn from_value(v: Value) -> Result { - let span = v.span(); + // FIXME: we may want to fail a little nicer here match v { - Value::Date { val, .. } => Ok(Spanned { item: val, span }), + Value::CellPath { val, .. } => Ok(NuGlob::Expand(val.to_string())), + Value::String { val, .. } => Ok(NuGlob::DoNotExpand(val)), + Value::Glob { + val, + no_expand: quoted, + .. + } => { + if quoted { + Ok(NuGlob::DoNotExpand(val)) + } else { + Ok(NuGlob::Expand(val)) + } + } v => Err(ShellError::CantConvert { - to_type: "date".into(), + to_type: Self::expected_type().to_string(), from_type: v.get_type().to_string(), span: v.span(), help: None, }), } } + + fn expected_type() -> Type { + Type::String + } } impl FromValue for Range { @@ -444,94 +618,16 @@ impl FromValue for Range { match v { Value::Range { val, .. } => Ok(*val), v => Err(ShellError::CantConvert { - to_type: "range".into(), + to_type: Self::expected_type().to_string(), from_type: v.get_type().to_string(), span: v.span(), help: None, }), } } -} -impl FromValue for Spanned { - fn from_value(v: Value) -> Result { - let span = v.span(); - match v { - Value::Range { val, .. } => Ok(Spanned { item: *val, span }), - v => Err(ShellError::CantConvert { - to_type: "range".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }), - } - } -} - -impl FromValue for Vec { - fn from_value(v: Value) -> Result { - match v { - Value::Binary { val, .. } => Ok(val), - Value::String { val, .. } => Ok(val.into_bytes()), - v => Err(ShellError::CantConvert { - to_type: "binary data".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }), - } - } -} - -impl FromValue for Spanned> { - fn from_value(v: Value) -> Result { - let span = v.span(); - match v { - Value::Binary { val, .. } => Ok(Spanned { item: val, span }), - Value::String { val, .. } => Ok(Spanned { - item: val.into_bytes(), - span, - }), - v => Err(ShellError::CantConvert { - to_type: "binary data".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }), - } - } -} - -impl FromValue for Spanned { - fn from_value(v: Value) -> Result { - let span = v.span(); - match v { - Value::String { val, .. } => Ok(Spanned { - item: val.into(), - span, - }), - v => Err(ShellError::CantConvert { - to_type: "range".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }), - } - } -} - -impl FromValue for Vec { - fn from_value(v: Value) -> Result { - // FIXME: we may want to fail a little nicer here - match v { - Value::List { vals, .. } => Ok(vals), - v => Err(ShellError::CantConvert { - to_type: "Vector of values".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }), - } + fn expected_type() -> Type { + Type::Range } } @@ -540,7 +636,7 @@ impl FromValue for Record { match v { Value::Record { val, .. } => Ok(val.into_owned()), v => Err(ShellError::CantConvert { - to_type: "Record".into(), + to_type: Self::expected_type().to_string(), from_type: v.get_type().to_string(), span: v.span(), help: None, @@ -549,31 +645,39 @@ impl FromValue for Record { } } -impl FromValue for Closure { - fn from_value(v: Value) -> Result { - match v { - Value::Closure { val, .. } => Ok(*val), - v => Err(ShellError::CantConvert { - to_type: "Closure".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }), - } - } -} +// Blanket Nu Implementations -impl FromValue for Spanned { +impl FromValue for Spanned +where + T: FromValue, +{ fn from_value(v: Value) -> Result { let span = v.span(); - match v { - Value::Closure { val, .. } => Ok(Spanned { item: *val, span }), - v => Err(ShellError::CantConvert { - to_type: "Closure".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }), - } + Ok(Spanned { + item: T::from_value(v)?, + span, + }) + } + + fn expected_type() -> Type { + T::expected_type() + } +} + +#[cfg(test)] +mod tests { + use crate::{engine::Closure, FromValue, Record, Type}; + + #[test] + fn expected_type_default_impl() { + assert_eq!( + Record::expected_type(), + Type::Custom("Record".to_string().into_boxed_str()) + ); + + assert_eq!( + Closure::expected_type(), + Type::Custom("Closure".to_string().into_boxed_str()) + ); } } diff --git a/crates/nu-protocol/src/value/into_value.rs b/crates/nu-protocol/src/value/into_value.rs new file mode 100644 index 0000000000..c27b19b651 --- /dev/null +++ b/crates/nu-protocol/src/value/into_value.rs @@ -0,0 +1,196 @@ +use std::collections::HashMap; + +use crate::{Record, ShellError, Span, Value}; + +/// A trait for converting a value into a [`Value`]. +/// +/// This conversion is infallible, for fallible conversions use [`TryIntoValue`]. +/// +/// # Derivable +/// This trait can be used with `#[derive]`. +/// When derived on structs with named fields, the resulting value representation will use +/// [`Value::Record`], where each field of the record corresponds to a field of the struct. +/// For structs with unnamed fields, the value representation will be [`Value::List`], with all +/// fields inserted into a list. +/// Unit structs will be represented as [`Value::Nothing`] since they contain no data. +/// +/// Only enums with no fields may derive this trait. +/// The resulting value representation will be the name of the variant as a [`Value::String`]. +/// By default, variant names will be converted to ["snake_case"](convert_case::Case::Snake). +/// You can customize the case conversion using `#[nu_value(rename_all = "kebab-case")]` on the enum. +/// All deterministic and useful case conversions provided by [`convert_case::Case`] are supported +/// by specifying the case name followed by "case". +/// Also all values for +/// [`#[serde(rename_all = "...")]`](https://serde.rs/container-attrs.html#rename_all) are valid +/// here. +/// +/// ``` +/// # use nu_protocol::{IntoValue, Value, Span}; +/// #[derive(IntoValue)] +/// #[nu_value(rename_all = "COBOL-CASE")] +/// enum Bird { +/// MountainEagle, +/// ForestOwl, +/// RiverDuck, +/// } +/// +/// assert_eq!( +/// Bird::RiverDuck.into_value(Span::unknown()), +/// Value::test_string("RIVER-DUCK") +/// ); +/// ``` +pub trait IntoValue: Sized { + /// Converts the given value to a [`Value`]. + fn into_value(self, span: Span) -> Value; +} + +// Primitive Types + +impl IntoValue for [T; N] +where + T: IntoValue, +{ + fn into_value(self, span: Span) -> Value { + Vec::from(self).into_value(span) + } +} + +macro_rules! primitive_into_value { + ($type:ty, $method:ident) => { + primitive_into_value!($type => $type, $method); + }; + + ($type:ty => $as_type:ty, $method:ident) => { + impl IntoValue for $type { + fn into_value(self, span: Span) -> Value { + Value::$method(<$as_type>::from(self), span) + } + } + }; +} + +primitive_into_value!(bool, bool); +primitive_into_value!(char, string); +primitive_into_value!(f32 => f64, float); +primitive_into_value!(f64, float); +primitive_into_value!(i8 => i64, int); +primitive_into_value!(i16 => i64, int); +primitive_into_value!(i32 => i64, int); +primitive_into_value!(i64, int); +primitive_into_value!(u8 => i64, int); +primitive_into_value!(u16 => i64, int); +primitive_into_value!(u32 => i64, int); +// u64 and usize may be truncated as Value only supports i64. + +impl IntoValue for isize { + fn into_value(self, span: Span) -> Value { + Value::int(self as i64, span) + } +} + +impl IntoValue for () { + fn into_value(self, span: Span) -> Value { + Value::nothing(span) + } +} + +macro_rules! tuple_into_value { + ($($t:ident:$n:tt),+) => { + impl<$($t),+> IntoValue for ($($t,)+) where $($t: IntoValue,)+ { + fn into_value(self, span: Span) -> Value { + let vals = vec![$(self.$n.into_value(span)),+]; + Value::list(vals, span) + } + } + } +} + +// Tuples in std are implemented for up to 12 elements, so we do it here too. +tuple_into_value!(T0:0); +tuple_into_value!(T0:0, T1:1); +tuple_into_value!(T0:0, T1:1, T2:2); +tuple_into_value!(T0:0, T1:1, T2:2, T3:3); +tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4); +tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5); +tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6); +tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7); +tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8); +tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8, T9:9); +tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8, T9:9, T10:10); +tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8, T9:9, T10:10, T11:11); + +// Other std Types + +impl IntoValue for String { + fn into_value(self, span: Span) -> Value { + Value::string(self, span) + } +} + +impl IntoValue for Vec +where + T: IntoValue, +{ + fn into_value(self, span: Span) -> Value { + Value::list(self.into_iter().map(|v| v.into_value(span)).collect(), span) + } +} + +impl IntoValue for Option +where + T: IntoValue, +{ + fn into_value(self, span: Span) -> Value { + match self { + Some(v) => v.into_value(span), + None => Value::nothing(span), + } + } +} + +impl IntoValue for HashMap +where + V: IntoValue, +{ + fn into_value(self, span: Span) -> Value { + let mut record = Record::new(); + for (k, v) in self.into_iter() { + // Using `push` is fine as a hashmaps have unique keys. + // To ensure this uniqueness, we only allow hashmaps with strings as + // keys and not keys which implement `Into` or `ToString`. + record.push(k, v.into_value(span)); + } + Value::record(record, span) + } +} + +// Nu Types + +impl IntoValue for Value { + fn into_value(self, span: Span) -> Value { + self.with_span(span) + } +} + +// TODO: use this type for all the `into_value` methods that types implement but return a Result +/// A trait for trying to convert a value into a `Value`. +/// +/// Types like streams may fail while collecting the `Value`, +/// for these types it is useful to implement a fallible variant. +/// +/// This conversion is fallible, for infallible conversions use [`IntoValue`]. +/// All types that implement `IntoValue` will automatically implement this trait. +pub trait TryIntoValue: Sized { + // TODO: instead of ShellError, maybe we could have a IntoValueError that implements Into + /// Tries to convert the given value into a `Value`. + fn try_into_value(self, span: Span) -> Result; +} + +impl TryIntoValue for T +where + T: IntoValue, +{ + fn try_into_value(self, span: Span) -> Result { + Ok(self.into_value(span)) + } +} diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index f924218b79..df030ad3e9 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -4,7 +4,10 @@ mod filesize; mod from; mod from_value; mod glob; +mod into_value; mod range; +#[cfg(test)] +mod test_derive; pub mod record; pub use custom_value::CustomValue; @@ -12,6 +15,7 @@ pub use duration::*; pub use filesize::*; pub use from_value::FromValue; pub use glob::*; +pub use into_value::{IntoValue, TryIntoValue}; pub use range::{FloatRange, IntRange, Range}; pub use record::Record; @@ -1089,7 +1093,7 @@ impl Value { } else { return Err(ShellError::CantFindColumn { col_name: column_name.clone(), - span: *origin_span, + span: Some(*origin_span), src_span: span, }); } @@ -1126,7 +1130,7 @@ impl Value { } else { Err(ShellError::CantFindColumn { col_name: column_name.clone(), - span: *origin_span, + span: Some(*origin_span), src_span: val_span, }) } @@ -1136,7 +1140,7 @@ impl Value { } _ => Err(ShellError::CantFindColumn { col_name: column_name.clone(), - span: *origin_span, + span: Some(*origin_span), src_span: val_span, }), } @@ -1237,7 +1241,7 @@ impl Value { v => { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v.span(), }); } @@ -1262,7 +1266,7 @@ impl Value { v => { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v.span(), }); } @@ -1342,7 +1346,7 @@ impl Value { } else { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v_span, }); } @@ -1351,7 +1355,7 @@ impl Value { v => { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v.span(), }); } @@ -1364,7 +1368,7 @@ impl Value { } else { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v_span, }); } @@ -1373,7 +1377,7 @@ impl Value { v => { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v.span(), }); } @@ -1427,7 +1431,7 @@ impl Value { if record.to_mut().remove(col_name).is_none() && !optional { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v_span, }); } @@ -1435,7 +1439,7 @@ impl Value { v => { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v.span(), }); } @@ -1447,7 +1451,7 @@ impl Value { if record.to_mut().remove(col_name).is_none() && !optional { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v_span, }); } @@ -1455,7 +1459,7 @@ impl Value { } v => Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v.span(), }), }, @@ -1504,7 +1508,7 @@ impl Value { } else if !optional { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v_span, }); } @@ -1512,7 +1516,7 @@ impl Value { v => { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v.span(), }); } @@ -1526,7 +1530,7 @@ impl Value { } else if !optional { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v_span, }); } @@ -1534,7 +1538,7 @@ impl Value { } v => Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v.span(), }), }, diff --git a/crates/nu-protocol/src/value/test_derive.rs b/crates/nu-protocol/src/value/test_derive.rs new file mode 100644 index 0000000000..1865a05418 --- /dev/null +++ b/crates/nu-protocol/src/value/test_derive.rs @@ -0,0 +1,386 @@ +use crate::{record, FromValue, IntoValue, Record, Span, Value}; +use std::collections::HashMap; + +// Make nu_protocol available in this namespace, consumers of this crate will +// have this without such an export. +// The derive macro fully qualifies paths to "nu_protocol". +use crate as nu_protocol; + +trait IntoTestValue { + fn into_test_value(self) -> Value; +} + +impl IntoTestValue for T +where + T: IntoValue, +{ + fn into_test_value(self) -> Value { + self.into_value(Span::test_data()) + } +} + +#[derive(IntoValue, FromValue, Debug, PartialEq)] +struct NamedFieldsStruct +where + T: IntoValue + FromValue, +{ + array: [u16; 4], + bool: bool, + char: char, + f32: f32, + f64: f64, + i8: i8, + i16: i16, + i32: i32, + i64: i64, + isize: isize, + u16: u16, + u32: u32, + unit: (), + tuple: (u32, bool), + some: Option, + none: Option, + vec: Vec, + string: String, + hashmap: HashMap, + nested: Nestee, +} + +#[derive(IntoValue, FromValue, Debug, PartialEq)] +struct Nestee { + u32: u32, + some: Option, + none: Option, +} + +impl NamedFieldsStruct { + fn make() -> Self { + Self { + array: [1, 2, 3, 4], + bool: true, + char: 'a', + f32: std::f32::consts::PI, + f64: std::f64::consts::E, + i8: 127, + i16: -32768, + i32: 2147483647, + i64: -9223372036854775808, + isize: 2, + u16: 65535, + u32: 4294967295, + unit: (), + tuple: (1, true), + some: Some(123), + none: None, + vec: vec![10, 20, 30], + string: "string".to_string(), + hashmap: HashMap::from_iter([("a".to_string(), 10), ("b".to_string(), 20)]), + nested: Nestee { + u32: 3, + some: Some(42), + none: None, + }, + } + } + + fn value() -> Value { + Value::test_record(record! { + "array" => Value::test_list(vec![ + Value::test_int(1), + Value::test_int(2), + Value::test_int(3), + Value::test_int(4) + ]), + "bool" => Value::test_bool(true), + "char" => Value::test_string('a'), + "f32" => Value::test_float(std::f32::consts::PI.into()), + "f64" => Value::test_float(std::f64::consts::E), + "i8" => Value::test_int(127), + "i16" => Value::test_int(-32768), + "i32" => Value::test_int(2147483647), + "i64" => Value::test_int(-9223372036854775808), + "isize" => Value::test_int(2), + "u16" => Value::test_int(65535), + "u32" => Value::test_int(4294967295), + "unit" => Value::test_nothing(), + "tuple" => Value::test_list(vec![ + Value::test_int(1), + Value::test_bool(true) + ]), + "some" => Value::test_int(123), + "none" => Value::test_nothing(), + "vec" => Value::test_list(vec![ + Value::test_int(10), + Value::test_int(20), + Value::test_int(30) + ]), + "string" => Value::test_string("string"), + "hashmap" => Value::test_record(record! { + "a" => Value::test_int(10), + "b" => Value::test_int(20) + }), + "nested" => Value::test_record(record! { + "u32" => Value::test_int(3), + "some" => Value::test_int(42), + "none" => Value::test_nothing(), + }) + }) + } +} + +#[test] +fn named_fields_struct_into_value() { + let expected = NamedFieldsStruct::value(); + let actual = NamedFieldsStruct::make().into_test_value(); + assert_eq!(expected, actual); +} + +#[test] +fn named_fields_struct_from_value() { + let expected = NamedFieldsStruct::make(); + let actual = NamedFieldsStruct::from_value(NamedFieldsStruct::value()).unwrap(); + assert_eq!(expected, actual); +} + +#[test] +fn named_fields_struct_roundtrip() { + let expected = NamedFieldsStruct::make(); + let actual = + NamedFieldsStruct::from_value(NamedFieldsStruct::make().into_test_value()).unwrap(); + assert_eq!(expected, actual); + + let expected = NamedFieldsStruct::value(); + let actual = NamedFieldsStruct::::from_value(NamedFieldsStruct::value()) + .unwrap() + .into_test_value(); + assert_eq!(expected, actual); +} + +#[test] +fn named_fields_struct_missing_value() { + let value = Value::test_record(Record::new()); + let res: Result, _> = NamedFieldsStruct::from_value(value); + assert!(res.is_err()); +} + +#[test] +fn named_fields_struct_incorrect_type() { + // Should work for every type that is not a record. + let value = Value::test_nothing(); + let res: Result, _> = NamedFieldsStruct::from_value(value); + assert!(res.is_err()); +} + +#[derive(IntoValue, FromValue, Debug, PartialEq)] +struct UnnamedFieldsStruct(u32, String, T) +where + T: IntoValue + FromValue; + +impl UnnamedFieldsStruct { + fn make() -> Self { + UnnamedFieldsStruct(420, "Hello, tuple!".to_string(), 33.33) + } + + fn value() -> Value { + Value::test_list(vec![ + Value::test_int(420), + Value::test_string("Hello, tuple!"), + Value::test_float(33.33), + ]) + } +} + +#[test] +fn unnamed_fields_struct_into_value() { + let expected = UnnamedFieldsStruct::value(); + let actual = UnnamedFieldsStruct::make().into_test_value(); + assert_eq!(expected, actual); +} + +#[test] +fn unnamed_fields_struct_from_value() { + let expected = UnnamedFieldsStruct::make(); + let value = UnnamedFieldsStruct::value(); + let actual = UnnamedFieldsStruct::from_value(value).unwrap(); + assert_eq!(expected, actual); +} + +#[test] +fn unnamed_fields_struct_roundtrip() { + let expected = UnnamedFieldsStruct::make(); + let actual = + UnnamedFieldsStruct::from_value(UnnamedFieldsStruct::make().into_test_value()).unwrap(); + assert_eq!(expected, actual); + + let expected = UnnamedFieldsStruct::value(); + let actual = UnnamedFieldsStruct::::from_value(UnnamedFieldsStruct::value()) + .unwrap() + .into_test_value(); + assert_eq!(expected, actual); +} + +#[test] +fn unnamed_fields_struct_missing_value() { + let value = Value::test_list(vec![]); + let res: Result, _> = UnnamedFieldsStruct::from_value(value); + assert!(res.is_err()); +} + +#[test] +fn unnamed_fields_struct_incorrect_type() { + // Should work for every type that is not a record. + let value = Value::test_nothing(); + let res: Result, _> = UnnamedFieldsStruct::from_value(value); + assert!(res.is_err()); +} + +#[derive(IntoValue, FromValue, Debug, PartialEq)] +struct UnitStruct; + +#[test] +fn unit_struct_into_value() { + let expected = Value::test_nothing(); + let actual = UnitStruct.into_test_value(); + assert_eq!(expected, actual); +} + +#[test] +fn unit_struct_from_value() { + let expected = UnitStruct; + let actual = UnitStruct::from_value(Value::test_nothing()).unwrap(); + assert_eq!(expected, actual); +} + +#[test] +fn unit_struct_roundtrip() { + let expected = UnitStruct; + let actual = UnitStruct::from_value(UnitStruct.into_test_value()).unwrap(); + assert_eq!(expected, actual); + + let expected = Value::test_nothing(); + let actual = UnitStruct::from_value(Value::test_nothing()) + .unwrap() + .into_test_value(); + assert_eq!(expected, actual); +} + +#[derive(IntoValue, FromValue, Debug, PartialEq)] +enum Enum { + AlphaOne, + BetaTwo, + CharlieThree, +} + +impl Enum { + fn make() -> [Self; 3] { + [Enum::AlphaOne, Enum::BetaTwo, Enum::CharlieThree] + } + + fn value() -> Value { + Value::test_list(vec![ + Value::test_string("alpha_one"), + Value::test_string("beta_two"), + Value::test_string("charlie_three"), + ]) + } +} + +#[test] +fn enum_into_value() { + let expected = Enum::value(); + let actual = Enum::make().into_test_value(); + assert_eq!(expected, actual); +} + +#[test] +fn enum_from_value() { + let expected = Enum::make(); + let actual = <[Enum; 3]>::from_value(Enum::value()).unwrap(); + assert_eq!(expected, actual); +} + +#[test] +fn enum_roundtrip() { + let expected = Enum::make(); + let actual = <[Enum; 3]>::from_value(Enum::make().into_test_value()).unwrap(); + assert_eq!(expected, actual); + + let expected = Enum::value(); + let actual = <[Enum; 3]>::from_value(Enum::value()) + .unwrap() + .into_test_value(); + assert_eq!(expected, actual); +} + +#[test] +fn enum_unknown_variant() { + let value = Value::test_string("delta_four"); + let res = Enum::from_value(value); + assert!(res.is_err()); +} + +#[test] +fn enum_incorrect_type() { + // Should work for every type that is not a record. + let value = Value::test_nothing(); + let res = Enum::from_value(value); + assert!(res.is_err()); +} + +// Generate the `Enum` from before but with all possible `rename_all` variants. +macro_rules! enum_rename_all { + ($($ident:ident: $case:literal => [$a1:literal, $b2:literal, $c3:literal]),*) => { + $( + #[derive(Debug, PartialEq, IntoValue, FromValue)] + #[nu_value(rename_all = $case)] + enum $ident { + AlphaOne, + BetaTwo, + CharlieThree + } + + impl $ident { + fn make() -> [Self; 3] { + [Self::AlphaOne, Self::BetaTwo, Self::CharlieThree] + } + + fn value() -> Value { + Value::test_list(vec![ + Value::test_string($a1), + Value::test_string($b2), + Value::test_string($c3), + ]) + } + } + )* + + #[test] + fn enum_rename_all_into_value() {$({ + let expected = $ident::value(); + let actual = $ident::make().into_test_value(); + assert_eq!(expected, actual); + })*} + + #[test] + fn enum_rename_all_from_value() {$({ + let expected = $ident::make(); + let actual = <[$ident; 3]>::from_value($ident::value()).unwrap(); + assert_eq!(expected, actual); + })*} + } +} + +enum_rename_all! { + Upper: "UPPER CASE" => ["ALPHA ONE", "BETA TWO", "CHARLIE THREE"], + Lower: "lower case" => ["alpha one", "beta two", "charlie three"], + Title: "Title Case" => ["Alpha One", "Beta Two", "Charlie Three"], + Camel: "camelCase" => ["alphaOne", "betaTwo", "charlieThree"], + Pascal: "PascalCase" => ["AlphaOne", "BetaTwo", "CharlieThree"], + Snake: "snake_case" => ["alpha_one", "beta_two", "charlie_three"], + UpperSnake: "UPPER_SNAKE_CASE" => ["ALPHA_ONE", "BETA_TWO", "CHARLIE_THREE"], + Kebab: "kebab-case" => ["alpha-one", "beta-two", "charlie-three"], + Cobol: "COBOL-CASE" => ["ALPHA-ONE", "BETA-TWO", "CHARLIE-THREE"], + Train: "Train-Case" => ["Alpha-One", "Beta-Two", "Charlie-Three"], + Flat: "flatcase" => ["alphaone", "betatwo", "charliethree"], + UpperFlat: "UPPERFLATCASE" => ["ALPHAONE", "BETATWO", "CHARLIETHREE"] +} diff --git a/crates/nu_plugin_custom_values/src/cool_custom_value.rs b/crates/nu_plugin_custom_values/src/cool_custom_value.rs index d838aa3f9e..ea0cf8ec92 100644 --- a/crates/nu_plugin_custom_values/src/cool_custom_value.rs +++ b/crates/nu_plugin_custom_values/src/cool_custom_value.rs @@ -87,7 +87,7 @@ impl CustomValue for CoolCustomValue { } else { Err(ShellError::CantFindColumn { col_name: column_name, - span: path_span, + span: Some(path_span), src_span: self_span, }) }