Add derive macros for FromValue and IntoValue to ease the use of Values in Rust code (#13031)

# 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.
This commit is contained in:
Piepmatz 2024-06-18 01:05:11 +02:00 committed by GitHub
parent 3a6d8aac0b
commit b79a2255d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 2378 additions and 385 deletions

22
Cargo.lock generated
View File

@ -920,6 +920,15 @@ dependencies = [
"unicode-xid", "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]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@ -3020,6 +3029,17 @@ dependencies = [
"winreg", "winreg",
] ]
[[package]]
name = "nu-derive-value"
version = "0.94.3"
dependencies = [
"convert_case",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.60",
]
[[package]] [[package]]
name = "nu-engine" name = "nu-engine"
version = "0.94.3" version = "0.94.3"
@ -3209,11 +3229,13 @@ dependencies = [
"byte-unit", "byte-unit",
"chrono", "chrono",
"chrono-humanize", "chrono-humanize",
"convert_case",
"fancy-regex", "fancy-regex",
"indexmap", "indexmap",
"lru", "lru",
"miette", "miette",
"nix", "nix",
"nu-derive-value",
"nu-path", "nu-path",
"nu-system", "nu-system",
"nu-test-support", "nu-test-support",

View File

@ -39,6 +39,7 @@ members = [
"crates/nu-lsp", "crates/nu-lsp",
"crates/nu-pretty-hex", "crates/nu-pretty-hex",
"crates/nu-protocol", "crates/nu-protocol",
"crates/nu-derive-value",
"crates/nu-plugin", "crates/nu-plugin",
"crates/nu-plugin-core", "crates/nu-plugin-core",
"crates/nu-plugin-engine", "crates/nu-plugin-engine",
@ -74,6 +75,7 @@ chardetng = "0.1.17"
chrono = { default-features = false, version = "0.4.34" } chrono = { default-features = false, version = "0.4.34" }
chrono-humanize = "0.2.3" chrono-humanize = "0.2.3"
chrono-tz = "0.8" chrono-tz = "0.8"
convert_case = "0.6"
crossbeam-channel = "0.5.8" crossbeam-channel = "0.5.8"
crossterm = "0.27" crossterm = "0.27"
csv = "1.3" csv = "1.3"
@ -123,11 +125,14 @@ pathdiff = "0.2"
percent-encoding = "2" percent-encoding = "2"
pretty_assertions = "1.4" pretty_assertions = "1.4"
print-positions = "0.6" print-positions = "0.6"
proc-macro-error = { version = "1.0", default-features = false }
proc-macro2 = "1.0"
procfs = "0.16.0" procfs = "0.16.0"
pwd = "1.3" pwd = "1.3"
quick-xml = "0.31.0" quick-xml = "0.31.0"
quickcheck = "1.0" quickcheck = "1.0"
quickcheck_macros = "1.0" quickcheck_macros = "1.0"
quote = "1.0"
rand = "0.8" rand = "0.8"
ratatui = "0.26" ratatui = "0.26"
rayon = "1.10" rayon = "1.10"
@ -147,6 +152,7 @@ serde_urlencoded = "0.7.1"
serde_yaml = "0.9" serde_yaml = "0.9"
sha2 = "0.10" sha2 = "0.10"
strip-ansi-escapes = "0.2.0" strip-ansi-escapes = "0.2.0"
syn = "2.0"
sysinfo = "0.30" sysinfo = "0.30"
tabled = { version = "0.14.0", default-features = false } tabled = { version = "0.14.0", default-features = false }
tempfile = "3.10" tempfile = "3.10"

View File

@ -194,7 +194,7 @@ pub fn eval_hook(
let Some(follow) = val.get("code") else { let Some(follow) = val.get("code") else {
return Err(ShellError::CantFindColumn { return Err(ShellError::CantFindColumn {
col_name: "code".into(), col_name: "code".into(),
span, span: Some(span),
src_span: span, src_span: span,
}); });
}; };

View File

@ -194,7 +194,7 @@ fn run_histogram(
if inputs.is_empty() { if inputs.is_empty() {
return Err(ShellError::CantFindColumn { return Err(ShellError::CantFindColumn {
col_name: col_name.clone(), col_name: col_name.clone(),
span: head_span, span: Some(head_span),
src_span: list_span, src_span: list_span,
}); });
} }

View File

@ -154,7 +154,7 @@ fn record_to_path_member(
let Some(value) = record.get("value") else { let Some(value) = record.get("value") else {
return Err(ShellError::CantFindColumn { return Err(ShellError::CantFindColumn {
col_name: "value".into(), col_name: "value".into(),
span: val_span, span: Some(val_span),
src_span: span, src_span: span,
}); });
}; };

View File

@ -130,7 +130,7 @@ pub fn split(
Some(group_key) => Ok(group_key.coerce_string()?), Some(group_key) => Ok(group_key.coerce_string()?),
None => Err(ShellError::CantFindColumn { None => Err(ShellError::CantFindColumn {
col_name: column_name.item.to_string(), col_name: column_name.item.to_string(),
span: column_name.span, span: Some(column_name.span),
src_span: row.span(), src_span: row.span(),
}), }),
} }

View File

@ -123,7 +123,7 @@ fn validate(vec: &[Value], columns: &[String], span: Span) -> Result<(), ShellEr
if let Some(nonexistent) = nonexistent_column(columns, record.columns()) { if let Some(nonexistent) = nonexistent_column(columns, record.columns()) {
return Err(ShellError::CantFindColumn { return Err(ShellError::CantFindColumn {
col_name: nonexistent, col_name: nonexistent,
span, span: Some(span),
src_span: val_span, src_span: val_span,
}); });
} }

View File

@ -81,7 +81,7 @@ pub fn sort(
if let Some(nonexistent) = nonexistent_column(&sort_columns, record.columns()) { if let Some(nonexistent) = nonexistent_column(&sort_columns, record.columns()) {
return Err(ShellError::CantFindColumn { return Err(ShellError::CantFindColumn {
col_name: nonexistent, col_name: nonexistent,
span, span: Some(span),
src_span: val_span, src_span: val_span,
}); });
} }

View File

@ -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 }

View File

@ -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.

View File

@ -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<Item = &'a Attribute>,
) -> Result<Self, DeriveError<M>> {
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<Item = &'a Attribute>,
) -> impl Iterator<Item = &'a Attribute> {
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<M>(attrs: &[Attribute]) -> Result<(), DeriveError<M>> {
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<M>(fields: &Fields) -> Result<(), DeriveError<M>> {
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(())
}

View File

@ -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<M> {
/// Marker variant, makes the `M` generic parameter valid.
_Marker(PhantomData<M>),
/// 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<dyn Debug>,
},
}
impl<M> From<DeriveError<M>> for Diagnostic {
fn from(value: DeriveError<M>) -> Self {
let derive_name = any::type_name::<M>().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"
))
}
}
}
}

