mirror of
https://github.com/nushell/nushell
synced 2024-12-27 05:23:11 +00:00
Add derive macros for FromValue
and IntoValue
to ease the use of Value
s 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:
parent
3a6d8aac0b
commit
b79a2255d2
24 changed files with 2378 additions and 385 deletions
22
Cargo.lock
generated
22
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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(),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
21
crates/nu-derive-value/Cargo.toml
Normal file
21
crates/nu-derive-value/Cargo.toml
Normal 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 }
|
21
crates/nu-derive-value/LICENSE
Normal file
21
crates/nu-derive-value/LICENSE
Normal 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.
|
116
crates/nu-derive-value/src/attributes.rs
Normal file
116
crates/nu-derive-value/src/attributes.rs
Normal 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(())
|
||||||
|
}
|
82
crates/nu-derive-value/src/error.rs
Normal file
82
crates/nu-derive-value/src/error.rs
Normal 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"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
539
crates/nu-derive-value/src/from.rs
Normal file
539
crates/nu-derive-value/src/from.rs
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
266
crates/nu-derive-value/src/into.rs
Normal file
266
crates/nu-derive-value/src/into.rs
Normal 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)),
|
||||||
|
}
|
||||||
|
}
|
69
crates/nu-derive-value/src/lib.rs
Normal file
69
crates/nu-derive-value/src/lib.rs
Normal 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)
|
||||||
|
}
|
157
crates/nu-derive-value/src/tests.rs
Normal file
157
crates/nu-derive-value/src/tests.rs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
|
@ -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 }
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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::*;
|
||||||
|
|
File diff suppressed because it is too large
Load diff
196
crates/nu-protocol/src/value/into_value.rs
Normal file
196
crates/nu-protocol/src/value/into_value.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
386
crates/nu-protocol/src/value/test_derive.rs
Normal file
386
crates/nu-protocol/src/value/test_derive.rs
Normal 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"]
|
||||||
|
}
|
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue