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.
2024-06-18 01:05:11 +02:00
|
|
|
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,
|
Allow missing fields in derived `FromValue::from_value` calls (#13206)
# Description
In #13031 I added the derive macros for `FromValue` and `IntoValue`. In
that implementation, in particular for structs with named fields, it was
not possible to omit fields while loading them from a value, when the
field is an `Option`. This PR adds extra handling for this behavior, so
if a field is an `Option` and that field is missing in the `Value`, then
the field becomes `None`. This behavior is also tested in
`nu_protocol::value::test_derive::missing_options`.
# User-Facing Changes
When using structs for options or similar, users can now just emit
fields in the record and the derive `from_value` method will be able to
understand this, if the struct has an `Option` type for that field.
# Tests + Formatting
- :green_circle: `toolkit fmt`
- :green_circle: `toolkit clippy`
- :green_circle: `toolkit test`
- :green_circle: `toolkit test stdlib`
# After Submitting
A showcase for this feature would be great, I tried to use the current
derive macro in a plugin of mine for a config but without this addition,
they are annoying to use. So, when this is done, I would add an example
for such plugin configs that may be loaded via `FromValue`.
2024-06-22 22:31:09 +02:00
|
|
|
Type,
|
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.
2024-06-18 01:05:11 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
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
|
|
|
|
/// })?,
|
|
|
|
/// )?,
|
Allow missing fields in derived `FromValue::from_value` calls (#13206)
# Description
In #13031 I added the derive macros for `FromValue` and `IntoValue`. In
that implementation, in particular for structs with named fields, it was
not possible to omit fields while loading them from a value, when the
field is an `Option`. This PR adds extra handling for this behavior, so
if a field is an `Option` and that field is missing in the `Value`, then
the field becomes `None`. This behavior is also tested in
`nu_protocol::value::test_derive::missing_options`.
# User-Facing Changes
When using structs for options or similar, users can now just emit
fields in the record and the derive `from_value` method will be able to
understand this, if the struct has an `Option` type for that field.
# Tests + Formatting
- :green_circle: `toolkit fmt`
- :green_circle: `toolkit clippy`
- :green_circle: `toolkit test`
- :green_circle: `toolkit test stdlib`
# After Submitting
A showcase for this feature would be great, I tried to use the current
derive macro in a plugin of mine for a config but without this addition,
they are annoying to use. So, when this is done, I would add an example
for such plugin configs that may be loaded via `FromValue`.
2024-06-22 22:31:09 +02:00
|
|
|
/// favorite_toy: record
|
|
|
|
/// .remove("favorite_toy")
|
|
|
|
/// .map(|v| <#ty as nu_protocol::FromValue>::from_value(v))
|
|
|
|
/// .transpose()?
|
|
|
|
/// .flatten(),
|
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.
2024-06-18 01:05:11 +02:00
|
|
|
/// })
|
|
|
|
/// }
|
|
|
|
/// }
|
|
|
|
/// ```
|
|
|
|
///
|
|
|
|
/// 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| {
|
|
|
|
let ident = field.ident.as_ref().expect("named has idents");
|
|
|
|
let ident_s = ident.to_string();
|
|
|
|
let ty = &field.ty;
|
Allow missing fields in derived `FromValue::from_value` calls (#13206)
# Description
In #13031 I added the derive macros for `FromValue` and `IntoValue`. In
that implementation, in particular for structs with named fields, it was
not possible to omit fields while loading them from a value, when the
field is an `Option`. This PR adds extra handling for this behavior, so
if a field is an `Option` and that field is missing in the `Value`, then
the field becomes `None`. This behavior is also tested in
`nu_protocol::value::test_derive::missing_options`.
# User-Facing Changes
When using structs for options or similar, users can now just emit
fields in the record and the derive `from_value` method will be able to
understand this, if the struct has an `Option` type for that field.
# Tests + Formatting
- :green_circle: `toolkit fmt`
- :green_circle: `toolkit clippy`
- :green_circle: `toolkit test`
- :green_circle: `toolkit test stdlib`
# After Submitting
A showcase for this feature would be great, I tried to use the current
derive macro in a plugin of mine for a config but without this addition,
they are annoying to use. So, when this is done, I would add an example
for such plugin configs that may be loaded via `FromValue`.
2024-06-22 22:31:09 +02:00
|
|
|
match type_is_option(ty) {
|
|
|
|
true => quote! {
|
|
|
|
#ident: record
|
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.
2024-06-18 01:05:11 +02:00
|
|
|
.remove(#ident_s)
|
Allow missing fields in derived `FromValue::from_value` calls (#13206)
# Description
In #13031 I added the derive macros for `FromValue` and `IntoValue`. In
that implementation, in particular for structs with named fields, it was
not possible to omit fields while loading them from a value, when the
field is an `Option`. This PR adds extra handling for this behavior, so
if a field is an `Option` and that field is missing in the `Value`, then
the field becomes `None`. This behavior is also tested in
`nu_protocol::value::test_derive::missing_options`.
# User-Facing Changes
When using structs for options or similar, users can now just emit
fields in the record and the derive `from_value` method will be able to
understand this, if the struct has an `Option` type for that field.
# Tests + Formatting
- :green_circle: `toolkit fmt`
- :green_circle: `toolkit clippy`
- :green_circle: `toolkit test`
- :green_circle: `toolkit test stdlib`
# After Submitting
A showcase for this feature would be great, I tried to use the current
derive macro in a plugin of mine for a config but without this addition,
they are annoying to use. So, when this is done, I would add an example
for such plugin configs that may be loaded via `FromValue`.
2024-06-22 22:31:09 +02:00
|
|
|
.map(|v| <#ty as nu_protocol::FromValue>::from_value(v))
|
|
|
|
.transpose()?
|
|
|
|
.flatten()
|
|
|
|
},
|
|
|
|
|
|
|
|
false => 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
|
|
|
|
})?,
|
|
|
|
)?
|
|
|
|
},
|
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.
2024-06-18 01:05:11 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
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
|
|
|
|
})
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
Allow missing fields in derived `FromValue::from_value` calls (#13206)
# Description
In #13031 I added the derive macros for `FromValue` and `IntoValue`. In
that implementation, in particular for structs with named fields, it was
not possible to omit fields while loading them from a value, when the
field is an `Option`. This PR adds extra handling for this behavior, so
if a field is an `Option` and that field is missing in the `Value`, then
the field becomes `None`. This behavior is also tested in
`nu_protocol::value::test_derive::missing_options`.
# User-Facing Changes
When using structs for options or similar, users can now just emit
fields in the record and the derive `from_value` method will be able to
understand this, if the struct has an `Option` type for that field.
# Tests + Formatting
- :green_circle: `toolkit fmt`
- :green_circle: `toolkit clippy`
- :green_circle: `toolkit test`
- :green_circle: `toolkit test stdlib`
# After Submitting
A showcase for this feature would be great, I tried to use the current
derive macro in a plugin of mine for a config but without this addition,
they are annoying to use. So, when this is done, I would add an example
for such plugin configs that may be loaded via `FromValue`.
2024-06-22 22:31:09 +02:00
|
|
|
|
|
|
|
const FULLY_QUALIFIED_OPTION: &str = "std::option::Option";
|
|
|
|
const PARTIALLY_QUALIFIED_OPTION: &str = "option::Option";
|
|
|
|
const PRELUDE_OPTION: &str = "Option";
|
|
|
|
|
|
|
|
/// Check if the field type is an `Option`.
|
|
|
|
///
|
|
|
|
/// This function checks if a given type is an `Option`.
|
|
|
|
/// We assume that an `Option` is [`std::option::Option`] because we can't see the whole code and
|
|
|
|
/// can't ask the compiler itself.
|
|
|
|
/// If the `Option` type isn't `std::option::Option`, the user will get a compile error due to a
|
|
|
|
/// type mismatch.
|
|
|
|
/// It's very unusual for people to override `Option`, so this should rarely be an issue.
|
|
|
|
///
|
|
|
|
/// When [rust#63084](https://github.com/rust-lang/rust/issues/63084) is resolved, we can use
|
|
|
|
/// [`std::any::type_name`] for a static assertion check to get a more direct error messages.
|
|
|
|
fn type_is_option(ty: &Type) -> bool {
|
|
|
|
let s = ty.to_token_stream().to_string();
|
|
|
|
s.starts_with(PRELUDE_OPTION)
|
|
|
|
|| s.starts_with(PARTIALLY_QUALIFIED_OPTION)
|
|
|
|
|| s.starts_with(FULLY_QUALIFIED_OPTION)
|
|
|
|
}
|