View File

@ -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<FromValue>;
/// 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<TokenStream2, DeriveError> {
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<Attribute>,
) -> Result<TokenStream2, DeriveError> {
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<String>,
/// }
///
/// impl nu_protocol::FromValue for Pet {
/// fn from_value(
/// v: nu_protocol::Value
/// ) -> std::result::Result<Self, nu_protocol::ShellError> {
/// let span = v.span();
/// let mut record = v.into_record()?;
/// std::result::Result::Ok(Pet {
/// name: <String as nu_protocol::FromValue>::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: <u8 as nu_protocol::FromValue>::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: <Option<String> 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<Self, nu_protocol::ShellError> {
/// 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(
/// {
/// <u8 as nu_protocol::FromValue>::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
/// })?,
/// )?
/// },
/// {
/// <u8 as nu_protocol::FromValue>::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
/// })?,
/// )?
/// },
/// {
/// <u8 as nu_protocol::FromValue>::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<Self, nu_protocol::ShellError> {
/// match v {
/// nu_protocol::Value::Nothing {..} => Ok(Self),
/// v => std::result::Result::Err(nu_protocol::ShellError::CantConvert {
/// to_type: std::string::ToString::to_string(&<Self as nu_protocol::FromValue>::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<Self, nu_protocol::ShellError> {
#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<String>,
/// }
///
/// impl nu_protocol::FromValue for Pet {
/// fn expected_type() -> nu_protocol::Type {
/// nu_protocol::Type::Record(
/// std::vec![
/// (
/// std::string::ToString::to_string("name"),
/// <String as nu_protocol::FromValue>::expected_type(),
/// ),
/// (
/// std::string::ToString::to_string("age"),
/// <u8 as nu_protocol::FromValue>::expected_type(),
/// ),
/// (
/// std::string::ToString::to_string("favorite_toy"),
/// <Option<String> 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!(
/// "[{}, {}, {}]",
/// <u8 as nu_protocol::FromValue>::expected_type(),
/// <u8 as nu_protocol::FromValue>::expected_type(),
/// <u8 as nu_protocol::FromValue>::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<Attribute>,
) -> Result<TokenStream2, DeriveError> {
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(
/// &<Self as nu_protocol::FromValue>::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<TokenStream2, DeriveError> {
let container_attrs = ContainerAttributes::parse_attrs(attrs.iter())?;
let arms: Vec<TokenStream2> = 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::<Result<_, _>>()?;
Ok(quote! {
fn from_value(
v: nu_protocol::Value
) -> std::result::Result<Self, nu_protocol::ShellError> {
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(
&<Self as nu_protocol::FromValue>::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(&<Self as nu_protocol::FromValue>::expected_type()),
from_type: std::string::ToString::to_string(&v.get_type()),
span: v.span(),
help: std::option::Option::None
})
}
},
}
}

View File

