mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 07:55:59 +02:00
Added record key renaming for derive macros IntoValue
and FromValue
(#13699)
# Description Using derived `IntoValue` and `FromValue` implementations on structs with named fields currently produce `Value::Record`s where each key is the key of the Rust struct. For records like the `$nu` constant, that won't work as this record uses `kebab-case` for it's keys. To accomodate this, I upgraded the `#[nu_value(rename_all = "...")]` helper attribute to also work on structs with named fields which will rename the keys via the same case conversion as the enums already have. # User-Facing Changes Users of these macros may choose different key styles for their in `Value` representation. # Tests + Formatting I added the same test suite as enums already have and updated the traits documentation with more examples that also pass the doc test. # After Submitting I played around with the `$nu` constant but got stuck at the point that these keys are kebab-cased, with this, I can play around more with it.
This commit is contained in:
@ -17,29 +17,36 @@ use std::{
|
||||
///
|
||||
/// # Derivable
|
||||
/// This trait can be used with `#[derive]`.
|
||||
///
|
||||
/// When derived on structs with named fields, it expects a [`Value::Record`] where each field of
|
||||
/// the struct maps to a corresponding field in the record.
|
||||
/// You can customize the case conversion of these field names by using
|
||||
/// `#[nu_value(rename_all = "...")]` on the struct.
|
||||
/// Supported case conversions include those provided by [`convert_case::Case`], such as
|
||||
/// "snake_case", "kebab-case", "PascalCase", and others.
|
||||
/// Additionally, all values accepted by
|
||||
/// [`#[serde(rename_all = "...")]`](https://serde.rs/container-attrs.html#rename_all) are valid here.
|
||||
/// If not set, the field names will match the original Rust field names as-is.
|
||||
///
|
||||
/// For structs with unnamed fields, it expects a [`Value::List`], and the fields are populated in
|
||||
/// the order they appear in the list.
|
||||
/// Unit structs expect a [`Value::Nothing`], as they contain no data.
|
||||
/// Unit structs expect a [`Value::Nothing`], as they contain no data.
|
||||
/// Attempting to convert from a non-matching `Value` type will result in an error.
|
||||
///
|
||||
/// Only enums with no fields may derive this trait.
|
||||
/// The expected value representation will be the name of the variant as a [`Value::String`].
|
||||
/// By default, variant names will be expected in ["snake_case"](convert_case::Case::Snake).
|
||||
/// You can customize the case conversion using `#[nu_value(rename_all = "kebab-case")]` on the enum.
|
||||
/// All deterministic and useful case conversions provided by [`convert_case::Case`] are supported
|
||||
/// by specifying the case name followed by "case".
|
||||
/// Also all values for
|
||||
/// [`#[serde(rename_all = "...")]`](https://serde.rs/container-attrs.html#rename_all) are valid
|
||||
/// here.
|
||||
///
|
||||
/// Additionally, you can use `#[nu_value(type_name = "...")]` in the derive macro to set a custom type name
|
||||
/// for `FromValue::expected_type`. This will result in a `Type::Custom` with the specified type name.
|
||||
/// This can be useful in situations where the default type name is not desired.
|
||||
///
|
||||
/// ```
|
||||
/// # use nu_protocol::{FromValue, Value, ShellError};
|
||||
/// # use nu_protocol::{FromValue, Value, ShellError, record, Span};
|
||||
/// #
|
||||
/// # let span = Span::unknown();
|
||||
/// #
|
||||
/// #[derive(FromValue, Debug, PartialEq)]
|
||||
/// #[nu_value(rename_all = "COBOL-CASE", type_name = "birb")]
|
||||
/// enum Bird {
|
||||
@ -57,6 +64,30 @@ use std::{
|
||||
/// &Bird::expected_type().to_string(),
|
||||
/// "birb"
|
||||
/// );
|
||||
///
|
||||
///
|
||||
/// #[derive(FromValue, PartialEq, Eq, Debug)]
|
||||
/// #[nu_value(rename_all = "kebab-case")]
|
||||
/// struct Person {
|
||||
/// first_name: String,
|
||||
/// last_name: String,
|
||||
/// age: u32,
|
||||
/// }
|
||||
///
|
||||
/// let value = Value::record(record! {
|
||||
/// "first-name" => Value::string("John", span),
|
||||
/// "last-name" => Value::string("Doe", span),
|
||||
/// "age" => Value::int(42, span),
|
||||
/// }, span);
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Person::from_value(value).unwrap(),
|
||||
/// Person {
|
||||
/// first_name: "John".into(),
|
||||
/// last_name: "Doe".into(),
|
||||
/// age: 42,
|
||||
/// }
|
||||
/// );
|
||||
/// ```
|
||||
pub trait FromValue: Sized {
|
||||
// TODO: instead of ShellError, maybe we could have a FromValueError that implements Into<ShellError>
|
||||
|
@ -10,6 +10,11 @@ use crate::{Record, ShellError, Span, Value};
|
||||
/// 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.
|
||||
/// By default, field names will be kept as-is, but you can customize the case conversion
|
||||
/// for all fields in a struct by using `#[nu_value(rename_all = "kebab-case")]` on the struct.
|
||||
/// All useful case options from [`convert_case::Case`] are supported, as well as the
|
||||
/// values allowed by [`#[serde(rename_all)]`](https://serde.rs/container-attrs.html#rename_all).
|
||||
///
|
||||
/// 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.
|
||||
@ -18,14 +23,12 @@ use crate::{Record, ShellError, Span, Value};
|
||||
/// 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};
|
||||
/// # use nu_protocol::{IntoValue, Value, Span, record};
|
||||
/// #
|
||||
/// # let span = Span::unknown();
|
||||
/// #
|
||||
/// #[derive(IntoValue)]
|
||||
/// #[nu_value(rename_all = "COBOL-CASE")]
|
||||
/// enum Bird {
|
||||
@ -35,9 +38,31 @@ use crate::{Record, ShellError, Span, Value};
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Bird::RiverDuck.into_value(Span::unknown()),
|
||||
/// Bird::RiverDuck.into_value(span),
|
||||
/// Value::test_string("RIVER-DUCK")
|
||||
/// );
|
||||
///
|
||||
///
|
||||
/// #[derive(IntoValue)]
|
||||
/// #[nu_value(rename_all = "kebab-case")]
|
||||
/// struct Person {
|
||||
/// first_name: String,
|
||||
/// last_name: String,
|
||||
/// age: u32,
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Person {
|
||||
/// first_name: "John".into(),
|
||||
/// last_name: "Doe".into(),
|
||||
/// age: 42,
|
||||
/// }.into_value(span),
|
||||
/// Value::record(record! {
|
||||
/// "first-name" => Value::string("John", span),
|
||||
/// "last-name" => Value::string("Doe", span),
|
||||
/// "age" => Value::int(42, span),
|
||||
/// }, span)
|
||||
/// );
|
||||
/// ```
|
||||
pub trait IntoValue: Sized {
|
||||
/// Converts the given value to a [`Value`].
|
||||
|
@ -384,62 +384,133 @@ fn enum_incorrect_type() {
|
||||
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
|
||||
}
|
||||
mod enum_rename_all {
|
||||
use super::*;
|
||||
use crate as nu_protocol;
|
||||
|
||||
impl $ident {
|
||||
fn make() -> [Self; 3] {
|
||||
[Self::AlphaOne, Self::BetaTwo, Self::CharlieThree]
|
||||
// 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
|
||||
}
|
||||
|
||||
fn value() -> Value {
|
||||
Value::test_list(vec![
|
||||
Value::test_string($a1),
|
||||
Value::test_string($b2),
|
||||
Value::test_string($c3),
|
||||
])
|
||||
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 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);
|
||||
})*}
|
||||
#[test]
|
||||
fn 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"]
|
||||
}
|
||||
}
|
||||
|
||||
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"]
|
||||
mod named_fields_struct_rename_all {
|
||||
use super::*;
|
||||
use crate as nu_protocol;
|
||||
|
||||
macro_rules! named_fields_struct_rename_all {
|
||||
($($ident:ident: $case:literal => [$a1:literal, $b2:literal, $c3:literal]),*) => {
|
||||
$(
|
||||
#[derive(Debug, PartialEq, IntoValue, FromValue)]
|
||||
#[nu_value(rename_all = $case)]
|
||||
struct $ident {
|
||||
alpha_one: (),
|
||||
beta_two: (),
|
||||
charlie_three: (),
|
||||
}
|
||||
|
||||
impl $ident {
|
||||
fn make() -> Self {
|
||||
Self {
|
||||
alpha_one: (),
|
||||
beta_two: (),
|
||||
charlie_three: (),
|
||||
}
|
||||
}
|
||||
|
||||
fn value() -> Value {
|
||||
Value::test_record(record! {
|
||||
$a1 => Value::test_nothing(),
|
||||
$b2 => Value::test_nothing(),
|
||||
$c3 => Value::test_nothing(),
|
||||
})
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
||||
#[test]
|
||||
fn into_value() {$({
|
||||
let expected = $ident::value();
|
||||
let actual = $ident::make().into_test_value();
|
||||
assert_eq!(expected, actual);
|
||||
})*}
|
||||
|
||||
#[test]
|
||||
fn from_value() {$({
|
||||
let expected = $ident::make();
|
||||
let actual = $ident::from_value($ident::value()).unwrap();
|
||||
assert_eq!(expected, actual);
|
||||
})*}
|
||||
}
|
||||
}
|
||||
|
||||
named_fields_struct_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"]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoValue, FromValue, Debug, PartialEq)]
|
||||
|
Reference in New Issue
Block a user