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, }) }