@ -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<IntoValue>;
/// Inner implementation of the `#[derive(IntoValue)]` macro for structs and enums.
///
/// Uses `proc_macro2::TokenStream` for better testing support, unlike `proc_macro::TokenStream`.
///
/// This function directs the `IntoValue` trait derivation to the correct implementation based on
/// the input type:
/// - For structs: [`struct_into_value`]
/// - For enums: [`enum_into_value`]
/// - Unions are not supported and will return an error.
pub fn derive_into_value(input: TokenStream2) -> Result<TokenStream2, DeriveError> {
let input: DeriveInput = syn::parse2(input).map_err(DeriveError::Syn)?;
match input.data {
Data::Struct(data_struct) => Ok(struct_into_value(
input.ident,
data_struct,
input.generics,
input.attrs,
)?),
Data::Enum(data_enum) => Ok(enum_into_value(
input.ident,
data_enum,
input.generics,
input.attrs,
)?),
Data::Union(_) => Err(DeriveError::UnsupportedUnions),
}
}
/// Implements the `#[derive(IntoValue)]` macro for structs.
///
/// Automatically derives the `IntoValue` trait for any struct where each field implements
/// `IntoValue`.
/// For structs with named fields, the derived implementation creates a `Value::Record` using the
/// struct fields as keys.
/// Each field value is converted using the `IntoValue::into_value` method.
/// For structs with unnamed fields, this generates a `Value::List` with each field in the list.
/// For unit structs, this generates `Value::Nothing`, because there is no data.
///
/// 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<String>,
/// }
///
/// impl nu_protocol::IntoValue for Pet {
/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
/// nu_protocol::Value::record(nu_protocol::record! {
/// "name" => nu_protocol::IntoValue::into_value(self.name, span),
/// "age" => nu_protocol::IntoValue::into_value(self.age, span),
/// "favorite_toy" => nu_protocol::IntoValue::into_value(self.favorite_toy, span),
/// }, span)
/// }
/// }
/// ```
///
/// Struct with unnamed fields:
/// ```rust
/// #[derive(IntoValue)]
/// struct Color(u8, u8, u8);
///
/// impl nu_protocol::IntoValue for Color {
/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
/// nu_protocol::Value::list(vec![
/// nu_protocol::IntoValue::into_value(self.0, span),
/// nu_protocol::IntoValue::into_value(self.1, span),
/// nu_protocol::IntoValue::into_value(self.2, span),
/// ], span)
/// }
/// }
/// ```
///
/// Unit struct:
/// ```rust
/// #[derive(IntoValue)]
/// struct Unicorn;
///
/// impl nu_protocol::IntoValue for Unicorn {
/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
/// nu_protocol::Value::nothing(span)
/// }
/// }
/// ```
fn struct_into_value(
ident: Ident,
data: DataStruct,
generics: Generics,
attrs: Vec<Attribute>,
) -> Result<TokenStream2, DeriveError> {
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<Attribute>,
) -> Result<TokenStream2, DeriveError> {
let container_attrs = ContainerAttributes::parse_attrs(attrs.iter())?;
let arms: Vec<TokenStream2> = data
.variants
.into_iter()
.map(|variant| {
attributes::deny(&variant.attrs)?;
let ident = variant.ident;
let ident_s = format!("{ident}")
.as_str()
.to_case(container_attrs.rename_all);
match &variant.fields {
// In the future we can implement more complexe enums here.
Fields::Named(fields) => Err(DeriveError::UnsupportedEnums {
fields_span: fields.span(),
}),
Fields::Unnamed(fields) => Err(DeriveError::UnsupportedEnums {
fields_span: fields.span(),
}),
Fields::Unit => {
Ok(quote!(Self::#ident => nu_protocol::Value::string(#ident_s, span)))
}
}
})
.collect::<Result<_, _>>()?;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
Ok(quote! {
impl #impl_generics nu_protocol::IntoValue for #ident #ty_generics #where_clause {
fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
match self {
#(#arms,)*
}
}
}
})
}
/// Constructs the final `Value` that the macro generates.
///
/// This function handles the construction of the final `Value` that the macro generates.
/// It is currently only used for structs but may be used for enums in the future.
/// The function takes two parameters: the `fields`, which allow iterating over each field of a data
/// type, and the `accessor`.
/// The fields determine whether we need to generate a `Value::Record`, `Value::List`, or
/// `Value::Nothing`.
/// For named fields, they are also directly used to generate the record key.
///
/// The `accessor` parameter generalizes how the data is accessed.
/// For named fields, this is usually the name of the fields preceded by `self` in a struct, and
/// maybe something else for enums.
/// For unnamed fields, this should be an iterator similar to the one with named fields, but
/// accessing tuple fields, so we get `self.n`.
/// For unit structs, this parameter is ignored.
/// By using the accessor like this, we can have the same code for structs and enums with data
/// variants in the future.
fn fields_return_value(
fields: &Fields,
accessor: impl Iterator<Item = impl ToTokens>,
) -> TokenStream2 {
match fields {
Fields::Named(fields) => {
let items: Vec<TokenStream2> = fields
.named
.iter()
.zip(accessor)
.map(|(field, accessor)| {
let ident = field.ident.as_ref().expect("named has idents");
let field = ident.to_string();
quote!(#field => nu_protocol::IntoValue::into_value(#accessor, span))
})
.collect();
quote! {
nu_protocol::Value::record(nu_protocol::record! {
#(#items),*
}, span)
}
}
Fields::Unnamed(fields) => {
let items =
fields.unnamed.iter().zip(accessor).map(
|(_, accessor)| quote!(nu_protocol::IntoValue::into_value(#accessor, span)),
);
quote!(nu_protocol::Value::list(std::vec![#(#items),*], span))
}
Fields::Unit => quote!(nu_protocol::Value::nothing(span)),
}
}

View File

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

View File

@ -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
);
}

View File

@ -16,11 +16,13 @@ bench = false
nu-utils = { path = "../nu-utils", version = "0.94.3" } nu-utils = { path = "../nu-utils", version = "0.94.3" }
nu-path = { path = "../nu-path", version = "0.94.3" } nu-path = { path = "../nu-path", version = "0.94.3" }
nu-system = { path = "../nu-system", 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 } brotli = { workspace = true, optional = true }
byte-unit = { version = "5.1", features = [ "serde" ] } byte-unit = { version = "5.1", features = [ "serde" ] }
chrono = { workspace = true, features = [ "serde", "std", "unstable-locales" ], default-features = false } chrono = { workspace = true, features = [ "serde", "std", "unstable-locales" ], default-features = false }
chrono-humanize = { workspace = true } chrono-humanize = { workspace = true }
convert_case = { workspace = true }
fancy-regex = { workspace = true } fancy-regex = { workspace = true }
indexmap = { workspace = true } indexmap = { workspace = true }
lru = { workspace = true } lru = { workspace = true }

View File

@ -557,12 +557,12 @@ pub enum ShellError {
/// ## Resolution /// ## Resolution
/// ///
/// Check the spelling of your column name. Did you forget to rename a column somewhere? /// 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))] #[diagnostic(code(nu::shell::column_not_found))]
CantFindColumn { CantFindColumn {
col_name: String, col_name: String,
#[label = "cannot find column '{col_name}'"] #[label = "cannot find column '{col_name}'"]
span: Span, span: Option<Span>,
#[label = "value originates here"] #[label = "value originates here"]
src_span: Span, src_span: Span,
}, },

View File

@ -39,3 +39,5 @@ pub use span::*;
pub use syntax_shape::*; pub use syntax_shape::*;
pub use ty::*; pub use ty::*;
pub use value::*; pub use value::*;
pub use nu_derive_value::*;

View File

@ -1,37 +1,188 @@
use crate::{ use crate::{
ast::{CellPath, PathMember}, ast::{CellPath, PathMember},
engine::Closure, engine::Closure,
NuGlob, Range, Record, ShellError, Spanned, Value, NuGlob, Range, Record, ShellError, Spanned, Type, Value,
}; };
use chrono::{DateTime, FixedOffset}; 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 { pub trait FromValue: Sized {
// TODO: instead of ShellError, maybe we could have a FromValueError that implements Into<ShellError>
/// 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<Self, ShellError>; fn from_value(v: Value) -> Result<Self, ShellError>;
}
impl FromValue for Value { /// Expected `Value` type.
fn from_value(v: Value) -> Result<Self, ShellError> { ///
Ok(v) /// 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::<Self>()
.split(':')
.last()
.expect("str::split returns an iterator with at least one element")
.to_string()
.into_boxed_str(),
)
} }
} }
impl FromValue for Spanned<i64> { // Primitive Types
impl<T, const N: usize> FromValue for [T; N]
where
T: FromValue,
{
fn from_value(v: Value) -> Result<Self, ShellError> { fn from_value(v: Value) -> Result<Self, ShellError> {
let span = v.span(); let span = v.span();
match v { let v_ty = v.get_type();
Value::Int { val, .. } => Ok(Spanned { item: val, span }), let vec = Vec::<T>::from_value(v)?;
Value::Filesize { val, .. } => Ok(Spanned { item: val, span }), vec.try_into()
Value::Duration { val, .. } => Ok(Spanned { item: val, span }), .map_err(|err_vec: Vec<T>| 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<Self, ShellError> {
match v {
Value::Bool { val, .. } => Ok(val),
v => Err(ShellError::CantConvert { v => Err(ShellError::CantConvert {
to_type: "int".into(), to_type: Self::expected_type().to_string(),
from_type: v.get_type().to_string(), from_type: v.get_type().to_string(),
span: v.span(), span: v.span(),
help: None, help: None,
}), }),
} }
} }
fn expected_type() -> Type {
Type::Bool
}
}
impl FromValue for char {
fn from_value(v: Value) -> Result<Self, ShellError> {
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<Self, ShellError> {
f64::from_value(v).map(|float| float as f32)
}
}
impl FromValue for f64 {
fn from_value(v: Value) -> Result<Self, ShellError> {
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 { impl FromValue for i64 {
@ -42,128 +193,207 @@ impl FromValue for i64 {
Value::Duration { val, .. } => Ok(val), Value::Duration { val, .. } => Ok(val),
v => Err(ShellError::CantConvert { v => Err(ShellError::CantConvert {
to_type: "int".into(), to_type: Self::expected_type().to_string(),
from_type: v.get_type().to_string(), from_type: v.get_type().to_string(),
span: v.span(), span: v.span(),
help: None, help: None,
}), }),
} }
} }
fn expected_type() -> Type {
Type::Int
}
} }
impl FromValue for Spanned<f64> { macro_rules! impl_from_value_for_int {
($type:ty) => {
impl FromValue for $type {
fn from_value(v: Value) -> Result<Self, ShellError> {
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<Self, ShellError> {
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<u8>,
// Rust would find two possible implementations then, Vec<u8> and Vec<T = u8>,
// and wouldn't compile.
// The blanket implementation for Vec<T> 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<Self, ShellError> {
match v {
Value::Nothing { .. } => Ok(()),
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::Nothing
}
}
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<Self, ShellError> { fn from_value(v: Value) -> Result<Self, ShellError> {
let span = v.span(); let span = v.span();
match v { match v {
Value::Int { val, .. } => Ok(Spanned { Value::List { vals, .. } => {
item: val as f64, let mut deque = VecDeque::from(vals);
span,
}),
Value::Float { val, .. } => Ok(Spanned { item: val, span }),
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 { v => Err(ShellError::CantConvert {
to_type: "float".into(), to_type: Self::expected_type().to_string(),
from_type: v.get_type().to_string(), from_type: v.get_type().to_string(),
span: v.span(), span: v.span(),
help: None, help: None,
}), }),
} }
} }
fn expected_type() -> Type {
Type::Custom(
format!(
$template,
$($t::expected_type()),*
)
.into_boxed_str(),
)
}
}
};
} }
impl FromValue for f64 { // 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<Self, ShellError> { fn from_value(v: Value) -> Result<Self, ShellError> {
match v { match v {
Value::Float { val, .. } => Ok(val), Value::String { val, .. } => Ok(val.into()),
Value::Int { val, .. } => Ok(val as f64),
v => Err(ShellError::CantConvert { v => Err(ShellError::CantConvert {
to_type: "float".into(), to_type: Self::expected_type().to_string(),
from_type: v.get_type().to_string(), from_type: v.get_type().to_string(),
span: v.span(), span: v.span(),
help: None, help: None,
}), }),
} }
} }
}
impl FromValue for Spanned<usize> { fn expected_type() -> Type {
fn from_value(v: Value) -> Result<Self, ShellError> { Type::String
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<Self, ShellError> {
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,
}),
}
} }
} }
@ -174,176 +404,111 @@ impl FromValue for String {
Value::CellPath { val, .. } => Ok(val.to_string()), Value::CellPath { val, .. } => Ok(val.to_string()),
Value::String { val, .. } => Ok(val), Value::String { val, .. } => Ok(val),
v => Err(ShellError::CantConvert { v => Err(ShellError::CantConvert {
to_type: "string".into(), to_type: Self::expected_type().to_string(),
from_type: v.get_type().to_string(), from_type: v.get_type().to_string(),
span: v.span(), span: v.span(),
help: None, help: None,
}), }),
} }
} }
}
impl FromValue for Spanned<String> { fn expected_type() -> Type {
fn from_value(v: Value) -> Result<Self, ShellError> { Type::String
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,
})
} }
} }
impl FromValue for NuGlob { // This impl is different from Vec<T> as it reads from Value::Binary and
// Value::String instead of Value::List.
// This also denies implementing FromValue for u8.
impl FromValue for Vec<u8> {
fn from_value(v: Value) -> Result<Self, ShellError> { fn from_value(v: Value) -> Result<Self, ShellError> {
// FIXME: we may want to fail a little nicer here
match v { match v {
Value::CellPath { val, .. } => Ok(NuGlob::Expand(val.to_string())), Value::Binary { val, .. } => Ok(val),
Value::String { val, .. } => Ok(NuGlob::DoNotExpand(val)), Value::String { val, .. } => Ok(val.into_bytes()),
Value::Glob {
val,
no_expand: quoted,
..
} => {
if quoted {
Ok(NuGlob::DoNotExpand(val))
} else {
Ok(NuGlob::Expand(val))
}
}
v => Err(ShellError::CantConvert { v => Err(ShellError::CantConvert {
to_type: "string".into(), to_type: Self::expected_type().to_string(),
from_type: v.get_type().to_string(), from_type: v.get_type().to_string(),
span: v.span(), span: v.span(),
help: None, help: None,
}), }),
} }
} }
}
impl FromValue for Spanned<NuGlob> { fn expected_type() -> Type {
fn from_value(v: Value) -> Result<Self, ShellError> { Type::Binary
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,
})
} }
} }
impl FromValue for Vec<String> { // Blanket std Implementations
impl<T> FromValue for Option<T>
where
T: FromValue,
{
fn from_value(v: Value) -> Result<Self, ShellError> { fn from_value(v: Value) -> Result<Self, ShellError> {
// FIXME: we may want to fail a little nicer here
match v { match v {
Value::List { vals, .. } => vals Value::Nothing { .. } => Ok(None),
v => T::from_value(v).map(Option::Some),
}
}
fn expected_type() -> Type {
T::expected_type()
}
}
impl<V> FromValue for HashMap<String, V>
where
V: FromValue,
{
fn from_value(v: Value) -> Result<Self, ShellError> {
let record = v.into_record()?;
let items: Result<Vec<(String, V)>, ShellError> = record
.into_iter() .into_iter()
.map(|val| match val { .map(|(k, v)| Ok((k, V::from_value(v)?)))
Value::String { val, .. } => Ok(val), .collect();
c => Err(ShellError::CantConvert { Ok(HashMap::from_iter(items?))
to_type: "string".into(),
from_type: c.get_type().to_string(),
span: c.span(),
help: None,
}),
})
.collect::<Result<Vec<String>, ShellError>>(),
v => Err(ShellError::CantConvert {
to_type: "string".into(),
from_type: v.get_type().to_string(),
span: v.span(),
help: None,
}),
} }
fn expected_type() -> Type {
Type::Record(vec![].into_boxed_slice())
} }
} }
impl FromValue for Vec<Spanned<String>> { impl<T> FromValue for Vec<T>
fn from_value(v: Value) -> Result<Self, ShellError> { where
// FIXME: we may want to fail a little nicer here T: FromValue,
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::<Result<Vec<Spanned<String>>, ShellError>>(),
v => Err(ShellError::CantConvert {
to_type: "string".into(),
from_type: v.get_type().to_string(),
span: v.span(),
help: None,
}),
}
}
}
impl FromValue for Vec<bool> {
fn from_value(v: Value) -> Result<Self, ShellError> { fn from_value(v: Value) -> Result<Self, ShellError> {
match v { match v {
Value::List { vals, .. } => vals Value::List { vals, .. } => vals
.into_iter() .into_iter()
.map(|val| match val { .map(|v| T::from_value(v))
Value::Bool { val, .. } => Ok(val), .collect::<Result<Vec<T>, ShellError>>(),
c => Err(ShellError::CantConvert {
to_type: "bool".into(),
from_type: c.get_type().to_string(),
span: c.span(),
help: None,
}),
})
.collect::<Result<Vec<bool>, ShellError>>(),
v => Err(ShellError::CantConvert { v => Err(ShellError::CantConvert {
to_type: "bool".into(), to_type: Self::expected_type().to_string(),
from_type: v.get_type().to_string(), from_type: v.get_type().to_string(),
span: v.span(), span: v.span(),
help: None, 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<Self, ShellError> {
Ok(v)
}
fn expected_type() -> Type {
Type::Any
}
} }
impl FromValue for CellPath { impl FromValue for CellPath {
@ -372,36 +537,25 @@ impl FromValue for CellPath {
} }
} }
x => Err(ShellError::CantConvert { x => Err(ShellError::CantConvert {
to_type: "cell path".into(), to_type: Self::expected_type().to_string(),
from_type: x.get_type().to_string(), from_type: x.get_type().to_string(),
span, span,
help: None, help: None,
}), }),
} }
} }
}
impl FromValue for bool { fn expected_type() -> Type {
fn from_value(v: Value) -> Result<Self, ShellError> { Type::CellPath
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,
}),
}
} }
} }
impl FromValue for Spanned<bool> { impl FromValue for Closure {
fn from_value(v: Value) -> Result<Self, ShellError> { fn from_value(v: Value) -> Result<Self, ShellError> {
let span = v.span();
match v { match v {
Value::Bool { val, .. } => Ok(Spanned { item: val, span }), Value::Closure { val, .. } => Ok(*val),
v => Err(ShellError::CantConvert { v => Err(ShellError::CantConvert {
to_type: "bool".into(), to_type: Self::expected_type().to_string(),
from_type: v.get_type().to_string(), from_type: v.get_type().to_string(),
span: v.span(), span: v.span(),
help: None, help: None,
@ -415,28 +569,48 @@ impl FromValue for DateTime<FixedOffset> {
match v { match v {
Value::Date { val, .. } => Ok(val), Value::Date { val, .. } => Ok(val),
v => Err(ShellError::CantConvert { v => Err(ShellError::CantConvert {
to_type: "date".into(), to_type: Self::expected_type().to_string(),
from_type: v.get_type().to_string(), from_type: v.get_type().to_string(),
span: v.span(), span: v.span(),
help: None, help: None,
}), }),
} }
} }
fn expected_type() -> Type {
Type::Date
}
} }
impl FromValue for Spanned<DateTime<FixedOffset>> { impl FromValue for NuGlob {
fn from_value(v: Value) -> Result<Self, ShellError> { fn from_value(v: Value) -> Result<Self, ShellError> {
let span = v.span(); // FIXME: we may want to fail a little nicer here
match v { 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 { v => Err(ShellError::CantConvert {
to_type: "date".into(), to_type: Self::expected_type().to_string(),
from_type: v.get_type().to_string(), from_type: v.get_type().to_string(),
span: v.span(), span: v.span(),
help: None, help: None,
}), }),
} }
} }
fn expected_type() -> Type {
Type::String
}
} }
impl FromValue for Range { impl FromValue for Range {
@ -444,94 +618,16 @@ impl FromValue for Range {
match v { match v {
Value::Range { val, .. } => Ok(*val), Value::Range { val, .. } => Ok(*val),
v => Err(ShellError::CantConvert { v => Err(ShellError::CantConvert {
to_type: "range".into(), to_type: Self::expected_type().to_string(),
from_type: v.get_type().to_string(), from_type: v.get_type().to_string(),
span: v.span(), span: v.span(),
help: None, help: None,
}), }),
} }
} }
}
impl FromValue for Spanned<Range> { fn expected_type() -> Type {
fn from_value(v: Value) -> Result<Self, ShellError> { Type::Range
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<u8> {
fn from_value(v: Value) -> Result<Self, ShellError> {
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<Vec<u8>> {
fn from_value(v: Value) -> Result<Self, ShellError> {
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<PathBuf> {
fn from_value(v: Value) -> Result<Self, ShellError> {
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<Value> {
fn from_value(v: Value) -> Result<Self, ShellError> {
// 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,
}),
}
} }
} }
@ -540,7 +636,7 @@ impl FromValue for Record {
match v { match v {
Value::Record { val, .. } => Ok(val.into_owned()), Value::Record { val, .. } => Ok(val.into_owned()),
v => Err(ShellError::CantConvert { v => Err(ShellError::CantConvert {
to_type: "Record".into(), to_type: Self::expected_type().to_string(),
from_type: v.get_type().to_string(), from_type: v.get_type().to_string(),
span: v.span(), span: v.span(),
help: None, help: None,
@ -549,31 +645,39 @@ impl FromValue for Record {
} }
} }
impl FromValue for Closure { // Blanket Nu Implementations
fn from_value(v: Value) -> Result<Self, ShellError> {
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,
}),
}
}
}
impl FromValue for Spanned<Closure> { impl<T> FromValue for Spanned<T>
where
T: FromValue,
{
fn from_value(v: Value) -> Result<Self, ShellError> { fn from_value(v: Value) -> Result<Self, ShellError> {
let span = v.span(); let span = v.span();
match v { Ok(Spanned {
Value::Closure { val, .. } => Ok(Spanned { item: *val, span }), item: T::from_value(v)?,
v => Err(ShellError::CantConvert { span,
to_type: "Closure".into(), })
from_type: v.get_type().to_string(),
span: v.span(),
help: None,
}),
} }
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())
);
} }
} }

View File

@ -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<T, const N: usize> 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<T> IntoValue for Vec<T>
where
T: IntoValue,
{
fn into_value(self, span: Span) -> Value {
Value::list(self.into_iter().map(|v| v.into_value(span)).collect(), span)
}
}
impl<T> IntoValue for Option<T>
where
T: IntoValue,
{
fn into_value(self, span: Span) -> Value {
match self {
Some(v) => v.into_value(span),
None => Value::nothing(span),
}
}
}
impl<V> IntoValue for HashMap<String, V>
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<String>` 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<ShellError>
/// Tries to convert the given value into a `Value`.
fn try_into_value(self, span: Span) -> Result<Value, ShellError>;
}
impl<T> TryIntoValue for T
where
T: IntoValue,
{
fn try_into_value(self, span: Span) -> Result<Value, ShellError> {
Ok(self.into_value(span))
}
}

View File

@ -4,7 +4,10 @@ mod filesize;
mod from; mod from;
mod from_value; mod from_value;
mod glob; mod glob;
mod into_value;
mod range; mod range;
#[cfg(test)]
mod test_derive;
pub mod record; pub mod record;
pub use custom_value::CustomValue; pub use custom_value::CustomValue;
@ -12,6 +15,7 @@ pub use duration::*;
pub use filesize::*; pub use filesize::*;
pub use from_value::FromValue; pub use from_value::FromValue;
pub use glob::*; pub use glob::*;
pub use into_value::{IntoValue, TryIntoValue};
pub use range::{FloatRange, IntRange, Range}; pub use range::{FloatRange, IntRange, Range};
pub use record::Record; pub use record::Record;
@ -1089,7 +1093,7 @@ impl Value {
} else { } else {
return Err(ShellError::CantFindColumn { return Err(ShellError::CantFindColumn {
col_name: column_name.clone(), col_name: column_name.clone(),
span: *origin_span, span: Some(*origin_span),
src_span: span, src_span: span,
}); });
} }
@ -1126,7 +1130,7 @@ impl Value {
} else { } else {
Err(ShellError::CantFindColumn { Err(ShellError::CantFindColumn {
col_name: column_name.clone(), col_name: column_name.clone(),
span: *origin_span, span: Some(*origin_span),
src_span: val_span, src_span: val_span,
}) })
} }
@ -1136,7 +1140,7 @@ impl Value {
} }
_ => Err(ShellError::CantFindColumn { _ => Err(ShellError::CantFindColumn {
col_name: column_name.clone(), col_name: column_name.clone(),
span: *origin_span, span: Some(*origin_span),
src_span: val_span, src_span: val_span,
}), }),
} }
@ -1237,7 +1241,7 @@ impl Value {
v => { v => {
return Err(ShellError::CantFindColumn { return Err(ShellError::CantFindColumn {
col_name: col_name.clone(), col_name: col_name.clone(),
span: *span, span: Some(*span),
src_span: v.span(), src_span: v.span(),
}); });
} }
@ -1262,7 +1266,7 @@ impl Value {
v => { v => {
return Err(ShellError::CantFindColumn { return Err(ShellError::CantFindColumn {
col_name: col_name.clone(), col_name: col_name.clone(),
span: *span, span: Some(*span),
src_span: v.span(), src_span: v.span(),
}); });
} }
@ -1342,7 +1346,7 @@ impl Value {
} else { } else {
return Err(ShellError::CantFindColumn { return Err(ShellError::CantFindColumn {
col_name: col_name.clone(), col_name: col_name.clone(),
span: *span, span: Some(*span),
src_span: v_span, src_span: v_span,
}); });
} }
@ -1351,7 +1355,7 @@ impl Value {
v => { v => {
return Err(ShellError::CantFindColumn { return Err(ShellError::CantFindColumn {
col_name: col_name.clone(), col_name: col_name.clone(),
span: *span, span: Some(*span),
src_span: v.span(), src_span: v.span(),
}); });
} }
@ -1364,7 +1368,7 @@ impl Value {
} else { } else {
return Err(ShellError::CantFindColumn { return Err(ShellError::CantFindColumn {
col_name: col_name.clone(), col_name: col_name.clone(),
span: *span, span: Some(*span),
src_span: v_span, src_span: v_span,
}); });
} }
@ -1373,7 +1377,7 @@ impl Value {
v => { v => {
return Err(ShellError::CantFindColumn { return Err(ShellError::CantFindColumn {
col_name: col_name.clone(), col_name: col_name.clone(),
span: *span, span: Some(*span),
src_span: v.span(), src_span: v.span(),
}); });
} }
@ -1427,7 +1431,7 @@ impl Value {
if record.to_mut().remove(col_name).is_none() && !optional { if record.to_mut().remove(col_name).is_none() && !optional {
return Err(ShellError::CantFindColumn { return Err(ShellError::CantFindColumn {
col_name: col_name.clone(), col_name: col_name.clone(),
span: *span, span: Some(*span),
src_span: v_span, src_span: v_span,
}); });
} }
@ -1435,7 +1439,7 @@ impl Value {
v => { v => {
return Err(ShellError::CantFindColumn { return Err(ShellError::CantFindColumn {
col_name: col_name.clone(), col_name: col_name.clone(),
span: *span, span: Some(*span),
src_span: v.span(), src_span: v.span(),
}); });
} }
@ -1447,7 +1451,7 @@ impl Value {
if record.to_mut().remove(col_name).is_none() && !optional { if record.to_mut().remove(col_name).is_none() && !optional {
return Err(ShellError::CantFindColumn { return Err(ShellError::CantFindColumn {
col_name: col_name.clone(), col_name: col_name.clone(),
span: *span, span: Some(*span),
src_span: v_span, src_span: v_span,
}); });
} }
@ -1455,7 +1459,7 @@ impl Value {
} }
v => Err(ShellError::CantFindColumn { v => Err(ShellError::CantFindColumn {
col_name: col_name.clone(), col_name: col_name.clone(),
span: *span, span: Some(*span),
src_span: v.span(), src_span: v.span(),
}), }),
}, },
@ -1504,7 +1508,7 @@ impl Value {
} else if !optional { } else if !optional {
return Err(ShellError::CantFindColumn { return Err(ShellError::CantFindColumn {
col_name: col_name.clone(), col_name: col_name.clone(),
span: *span, span: Some(*span),
src_span: v_span, src_span: v_span,
}); });
} }
@ -1512,7 +1516,7 @@ impl Value {
v => { v => {
return Err(ShellError::CantFindColumn { return Err(ShellError::CantFindColumn {
col_name: col_name.clone(), col_name: col_name.clone(),
span: *span, span: Some(*span),
src_span: v.span(), src_span: v.span(),
}); });
} }
@ -1526,7 +1530,7 @@ impl Value {
} else if !optional { } else if !optional {
return Err(ShellError::CantFindColumn { return Err(ShellError::CantFindColumn {
col_name: col_name.clone(), col_name: col_name.clone(),
span: *span, span: Some(*span),
src_span: v_span, src_span: v_span,
}); });
} }
@ -1534,7 +1538,7 @@ impl Value {
} }
v => Err(ShellError::CantFindColumn { v => Err(ShellError::CantFindColumn {
col_name: col_name.clone(), col_name: col_name.clone(),
span: *span, span: Some(*span),
src_span: v.span(), src_span: v.span(),
}), }),
}, },

View File

@ -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<T> 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<T>
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<u32>,
none: Option<u32>,
vec: Vec<T>,
string: String,
hashmap: HashMap<String, u32>,
nested: Nestee,
}
#[derive(IntoValue, FromValue, Debug, PartialEq)]
struct Nestee {
u32: u32,
some: Option<u32>,
none: Option<u32>,
}
impl NamedFieldsStruct<u32> {
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::<u32>::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<u32>, _> = 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<u32>, _> = NamedFieldsStruct::from_value(value);
assert!(res.is_err());
}
#[derive(IntoValue, FromValue, Debug, PartialEq)]
struct UnnamedFieldsStruct<T>(u32, String, T)
where
T: IntoValue + FromValue;
impl UnnamedFieldsStruct<f64> {
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::<f64>::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<f64>, _> = 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<f64>, _> = 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"]
}

View File

@ -87,7 +87,7 @@ impl CustomValue for CoolCustomValue {
} else { } else {
Err(ShellError::CantFindColumn { Err(ShellError::CantFindColumn {
col_name: column_name, col_name: column_name,
span: path_span, span: Some(path_span),
src_span: self_span, src_span: self_span,
}) })
} }