diff --git a/crates/nu-derive-value/src/from.rs b/crates/nu-derive-value/src/from.rs index e37c117637..0e1a9d2468 100644 --- a/crates/nu-derive-value/src/from.rs +++ b/crates/nu-derive-value/src/from.rs @@ -31,7 +31,7 @@ pub fn derive_from_value(input: TokenStream2) -> Result Ok(enum_from_value(input.ident, data_enum, input.generics)), + Data::Enum(data_enum) => Ok(derive_enum_from_value(input.ident, data_enum, input.generics)), Data::Union(_) => Err(DeriveError::Unsupported), } } @@ -50,67 +50,7 @@ fn derive_struct_from_value(ident: Ident, data: DataStruct, generics: Generics) } fn struct_from_value(data: &DataStruct) -> TokenStream2 { - let body = match &data.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! { - #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: call_span, - src_span: span - })?, - call_span, - )? - } - }); - quote! { - let span = v.span(); - let mut record = v.into_record()?; - std::result::Result::Ok(Self {#(#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: call_span, - src_span: span - })?, - call_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(#(#fields),*)) - } - } - Fields::Unit => quote! { - match v { - nu_protocol::Value::Nothing {..} => Ok(Self), - v => std::result::Result::Err(nu_protocol::ShellError::CantConvert { - to_type: ::expected_type().to_string(), - from_type: v.get_type().to_string(), - span: v.span(), - help: std::option::Option::None - }) - } - }, - }; - + let body = fields_from_record(&data.fields, quote!(Self)); quote! { fn from_value( v: nu_protocol::Value, @@ -168,6 +108,128 @@ fn struct_expected_type(fields: &Fields) -> TokenStream2 { } } -fn enum_from_value(ident: Ident, data: DataEnum, generics: Generics) -> TokenStream2 { - todo!() +fn derive_enum_from_value(ident: Ident, data: DataEnum, generics: Generics) -> TokenStream2 { + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let from_value_impl = enum_from_value(&data); + // As variants are hard to type with the current type system, we use the + // default impl for `expected_type`. + quote! { + #[automatically_derived] + impl #impl_generics nu_protocol::FromValue for #ident #ty_generics #where_clause { + #from_value_impl + } + } +} + +fn enum_from_value(data: &DataEnum) -> TokenStream2 { + let arms = data.variants.iter().map(|variant| { + let ident = &variant.ident; + let ident_s = format!("{ident}").as_str().to_case(Case::Snake); + let fields = fields_from_record(&variant.fields, quote!(Self::#ident)); + quote!(#ident_s => {#fields}) + }); + + quote! { + fn from_value( + v: nu_protocol::Value, + call_span: nu_protocol::Span + ) -> std::result::Result { + let span = v.span(); + let mut record = v.into_record()?; + + let ty = record.remove("type").ok_or_else(|| nu_protocol::ShellError::CantFindColumn { + col_name: std::string::ToString::to_string("type"), + span: call_span, + src_span: span + })?; + let ty = ty.into_string()?; + + // TODO: make content optional if type refers to a unit variant + + let v = record.remove("content").ok_or_else(|| nu_protocol::ShellError::CantFindColumn { + col_name: std::string::ToString::to_string("content"), + span: call_span, + src_span: span + })?; + + match ty.as_str() { + #(#arms),* + _ => std::result::Result::Err(nu_protocol::ShellError::CantConvert { + to_type: std::string::ToString::to_string( + &::expected_type() + ), + from_type: ty, + span: span, + help: std::option::Option::None + }), + } + } + } +} + +fn fields_from_record( + 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: call_span, + src_span: span + })?, + call_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: call_span, + src_span: span + })?, + call_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(&::expected_type()), + from_type: std::string::ToString::to_string(&v.get_type()), + span: v.span(), + help: std::option::Option::None + }) + } + } + } } diff --git a/crates/nu-protocol/src/value/test_derive.rs b/crates/nu-protocol/src/value/test_derive.rs index e6a1543215..081478ebff 100644 --- a/crates/nu-protocol/src/value/test_derive.rs +++ b/crates/nu-protocol/src/value/test_derive.rs @@ -265,7 +265,7 @@ fn unit_struct_roundtrip() { assert_eq!(expected, actual); } -#[derive(IntoValue)] +#[derive(IntoValue, FromValue, Debug, PartialEq)] enum Enum { Unit, Tuple(u32, String), @@ -283,29 +283,40 @@ impl Enum { }, ] } + + fn value() -> Value { + Value::test_list(vec![ + Value::test_record(record! { + "type" => Value::test_string("unit") + }), + Value::test_record(record! { + "type" => Value::test_string("tuple"), + "content" => Value::test_list(vec![ + Value::test_int(12), + Value::test_string("Tuple variant") + ]) + }), + Value::test_record(record! { + "type" => Value::test_string("struct"), + "content" => Value::test_record(record! { + "a" => Value::test_int(34), + "b" => Value::test_string("Struct variant") + }) + }), + ]) + } } #[test] fn enum_into_value() { - let enums = Enum::make().into_value_unknown(); - let expected = Value::test_list(vec![ - Value::test_record(record! { - "type" => Value::test_string("unit") - }), - Value::test_record(record! { - "type" => Value::test_string("tuple"), - "content" => Value::test_list(vec![ - Value::test_int(12), - Value::test_string("Tuple variant") - ]) - }), - Value::test_record(record! { - "type" => Value::test_string("struct"), - "content" => Value::test_record(record! { - "a" => Value::test_int(34), - "b" => Value::test_string("Struct variant") - }) - }), - ]); - assert_eq!(enums, expected); + let expected = Enum::value(); + let actual = Enum::make().into_value_unknown(); + assert_eq!(expected, actual); +} + +#[test] +fn enum_from_value() { + let expected = Enum::make(); + let actual = <[Enum; 3]>::from_value(Enum::value(), Span::test_data()).unwrap(); + assert_eq!(expected, actual); }