From bd8cde0baacd08f862f822ab341787134c06b6e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Vandecr=C3=A8me?= Date: Wed, 10 Sep 2025 20:11:57 +0200 Subject: [PATCH] Allow mapping between struct and tuple variants --- README.md | 12 +- src/enum_from/generator.rs | 311 ++++++++++++++--- src/enum_into/generator.rs | 315 +++++++++++++++--- src/idents.rs | 6 + src/lib.rs | 36 +- .../mapping_struct_to_tuple_fields.stderr | 5 - .../mapping_tuple_to_struct_fields.stderr | 5 - ...missing_annotation_for_struct_to_tuple.rs} | 2 +- ...sing_annotation_for_struct_to_tuple.stderr | 5 + ...missing_annotation_for_tuple_to_struct.rs} | 4 +- ...sing_annotation_for_tuple_to_struct.stderr | 5 + .../field/mixing_source_variant_kind.stderr | 6 +- .../pass/mapping_struct_to_tuple_fields.rs | 22 ++ .../pass/mapping_tuple_to_struct_fields.rs | 24 ++ .../mapping_struct_to_tuple_fields.stderr | 5 - .../mapping_tuple_to_struct_fields.stderr | 5 - ...missing_annotation_for_struct_to_tuple.rs} | 8 +- ...sing_annotation_for_struct_to_tuple.stderr | 5 + .../missing_annotation_for_tuple_to_struct.rs | 18 + ...sing_annotation_for_tuple_to_struct.stderr | 5 + .../field/mixing_target_variant_kind.stderr | 10 +- .../pass/mapping_struct_to_tuple_fields.rs | 24 ++ .../mapping_tuple_to_struct_fields.rs | 9 +- 23 files changed, 687 insertions(+), 160 deletions(-) delete mode 100644 tests/enum_from/compile_fail/field/mapping_struct_to_tuple_fields.stderr delete mode 100644 tests/enum_from/compile_fail/field/mapping_tuple_to_struct_fields.stderr rename tests/enum_from/compile_fail/field/{mapping_struct_to_tuple_fields.rs => missing_annotation_for_struct_to_tuple.rs} (77%) create mode 100644 tests/enum_from/compile_fail/field/missing_annotation_for_struct_to_tuple.stderr rename tests/enum_from/compile_fail/field/{mapping_tuple_to_struct_fields.rs => missing_annotation_for_tuple_to_struct.rs} (78%) create mode 100644 tests/enum_from/compile_fail/field/missing_annotation_for_tuple_to_struct.stderr create mode 100644 tests/enum_from/pass/mapping_struct_to_tuple_fields.rs create mode 100644 tests/enum_from/pass/mapping_tuple_to_struct_fields.rs delete mode 100644 tests/enum_into/compile_fail/field/mapping_struct_to_tuple_fields.stderr delete mode 100644 tests/enum_into/compile_fail/field/mapping_tuple_to_struct_fields.stderr rename tests/enum_into/compile_fail/field/{mapping_struct_to_tuple_fields.rs => missing_annotation_for_struct_to_tuple.rs} (67%) create mode 100644 tests/enum_into/compile_fail/field/missing_annotation_for_struct_to_tuple.stderr create mode 100644 tests/enum_into/compile_fail/field/missing_annotation_for_tuple_to_struct.rs create mode 100644 tests/enum_into/compile_fail/field/missing_annotation_for_tuple_to_struct.stderr create mode 100644 tests/enum_into/pass/mapping_struct_to_tuple_fields.rs rename tests/enum_into/{compile_fail/field => pass}/mapping_tuple_to_struct_fields.rs (65%) diff --git a/README.md b/README.md index 6617754..c4403d2 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,8 @@ enum Source { Record { name: String, value: i32, - } + }, + Point(i32, i32), } #[derive(EnumFrom)] @@ -121,7 +122,14 @@ enum Target { #[enum_from(Source::Record.name)] // Maps Source::Record.name to Target::Record.title title: String, value: i32, - } + }, + #[enum_from] + Point { + #[enum_from(Source::Point.0)] + x: i64, + #[enum_from(Source::Point.1)] + y: i64, + }, } ``` diff --git a/src/enum_from/generator.rs b/src/enum_from/generator.rs index 4e68d2e..3f59bd0 100644 --- a/src/enum_from/generator.rs +++ b/src/enum_from/generator.rs @@ -2,7 +2,7 @@ use std::collections::{BTreeMap, HashMap}; use proc_macro2::{Span, TokenStream}; use quote::quote; -use syn::{Fields, Variant}; +use syn::{Fields, FieldsNamed, FieldsUnnamed, Variant, spanned::Spanned}; use crate::{ enum_from::parser::{ @@ -22,25 +22,35 @@ pub struct EnumFromGenerator { struct VariantsMapping(HashMap); enum VariantMapping { - Unit { + UnitToUnit { target_variant: VariantIdent, }, - Tuple { + TupleToTuple { target_variant: VariantIdent, fields_mapping: HashMap, }, - Struct { + TupleToStruct { + target_variant: VariantIdent, + fields_mapping: HashMap, + }, + StructToStruct { target_variant: VariantIdent, fields_mapping: HashMap, }, + StructToTuple { + target_variant: VariantIdent, + fields_mapping: HashMap, + }, } impl VariantMapping { fn target_variant(&self) -> &VariantIdent { match self { - VariantMapping::Unit { target_variant } => target_variant, - VariantMapping::Tuple { target_variant, .. } => target_variant, - VariantMapping::Struct { target_variant, .. } => target_variant, + VariantMapping::UnitToUnit { target_variant } => target_variant, + VariantMapping::TupleToTuple { target_variant, .. } => target_variant, + VariantMapping::TupleToStruct { target_variant, .. } => target_variant, + VariantMapping::StructToStruct { target_variant, .. } => target_variant, + VariantMapping::StructToTuple { target_variant, .. } => target_variant, } } } @@ -107,12 +117,12 @@ fn generate_match_arm( variant: &Variant, ) -> TokenStream { match (&variant.fields, variant_mapping) { - (Fields::Unit, VariantMapping::Unit { target_variant }) => { + (Fields::Unit, VariantMapping::UnitToUnit { target_variant }) => { quote! { #source_enum::#source_variant => #target_enum::#target_variant, } } ( Fields::Unnamed(fields), - VariantMapping::Tuple { + VariantMapping::TupleToTuple { target_variant, fields_mapping, }, @@ -134,9 +144,29 @@ fn generate_match_arm( #target_enum::#target_variant(#(#target_fields),*), } } + ( + Fields::Unnamed(fields), + VariantMapping::StructToTuple { + target_variant, + fields_mapping, + }, + ) => { + let (source_fields, target_fields): (Vec<_>, Vec<_>) = (0..fields.unnamed.len()) + .map(|field_target_pos| { + let source_ident = fields_mapping + .get(&field_target_pos) + .expect("fields_mapping exhaustiveness should have been checked"); + (quote! { #source_ident }, quote! { #source_ident.into() }) + }) + .unzip(); + quote! { + #source_enum::#source_variant { #(#source_fields),* } => + #target_enum::#target_variant(#(#target_fields),*), + } + } ( Fields::Named(fields), - VariantMapping::Struct { + VariantMapping::StructToStruct { target_variant, fields_mapping, }, @@ -165,6 +195,31 @@ fn generate_match_arm( #target_enum::#target_variant { #(#target_fields),* }, } } + ( + Fields::Named(_), + VariantMapping::TupleToStruct { + target_variant, + fields_mapping, + }, + ) => { + let (source_fields, target_fields): (Vec<_>, Vec<_>) = fields_mapping + .into_iter() + .map(|(target_ident, source_pos)| (source_pos, target_ident)) + .collect::>() + .into_values() + .map(|target_ident| { + ( + quote! { #target_ident }, + quote! { #target_ident: #target_ident.into() }, + ) + }) + .unzip(); + + quote! { + #source_enum::#source_variant(#(#source_fields),*) => + #target_enum::#target_variant { #(#target_fields),* }, + } + } (_, _) => panic!("Unexpected mixing of variant types"), } } @@ -224,47 +279,13 @@ impl TryFrom for EnumFromGenerator { )?; let fields = &target_variant.fields; let target_variant = VariantIdent(target_variant.ident.clone()); - let variant_mapping = match fields { - Fields::Unit => VariantMapping::Unit { target_variant }, - Fields::Unnamed(_) => VariantMapping::Tuple { - target_variant, - fields_mapping: fields_annotations - .into_iter() - .map(|target_to_source| match target_to_source { - ( - FieldRef::FieldPos(target_pos), - FieldAnnotation { - source_field: FieldRef::FieldPos(source_pos), - .. - }, - ) => Ok((target_pos, source_pos)), - (_, FieldAnnotation { field_span, .. }) => Err(syn::Error::new( - field_span, - "Unexpected mapping to named field for tuple variant", - )), - }) - .collect::>()?, - }, - Fields::Named(_) => VariantMapping::Struct { - target_variant, - fields_mapping: fields_annotations - .into_iter() - .map(|target_to_source| match target_to_source { - ( - FieldRef::FieldIdent(target_ident), - FieldAnnotation { - source_field: FieldRef::FieldIdent(source_ident), - .. - }, - ) => Ok((target_ident, source_ident)), - (_, FieldAnnotation { field_span, .. }) => Err(syn::Error::new( - field_span, - "Unexpected mapping to positional field for struct variant", - )), - }) - .collect::>()?, - }, - }; + let variant_mapping = compute_variant_mapping( + &source_enum, + &source_variant, + fields_annotations, + fields, + target_variant, + )?; variants_mapping.insert(source_variant, variant_mapping); } @@ -281,6 +302,194 @@ impl TryFrom for EnumFromGenerator { } } +fn compute_variant_mapping( + source_enum: &ContainerIdent, + source_variant: &VariantIdent, + fields_annotations: BTreeMap, + fields: &Fields, + target_variant: VariantIdent, +) -> syn::Result { + match ( + fields, + fields_annotations + .first_key_value() + .map(|(_, field_annotation)| &field_annotation.source_field), + ) { + (Fields::Unit, None) => Ok(VariantMapping::UnitToUnit { target_variant }), + (Fields::Unit, Some(_)) => panic!("A unit variant cannot have field annotations"), + (Fields::Unnamed(_), None) | (Fields::Unnamed(_), Some(FieldRef::FieldPos(_))) => { + compute_tuple_to_tuple_variant_mapping(fields_annotations, target_variant) + } + (Fields::Named(_), None) | (Fields::Named(_), Some(FieldRef::FieldIdent(_))) => { + compute_struct_to_struct_variant_mapping(fields_annotations, target_variant) + } + (Fields::Unnamed(fields), Some(FieldRef::FieldIdent(_))) => { + compute_struct_to_tuple_variant_mapping( + source_enum, + source_variant, + fields_annotations, + fields, + target_variant, + ) + } + (Fields::Named(fields), Some(FieldRef::FieldPos(_))) => { + compute_tuple_to_struct_variant_mapping( + source_enum, + source_variant, + fields_annotations, + fields, + target_variant, + ) + } + } +} + +fn compute_tuple_to_tuple_variant_mapping( + fields_annotations: BTreeMap, + target_variant: VariantIdent, +) -> syn::Result { + let fields_mapping = fields_annotations + .into_iter() + .map(|target_to_source| match target_to_source { + ( + FieldRef::FieldPos(target_pos), + FieldAnnotation { + source_field: FieldRef::FieldPos(source_pos), + .. + }, + ) => Ok((target_pos, source_pos)), + (_, FieldAnnotation { field_span, .. }) => Err(syn::Error::new( + field_span, + "Unexpected mapping to named field while another field mapped to a positional field.", + )), + }) + .collect::>()?; + + Ok(VariantMapping::TupleToTuple { + target_variant, + fields_mapping, + }) +} + +fn compute_struct_to_struct_variant_mapping( + fields_annotations: BTreeMap, + target_variant: VariantIdent, +) -> syn::Result { + let fields_mapping = fields_annotations + .into_iter() + .map(|target_to_source| match target_to_source { + ( + FieldRef::FieldIdent(target_ident), + FieldAnnotation { + source_field: FieldRef::FieldIdent(source_ident), + .. + }, + ) => Ok((target_ident, source_ident)), + (_, FieldAnnotation { field_span, .. }) => Err(syn::Error::new( + field_span, + "Unexpected mapping to positional field while another field mapped to a named field.", + )), + }) + .collect::>()?; + + Ok(VariantMapping::StructToStruct { + target_variant, + fields_mapping, + }) +} + +fn compute_struct_to_tuple_variant_mapping( + source_enum: &ContainerIdent, + source_variant: &VariantIdent, + fields_annotations: BTreeMap, + fields: &FieldsUnnamed, + target_variant: VariantIdent, +) -> syn::Result { + let fields_mapping = fields_annotations + .into_iter() + .map(|target_to_source| match target_to_source { + (FieldRef::FieldIdent(_), _) => { + panic!("Target is a tuple variant but got named fields") + }, + ( + FieldRef::FieldPos(target_pos), + FieldAnnotation { + source_field: FieldRef::FieldIdent(source_ident), + .. + }, + ) => Ok((target_pos, source_ident)), + (FieldRef::FieldPos(_), FieldAnnotation { source_field: FieldRef::FieldPos(_), field_span, .. }) => { + Err(syn::Error::new( + field_span, + "Unexpected mapping to positional field while another field mapped to a named field.", + )) + }, + }) + .collect::>>()?; + + for (pos, field) in fields.unnamed.iter().enumerate() { + if !fields_mapping.contains_key(&pos) { + Err(syn::Error::new( + field.span(), + format!( + "Missing required mapping to named field for {source_enum}::{source_variant}" + ), + ))?; + } + } + + Ok(VariantMapping::StructToTuple { + target_variant, + fields_mapping, + }) +} + +fn compute_tuple_to_struct_variant_mapping( + source_enum: &ContainerIdent, + source_variant: &VariantIdent, + fields_annotations: BTreeMap, + fields: &FieldsNamed, + target_variant: VariantIdent, +) -> syn::Result { + let fields_mapping = fields_annotations + .into_iter() + .map(|target_to_source| match target_to_source { + (FieldRef::FieldPos(_), _) => { + panic!("Target is a struct variant but got positional fields") + }, + ( + FieldRef::FieldIdent(target_ident), + FieldAnnotation { + source_field: FieldRef::FieldPos(source_pos), + .. + }, + ) => Ok((target_ident, source_pos)), + (FieldRef::FieldIdent(_), FieldAnnotation { source_field: FieldRef::FieldIdent(_), field_span, .. }) => Err(syn::Error::new( + field_span, + "Unexpected mapping to named field while another field mapped to a positional field.", + )), + }) + .collect::>>()?; + + for field in fields.named.iter() { + if !fields_mapping.contains_key(&FieldIdent( + field.ident.clone().expect("Named fields have idents"), + )) { + Err(syn::Error::new( + field.span(), + format!( + "Missing required mapping to named field for {source_enum}::{source_variant}" + ), + ))?; + } + } + + Ok(VariantMapping::TupleToStruct { + target_variant, + fields_mapping, + }) +} + fn check_unused_fields_annotations( source_enums: &HashMap, fields_annotations: HashMap, diff --git a/src/enum_into/generator.rs b/src/enum_into/generator.rs index 547a677..2c96fe7 100644 --- a/src/enum_into/generator.rs +++ b/src/enum_into/generator.rs @@ -2,7 +2,7 @@ use std::collections::{BTreeMap, HashMap}; use proc_macro2::{Span, TokenStream}; use quote::quote; -use syn::{Fields, Variant}; +use syn::{Fields, FieldsNamed, FieldsUnnamed, Variant, spanned::Spanned as _}; use crate::{ enum_into::parser::{ @@ -22,25 +22,35 @@ pub struct EnumIntoGenerator { struct VariantsMapping(HashMap>); enum VariantMapping { - Unit { + UnitToUnit { source_variant: VariantIdent, }, - Tuple { + TupleToTuple { source_variant: VariantIdent, fields_mapping: HashMap, }, - Struct { + TupleToStruct { + source_variant: VariantIdent, + fields_mapping: HashMap, + }, + StructToStruct { source_variant: VariantIdent, fields_mapping: HashMap, }, + StructToTuple { + source_variant: VariantIdent, + fields_mapping: HashMap, + }, } impl VariantMapping { fn source_variant(&self) -> &VariantIdent { match self { - VariantMapping::Unit { source_variant } => source_variant, - VariantMapping::Tuple { source_variant, .. } => source_variant, - VariantMapping::Struct { source_variant, .. } => source_variant, + VariantMapping::UnitToUnit { source_variant } => source_variant, + VariantMapping::TupleToTuple { source_variant, .. } => source_variant, + VariantMapping::TupleToStruct { source_variant, .. } => source_variant, + VariantMapping::StructToStruct { source_variant, .. } => source_variant, + VariantMapping::StructToTuple { source_variant, .. } => source_variant, } } } @@ -108,12 +118,12 @@ fn generate_match_arm( variant: &Variant, ) -> TokenStream { match (&variant.fields, variant_mapping) { - (Fields::Unit, VariantMapping::Unit { source_variant }) => quote! { - #source_enum::#source_variant => #target_enum::#target_variant, - }, + (Fields::Unit, VariantMapping::UnitToUnit { source_variant }) => { + quote! { #source_enum::#source_variant => #target_enum::#target_variant, } + } ( Fields::Unnamed(fields), - VariantMapping::Tuple { + VariantMapping::TupleToTuple { source_variant, fields_mapping, }, @@ -135,9 +145,32 @@ fn generate_match_arm( #target_enum::#target_variant(#(#target_fields),*), } } + ( + Fields::Unnamed(fields), + VariantMapping::TupleToStruct { + source_variant, + fields_mapping, + }, + ) => { + let (source_fields, target_fields): (Vec<_>, Vec<_>) = (0..fields.unnamed.len()) + .map(|field_source_pos| { + let target_ident = fields_mapping + .get(&field_source_pos) + .expect("fields_mapping exhaustiveness should have been checked"); + ( + quote! { #target_ident }, + quote! { #target_ident: #target_ident.into() }, + ) + }) + .unzip(); + quote! { + #source_enum::#source_variant(#(#source_fields),*) => + #target_enum::#target_variant { #(#target_fields),* }, + } + } ( Fields::Named(fields), - VariantMapping::Struct { + VariantMapping::StructToStruct { source_variant, fields_mapping, }, @@ -166,6 +199,26 @@ fn generate_match_arm( #target_enum::#target_variant { #(#target_fields),* }, } } + ( + Fields::Named(_), + VariantMapping::StructToTuple { + source_variant, + fields_mapping, + }, + ) => { + let (source_fields, target_fields): (Vec<_>, Vec<_>) = fields_mapping + .into_iter() + .map(|(source_ident, target_pos)| (target_pos, source_ident)) + .collect::>() + .into_values() + .map(|source_ident| (quote! { #source_ident }, quote! { #source_ident.into() })) + .unzip(); + + quote! { + #source_enum::#source_variant { #(#source_fields),* } => + #target_enum::#target_variant(#(#target_fields),*), + } + } (_, _) => panic!("Unexpected mixing of variant types"), } } @@ -224,47 +277,13 @@ impl TryFrom for EnumIntoGenerator { )?; let fields = &source_variant.fields; let source_variant = VariantIdent(source_variant.ident.clone()); - let variant_mapping = match fields { - Fields::Unit => VariantMapping::Unit { source_variant }, - Fields::Unnamed(_) => VariantMapping::Tuple { - source_variant, - fields_mapping: fields_annotations - .into_iter() - .map(|source_to_target| match source_to_target { - ( - FieldRef::FieldPos(source_pos), - FieldAnnotation { - target_field: FieldRef::FieldPos(target_pos), - .. - }, - ) => Ok((source_pos, target_pos)), - (_, FieldAnnotation { field_span, .. }) => Err(syn::Error::new( - field_span, - "Unexpected mapping to named field for tuple variant", - )), - }) - .collect::>()?, - }, - Fields::Named(_) => VariantMapping::Struct { - source_variant, - fields_mapping: fields_annotations - .into_iter() - .map(|source_to_target| match source_to_target { - ( - FieldRef::FieldIdent(source_ident), - FieldAnnotation { - target_field: FieldRef::FieldIdent(target_ident), - .. - }, - ) => Ok((source_ident, target_ident)), - (_, FieldAnnotation { field_span, .. }) => Err(syn::Error::new( - field_span, - "Unexpected mapping to positional field for struct variant", - )), - }) - .collect::>()?, - }, - }; + let variant_mapping = compute_variant_mapping( + target_enum, + &target_variant, + fields_annotations, + fields, + source_variant, + )?; let mut variant_mappings = variants_mapping .remove(&target_variant) @@ -288,6 +307,196 @@ impl TryFrom for EnumIntoGenerator { } } +fn compute_variant_mapping( + target_enum: &ContainerIdent, + target_variant: &VariantIdent, + fields_annotations: BTreeMap, + fields: &Fields, + source_variant: VariantIdent, +) -> syn::Result { + match ( + fields, + fields_annotations + .first_key_value() + .map(|(_, field_annotation)| &field_annotation.target_field), + ) { + (Fields::Unit, None) => Ok(VariantMapping::UnitToUnit { source_variant }), + (Fields::Unit, Some(_)) => panic!("A unit variant cannot have field annotations"), + (Fields::Unnamed(_), None) | (Fields::Unnamed(_), Some(FieldRef::FieldPos(_))) => { + compute_tuple_to_tuple_variant_mapping(fields_annotations, source_variant) + } + (Fields::Named(_), None) | (Fields::Named(_), Some(FieldRef::FieldIdent(_))) => { + compute_struct_to_struct_variant_mapping(fields_annotations, source_variant) + } + (Fields::Unnamed(fields), Some(FieldRef::FieldIdent(_))) => { + compute_tuple_to_struct_variant_mapping( + target_enum, + target_variant, + fields_annotations, + fields, + source_variant, + ) + } + (Fields::Named(fields), Some(FieldRef::FieldPos(_))) => { + compute_struct_to_tuple_variant_mapping( + target_enum, + target_variant, + fields_annotations, + fields, + source_variant, + ) + } + } +} + +fn compute_tuple_to_tuple_variant_mapping( + fields_annotations: BTreeMap, + source_variant: VariantIdent, +) -> syn::Result { + let fields_mapping = fields_annotations + .into_iter() + .map(|source_to_target| match source_to_target { + ( + FieldRef::FieldPos(source_pos), + FieldAnnotation { + target_field: FieldRef::FieldPos(target_pos), + .. + }, + ) => Ok((source_pos, target_pos)), + (_, FieldAnnotation { field_span, .. }) => Err(syn::Error::new( + field_span, + "Unexpected mapping to named field while another field mapped to a positional field.", + )), + }) + .collect::>()?; + + Ok(VariantMapping::TupleToTuple { + source_variant, + fields_mapping, + }) +} + +fn compute_struct_to_struct_variant_mapping( + fields_annotations: BTreeMap, + source_variant: VariantIdent, +) -> syn::Result { + let fields_mapping = fields_annotations + .into_iter() + .map(|source_to_target| match source_to_target { + ( + FieldRef::FieldIdent(source_ident), + FieldAnnotation { + target_field: FieldRef::FieldIdent(target_ident), + .. + }, + ) => Ok((source_ident, target_ident)), + (_, FieldAnnotation { field_span, .. }) => Err(syn::Error::new( + field_span, + "Unexpected mapping to positional field while another field mapped to a named field.", + )), + }) + .collect::>()?; + + Ok(VariantMapping::StructToStruct { + source_variant, + fields_mapping, + }) +} + +fn compute_struct_to_tuple_variant_mapping( + target_enum: &ContainerIdent, + target_variant: &VariantIdent, + fields_annotations: BTreeMap, + fields: &FieldsNamed, + source_variant: VariantIdent, +) -> syn::Result { + let fields_mapping = fields_annotations + .into_iter() + .map(|source_to_target| match source_to_target { + (FieldRef::FieldPos(_), _) => { + panic!("Source is a struct variant but got positional fields") + }, + ( + FieldRef::FieldIdent(source_ident), + FieldAnnotation { + target_field: FieldRef::FieldPos(target_pos), + .. + }, + ) => Ok((source_ident, target_pos)), + (FieldRef::FieldIdent(_), FieldAnnotation { target_field: FieldRef::FieldIdent(_), field_span, .. }) => { + Err(syn::Error::new( + field_span, + "Unexpected mapping to named field while another field mapped to a positional field.", + )) + }, + }) + .collect::>>()?; + + for field in fields.named.iter() { + if !fields_mapping.contains_key(&FieldIdent( + field.ident.clone().expect("Named fields have idents"), + )) { + Err(syn::Error::new( + field.span(), + format!( + "Missing required mapping to named field for {target_enum}::{target_variant}" + ), + ))?; + } + } + + Ok(VariantMapping::StructToTuple { + source_variant, + fields_mapping, + }) +} + +fn compute_tuple_to_struct_variant_mapping( + target_enum: &ContainerIdent, + target_variant: &VariantIdent, + fields_annotations: BTreeMap, + fields: &FieldsUnnamed, + source_variant: VariantIdent, +) -> syn::Result { + let fields_mapping = fields_annotations + .into_iter() + .map(|source_to_target| match source_to_target { + (FieldRef::FieldIdent(_), _) => { + panic!("Source is a tuple variant but got named fields") + }, + ( + FieldRef::FieldPos(source_pos), + FieldAnnotation { + target_field: FieldRef::FieldIdent(target_ident), + .. + }, + ) => Ok((source_pos, target_ident)), + (FieldRef::FieldPos(_), FieldAnnotation { target_field: FieldRef::FieldPos(_), field_span, .. }) => { + Err(syn::Error::new( + field_span, + "Unexpected mapping to positional field while another field mapped to a named field.", + )) + }, + }) + .collect::>>()?; + + for (pos, field) in fields.unnamed.iter().enumerate() { + if !fields_mapping.contains_key(&pos) { + Err(syn::Error::new( + field.span(), + format!( + "Missing required mapping to named field for {target_enum}::{target_variant}" + ), + ))?; + } + } + + Ok(VariantMapping::TupleToStruct { + source_variant, + fields_mapping, + }) +} + fn check_unused_variants_annotations( target_variants: HashMap, ) -> syn::Result<()> { diff --git a/src/idents.rs b/src/idents.rs index d232f81..bb617e8 100644 --- a/src/idents.rs +++ b/src/idents.rs @@ -21,6 +21,12 @@ impl ToTokens for ContainerIdent { #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct VariantIdent(pub Ident); +impl Display for VariantIdent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + impl ToTokens for VariantIdent { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { self.0.to_tokens(tokens); diff --git a/src/lib.rs b/src/lib.rs index 1dd9114..e92df08 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,7 @@ mod idents; /// Unit, /// OtherUnit, /// Tuple(i32, &'static str), -/// DifferentName { x: i32, y: i32 } +/// DifferentName { x: i32, y: i32 }, /// } /// /// #[derive(EnumFrom)] @@ -29,7 +29,7 @@ mod idents; /// Tuple(i64, String), /// #[enum_from(Source::DifferentName)] /// Struct { x: f64, y: f64 }, -/// Extra // This variant cannot be built from Source +/// Extra, // This variant cannot be built from Source /// } /// /// let source = Source::Unit; @@ -50,7 +50,7 @@ mod idents; /// assert!(matches!(target, Target::Struct { x, y } if x == 1.0 && y == 2.0)); /// ``` /// -/// ## Multiple source enums +/// ## Multiple source enums with fields mapping /// ``` /// use enum_convert::EnumFrom; /// @@ -66,7 +66,7 @@ mod idents; /// /// enum SecondSource { /// Empty, -/// Struct { a: i32, b: i32, s: &'static str }, +/// Data(i32, i32, &'static str), /// } /// /// #[derive(EnumFrom)] @@ -80,14 +80,15 @@ mod idents; /// #[enum_from(FirstSource::Tuple.0)] String, /// ), /// #[enum_from(FirstSource::DifferentName, SecondSource)] -/// Struct { -/// #[enum_from(FirstSource::DifferentName.alpha, SecondSource::Struct.a)] +/// Data { +/// #[enum_from(FirstSource::DifferentName.alpha, SecondSource::Data.0)] /// x: f64, -/// #[enum_from(SecondSource::Struct.b)] +/// #[enum_from(SecondSource::Data.1)] /// y: f64, +/// #[enum_from(SecondSource::Data.2)] /// s: &'static str, /// }, -/// Extra +/// Extra, /// } /// /// let first_source = FirstSource::Unit; @@ -105,12 +106,12 @@ mod idents; /// /// let first_source = FirstSource::DifferentName { alpha: 1.0, y: 2.0, s: "hello" }; /// let target: Target = first_source.into(); -/// assert!(matches!(target, Target::Struct { x, y, s } if x == 1.0 && y == 2.0 && s == "hello")); +/// assert!(matches!(target, Target::Data { x, y, s } if x == 1.0 && y == 2.0 && s == "hello")); /// -/// // Target::Struct can also come from SecondSource::Struct -/// let second_source = SecondSource::Struct { a: 1, b: 2, s: "hello" }; +/// // Target::Data can also come from SecondSource::Data +/// let second_source = SecondSource::Data(1, 2, "hello"); /// let target: Target = second_source.into(); -/// assert!(matches!(target, Target::Struct { x, y, s } if x == 1.0 && y == 2.0 && s == "hello")); +/// assert!(matches!(target, Target::Data { x, y, s } if x == 1.0 && y == 2.0 && s == "hello")); /// ``` #[proc_macro_derive(EnumFrom, attributes(enum_from))] pub fn derive_enum_from(input: TokenStream) -> TokenStream { @@ -176,22 +177,23 @@ pub fn derive_enum_from(input: TokenStream) -> TokenStream { /// ), /// #[enum_into(FirstTarget::Data, SecondTarget::Info)] // Maps to different variants /// Record { -/// #[enum_into(FirstTarget::Data.name, SecondTarget::Info.title)] // Maps fields differently +/// #[enum_into(FirstTarget::Data.name, SecondTarget::Info.0)] // Maps fields differently /// label: String, +/// #[enum_into(SecondTarget::Info.1)] /// value: i32 -/// } +/// }, /// } /// /// enum FirstTarget { /// Unit, /// Tuple(String, i32), -/// Data { name: String, value: i64 } +/// Data { name: String, value: i64 }, /// } /// /// enum SecondTarget { /// Unit, /// Tuple(i32, String), -/// Info { title: String, value: i64 } +/// Info(String, i64), /// } /// /// let source = Source::Unit; @@ -219,7 +221,7 @@ pub fn derive_enum_from(input: TokenStream) -> TokenStream { /// // Source::Record can also go to SecondTarget::Info with different field mapping /// let source = Source::Record { label: "test".to_string(), value: 42 }; /// let second_target: SecondTarget = source.into(); -/// assert!(matches!(second_target, SecondTarget::Info { title, value } if title == "test" && value == 42)); +/// assert!(matches!(second_target, SecondTarget::Info(label, value) if label == "test" && value == 42)); /// ``` #[proc_macro_derive(EnumInto, attributes(enum_into))] pub fn derive_enum_into(input: TokenStream) -> TokenStream { diff --git a/tests/enum_from/compile_fail/field/mapping_struct_to_tuple_fields.stderr b/tests/enum_from/compile_fail/field/mapping_struct_to_tuple_fields.stderr deleted file mode 100644 index 838a5da..0000000 --- a/tests/enum_from/compile_fail/field/mapping_struct_to_tuple_fields.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: Unexpected mapping to named field for tuple variant - --> tests/enum_from/compile_fail/field/mapping_struct_to_tuple_fields.rs:12:36 - | -12 | #[enum_from(Source::Struct.aa)] i32, - | ^^ diff --git a/tests/enum_from/compile_fail/field/mapping_tuple_to_struct_fields.stderr b/tests/enum_from/compile_fail/field/mapping_tuple_to_struct_fields.stderr deleted file mode 100644 index 37180f3..0000000 --- a/tests/enum_from/compile_fail/field/mapping_tuple_to_struct_fields.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: Unexpected mapping to positional field for struct variant - --> tests/enum_from/compile_fail/field/mapping_tuple_to_struct_fields.rs:12:35 - | -12 | #[enum_from(Source::Tuple.0)] - | ^ diff --git a/tests/enum_from/compile_fail/field/mapping_struct_to_tuple_fields.rs b/tests/enum_from/compile_fail/field/missing_annotation_for_struct_to_tuple.rs similarity index 77% rename from tests/enum_from/compile_fail/field/mapping_struct_to_tuple_fields.rs rename to tests/enum_from/compile_fail/field/missing_annotation_for_struct_to_tuple.rs index d011958..376a138 100644 --- a/tests/enum_from/compile_fail/field/mapping_struct_to_tuple_fields.rs +++ b/tests/enum_from/compile_fail/field/missing_annotation_for_struct_to_tuple.rs @@ -10,7 +10,7 @@ enum Target { #[enum_from(Source::Struct)] Tuple( #[enum_from(Source::Struct.aa)] i32, - #[enum_from(Source::Struct.bb)] i32, + i64, // Annotation #[enum_from(Source::Struct.aa)] is missing ), } diff --git a/tests/enum_from/compile_fail/field/missing_annotation_for_struct_to_tuple.stderr b/tests/enum_from/compile_fail/field/missing_annotation_for_struct_to_tuple.stderr new file mode 100644 index 0000000..b20f9bc --- /dev/null +++ b/tests/enum_from/compile_fail/field/missing_annotation_for_struct_to_tuple.stderr @@ -0,0 +1,5 @@ +error: Missing required mapping to named field for Source::Struct + --> tests/enum_from/compile_fail/field/missing_annotation_for_struct_to_tuple.rs:13:9 + | +13 | i64, // Annotation #[enum_from(Source::Struct.aa)] is missing + | ^^^ diff --git a/tests/enum_from/compile_fail/field/mapping_tuple_to_struct_fields.rs b/tests/enum_from/compile_fail/field/missing_annotation_for_tuple_to_struct.rs similarity index 78% rename from tests/enum_from/compile_fail/field/mapping_tuple_to_struct_fields.rs rename to tests/enum_from/compile_fail/field/missing_annotation_for_tuple_to_struct.rs index bb762bb..cb37508 100644 --- a/tests/enum_from/compile_fail/field/mapping_tuple_to_struct_fields.rs +++ b/tests/enum_from/compile_fail/field/missing_annotation_for_tuple_to_struct.rs @@ -9,9 +9,9 @@ enum Source { enum Target { #[enum_from(Source::Tuple)] Struct { + // #[enum_from(Source::Tuple.1)] missing + a: i64, #[enum_from(Source::Tuple.0)] - a: i32, - #[enum_from(Source::Tuple.1)] b: i32, }, } diff --git a/tests/enum_from/compile_fail/field/missing_annotation_for_tuple_to_struct.stderr b/tests/enum_from/compile_fail/field/missing_annotation_for_tuple_to_struct.stderr new file mode 100644 index 0000000..e3e1fb6 --- /dev/null +++ b/tests/enum_from/compile_fail/field/missing_annotation_for_tuple_to_struct.stderr @@ -0,0 +1,5 @@ +error: Missing required mapping to named field for Source::Tuple + --> tests/enum_from/compile_fail/field/missing_annotation_for_tuple_to_struct.rs:13:9 + | +13 | a: i64, + | ^ diff --git a/tests/enum_from/compile_fail/field/mixing_source_variant_kind.stderr b/tests/enum_from/compile_fail/field/mixing_source_variant_kind.stderr index 97a266c..9a69385 100644 --- a/tests/enum_from/compile_fail/field/mixing_source_variant_kind.stderr +++ b/tests/enum_from/compile_fail/field/mixing_source_variant_kind.stderr @@ -1,5 +1,5 @@ -error: Unexpected mapping to positional field for struct variant - --> tests/enum_from/compile_fail/field/mixing_source_variant_kind.rs:12:35 +error: Unexpected mapping to named field while another field mapped to a positional field. + --> tests/enum_from/compile_fail/field/mixing_source_variant_kind.rs:14:35 | -12 | #[enum_from(Source::Tuple.0)] // .0 Does not make sense for a Struct source +14 | #[enum_from(Source::Tuple.b)] | ^ diff --git a/tests/enum_from/pass/mapping_struct_to_tuple_fields.rs b/tests/enum_from/pass/mapping_struct_to_tuple_fields.rs new file mode 100644 index 0000000..4570fa6 --- /dev/null +++ b/tests/enum_from/pass/mapping_struct_to_tuple_fields.rs @@ -0,0 +1,22 @@ +use enum_convert::EnumFrom; + +enum Source { + Struct { aa: i32, bb: i32 }, +} + +#[derive(EnumFrom)] +#[enum_from(Source)] +enum Target { + #[enum_from(Source::Struct)] + Tuple( + #[enum_from(Source::Struct.bb)] i32, + #[enum_from(Source::Struct.aa)] i64, + ), +} + +fn main() { + assert!(matches!( + Target::from(Source::Struct { aa: 1, bb: 2 }), + Target::Tuple(bb, aa) if aa == 1 && bb == 2, + )); +} diff --git a/tests/enum_from/pass/mapping_tuple_to_struct_fields.rs b/tests/enum_from/pass/mapping_tuple_to_struct_fields.rs new file mode 100644 index 0000000..2b58e71 --- /dev/null +++ b/tests/enum_from/pass/mapping_tuple_to_struct_fields.rs @@ -0,0 +1,24 @@ +use enum_convert::EnumFrom; + +enum Source { + Tuple(i32, i32), +} + +#[derive(EnumFrom)] +#[enum_from(Source)] +enum Target { + #[enum_from(Source::Tuple)] + Struct { + #[enum_from(Source::Tuple.1)] + a: i64, + #[enum_from(Source::Tuple.0)] + b: i32, + }, +} + +fn main() { + assert!(matches!( + Target::from(Source::Tuple(1, 2)), + Target::Struct { a, b } if a == 2 && b == 1, + )); +} diff --git a/tests/enum_into/compile_fail/field/mapping_struct_to_tuple_fields.stderr b/tests/enum_into/compile_fail/field/mapping_struct_to_tuple_fields.stderr deleted file mode 100644 index e6993dc..0000000 --- a/tests/enum_into/compile_fail/field/mapping_struct_to_tuple_fields.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: Unexpected mapping to positional field for struct variant - --> tests/enum_into/compile_fail/field/mapping_struct_to_tuple_fields.rs:8:35 - | -8 | #[enum_into(Target::Tuple.0)] - | ^ diff --git a/tests/enum_into/compile_fail/field/mapping_tuple_to_struct_fields.stderr b/tests/enum_into/compile_fail/field/mapping_tuple_to_struct_fields.stderr deleted file mode 100644 index 36ed48d..0000000 --- a/tests/enum_into/compile_fail/field/mapping_tuple_to_struct_fields.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: Unexpected mapping to named field for tuple variant - --> tests/enum_into/compile_fail/field/mapping_tuple_to_struct_fields.rs:8:36 - | -8 | #[enum_into(Target::Struct.aa)] i32, - | ^^ diff --git a/tests/enum_into/compile_fail/field/mapping_struct_to_tuple_fields.rs b/tests/enum_into/compile_fail/field/missing_annotation_for_struct_to_tuple.rs similarity index 67% rename from tests/enum_into/compile_fail/field/mapping_struct_to_tuple_fields.rs rename to tests/enum_into/compile_fail/field/missing_annotation_for_struct_to_tuple.rs index 6854e93..e9f0afc 100644 --- a/tests/enum_into/compile_fail/field/mapping_struct_to_tuple_fields.rs +++ b/tests/enum_into/compile_fail/field/missing_annotation_for_struct_to_tuple.rs @@ -5,15 +5,15 @@ use enum_convert::EnumInto; enum Source { #[enum_into(Target::Tuple)] Struct { + // #[enum_into(Target::Tuple.1)] missing + aa: i32, #[enum_into(Target::Tuple.0)] - a: i32, - #[enum_into(Target::Tuple.1)] - b: i32, + bb: i32, }, } enum Target { - Tuple(i32, i32), + Tuple(i64), } fn main() {} diff --git a/tests/enum_into/compile_fail/field/missing_annotation_for_struct_to_tuple.stderr b/tests/enum_into/compile_fail/field/missing_annotation_for_struct_to_tuple.stderr new file mode 100644 index 0000000..f3da5f0 --- /dev/null +++ b/tests/enum_into/compile_fail/field/missing_annotation_for_struct_to_tuple.stderr @@ -0,0 +1,5 @@ +error: Missing required mapping to named field for Target::Tuple + --> tests/enum_into/compile_fail/field/missing_annotation_for_struct_to_tuple.rs:9:9 + | +9 | aa: i32, + | ^^ diff --git a/tests/enum_into/compile_fail/field/missing_annotation_for_tuple_to_struct.rs b/tests/enum_into/compile_fail/field/missing_annotation_for_tuple_to_struct.rs new file mode 100644 index 0000000..53207b8 --- /dev/null +++ b/tests/enum_into/compile_fail/field/missing_annotation_for_tuple_to_struct.rs @@ -0,0 +1,18 @@ +use enum_convert::EnumInto; + +#[derive(EnumInto)] +#[enum_into(Target)] +enum Source { + #[enum_into(Target::Struct)] + Tuple( + // #[enum_into(Target::Struct.b)] missing + i32, + #[enum_into(Target::Struct.a)] i32, + ), +} + +enum Target { + Struct { a: i64, b: i32 }, +} + +fn main() {} diff --git a/tests/enum_into/compile_fail/field/missing_annotation_for_tuple_to_struct.stderr b/tests/enum_into/compile_fail/field/missing_annotation_for_tuple_to_struct.stderr new file mode 100644 index 0000000..48f1aa2 --- /dev/null +++ b/tests/enum_into/compile_fail/field/missing_annotation_for_tuple_to_struct.stderr @@ -0,0 +1,5 @@ +error: Missing required mapping to named field for Target::Struct + --> tests/enum_into/compile_fail/field/missing_annotation_for_tuple_to_struct.rs:9:9 + | +9 | i32, + | ^^^ diff --git a/tests/enum_into/compile_fail/field/mixing_target_variant_kind.stderr b/tests/enum_into/compile_fail/field/mixing_target_variant_kind.stderr index 1b3f4d3..cfb1b48 100644 --- a/tests/enum_into/compile_fail/field/mixing_target_variant_kind.stderr +++ b/tests/enum_into/compile_fail/field/mixing_target_variant_kind.stderr @@ -1,5 +1,5 @@ -error: Unexpected mapping to positional field for struct variant - --> tests/enum_into/compile_fail/field/mixing_target_variant_kind.rs:8:36 - | -8 | #[enum_into(Target::Struct.0)] // .0 Does not make sense for a Struct target - | ^ +error: Unexpected mapping to named field while another field mapped to a positional field. + --> tests/enum_into/compile_fail/field/mixing_target_variant_kind.rs:10:36 + | +10 | #[enum_into(Target::Struct.b)] + | ^ diff --git a/tests/enum_into/pass/mapping_struct_to_tuple_fields.rs b/tests/enum_into/pass/mapping_struct_to_tuple_fields.rs new file mode 100644 index 0000000..22b7682 --- /dev/null +++ b/tests/enum_into/pass/mapping_struct_to_tuple_fields.rs @@ -0,0 +1,24 @@ +use enum_convert::EnumInto; + +#[derive(EnumInto)] +#[enum_into(Target)] +enum Source { + #[enum_into(Target::Tuple)] + Struct { + #[enum_into(Target::Tuple.0)] + a: i32, + #[enum_into(Target::Tuple.1)] + b: i32, + }, +} + +enum Target { + Tuple(i32, i32), +} + +fn main() { + assert!(matches!( + Target::from(Source::Struct { a: 1, b: 2 }), + Target::Tuple(a, b) if a == 1 && b == 2, + )); +} diff --git a/tests/enum_into/compile_fail/field/mapping_tuple_to_struct_fields.rs b/tests/enum_into/pass/mapping_tuple_to_struct_fields.rs similarity index 65% rename from tests/enum_into/compile_fail/field/mapping_tuple_to_struct_fields.rs rename to tests/enum_into/pass/mapping_tuple_to_struct_fields.rs index 44dd08c..ce97c30 100644 --- a/tests/enum_into/compile_fail/field/mapping_tuple_to_struct_fields.rs +++ b/tests/enum_into/pass/mapping_tuple_to_struct_fields.rs @@ -5,8 +5,8 @@ use enum_convert::EnumInto; enum Source { #[enum_into(Target::Struct)] Tuple( - #[enum_into(Target::Struct.aa)] i32, #[enum_into(Target::Struct.bb)] i32, + #[enum_into(Target::Struct.aa)] i32, ), } @@ -14,4 +14,9 @@ enum Target { Struct { aa: i32, bb: i32 }, } -fn main() {} +fn main() { + assert!(matches!( + Target::from(Source::Tuple(1, 2)), + Target::Struct { aa, bb } if aa == 2 && bb == 1, + )); +}