mirror of
https://github.com/nushell/nushell.git
synced 2025-04-01 11:46:20 +02:00
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 - 🟢 `toolkit fmt` - 🟢 `toolkit clippy` - 🟢 `toolkit test` - 🟢 `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`.
This commit is contained in:
parent
b6bdadbc6f
commit
9b7f899410
@ -3,6 +3,7 @@ use proc_macro2::TokenStream as TokenStream2;
|
|||||||
use quote::{quote, ToTokens};
|
use quote::{quote, ToTokens};
|
||||||
use syn::{
|
use syn::{
|
||||||
spanned::Spanned, Attribute, Data, DataEnum, DataStruct, DeriveInput, Fields, Generics, Ident,
|
spanned::Spanned, Attribute, Data, DataEnum, DataStruct, DeriveInput, Fields, Generics, Ident,
|
||||||
|
Type,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::attributes::{self, ContainerAttributes};
|
use crate::attributes::{self, ContainerAttributes};
|
||||||
@ -116,15 +117,11 @@ fn derive_struct_from_value(
|
|||||||
/// src_span: span
|
/// src_span: span
|
||||||
/// })?,
|
/// })?,
|
||||||
/// )?,
|
/// )?,
|
||||||
/// favorite_toy: <Option<String> as nu_protocol::FromValue>::from_value(
|
/// favorite_toy: record
|
||||||
/// record
|
|
||||||
/// .remove("favorite_toy")
|
/// .remove("favorite_toy")
|
||||||
/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
|
/// .map(|v| <#ty as nu_protocol::FromValue>::from_value(v))
|
||||||
/// col_name: std::string::ToString::to_string("favorite_toy"),
|
/// .transpose()?
|
||||||
/// span: std::option::Option::None,
|
/// .flatten(),
|
||||||
/// src_span: span
|
|
||||||
/// })?,
|
|
||||||
/// )?,
|
|
||||||
/// })
|
/// })
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
@ -480,11 +477,19 @@ fn parse_value_via_fields(fields: &Fields, self_ident: impl ToTokens) -> TokenSt
|
|||||||
match fields {
|
match fields {
|
||||||
Fields::Named(fields) => {
|
Fields::Named(fields) => {
|
||||||
let fields = fields.named.iter().map(|field| {
|
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 = field.ident.as_ref().expect("named has idents");
|
||||||
let ident_s = ident.to_string();
|
let ident_s = ident.to_string();
|
||||||
let ty = &field.ty;
|
let ty = &field.ty;
|
||||||
quote! {
|
match type_is_option(ty) {
|
||||||
|
true => quote! {
|
||||||
|
#ident: record
|
||||||
|
.remove(#ident_s)
|
||||||
|
.map(|v| <#ty as nu_protocol::FromValue>::from_value(v))
|
||||||
|
.transpose()?
|
||||||
|
.flatten()
|
||||||
|
},
|
||||||
|
|
||||||
|
false => quote! {
|
||||||
#ident: <#ty as nu_protocol::FromValue>::from_value(
|
#ident: <#ty as nu_protocol::FromValue>::from_value(
|
||||||
record
|
record
|
||||||
.remove(#ident_s)
|
.remove(#ident_s)
|
||||||
@ -494,6 +499,7 @@ fn parse_value_via_fields(fields: &Fields, self_ident: impl ToTokens) -> TokenSt
|
|||||||
src_span: span
|
src_span: span
|
||||||
})?,
|
})?,
|
||||||
)?
|
)?
|
||||||
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
quote! {
|
quote! {
|
||||||
@ -537,3 +543,25 @@ fn parse_value_via_fields(fields: &Fields, self_ident: impl ToTokens) -> TokenSt
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
@ -171,6 +171,62 @@ fn named_fields_struct_incorrect_type() {
|
|||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(IntoValue, FromValue, Debug, PartialEq, Default)]
|
||||||
|
struct ALotOfOptions {
|
||||||
|
required: bool,
|
||||||
|
float: Option<f64>,
|
||||||
|
int: Option<i64>,
|
||||||
|
value: Option<Value>,
|
||||||
|
nested: Option<Nestee>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_options() {
|
||||||
|
let value = Value::test_record(Record::new());
|
||||||
|
let res: Result<ALotOfOptions, _> = ALotOfOptions::from_value(value);
|
||||||
|
assert!(res.is_err());
|
||||||
|
|
||||||
|
let value = Value::test_record(record! {"required" => Value::test_bool(true)});
|
||||||
|
let expected = ALotOfOptions {
|
||||||
|
required: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let actual = ALotOfOptions::from_value(value).unwrap();
|
||||||
|
assert_eq!(expected, actual);
|
||||||
|
|
||||||
|
let value = Value::test_record(record! {
|
||||||
|
"required" => Value::test_bool(true),
|
||||||
|
"float" => Value::test_float(std::f64::consts::PI),
|
||||||
|
});
|
||||||
|
let expected = ALotOfOptions {
|
||||||
|
required: true,
|
||||||
|
float: Some(std::f64::consts::PI),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let actual = ALotOfOptions::from_value(value).unwrap();
|
||||||
|
assert_eq!(expected, actual);
|
||||||
|
|
||||||
|
let value = Value::test_record(record! {
|
||||||
|
"required" => Value::test_bool(true),
|
||||||
|
"int" => Value::test_int(12),
|
||||||
|
"nested" => Value::test_record(record! {
|
||||||
|
"u32" => Value::test_int(34),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
let expected = ALotOfOptions {
|
||||||
|
required: true,
|
||||||
|
int: Some(12),
|
||||||
|
nested: Some(Nestee {
|
||||||
|
u32: 34,
|
||||||
|
some: None,
|
||||||
|
none: None,
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let actual = ALotOfOptions::from_value(value).unwrap();
|
||||||
|
assert_eq!(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(IntoValue, FromValue, Debug, PartialEq)]
|
#[derive(IntoValue, FromValue, Debug, PartialEq)]
|
||||||
struct UnnamedFieldsStruct<T>(u32, String, T)
|
struct UnnamedFieldsStruct<T>(u32, String, T)
|
||||||
where
|
where
|
||||||
|
Loading…
Reference in New Issue
Block a user