From df4e6bc8a496e9b413e5face6a5970601480460b Mon Sep 17 00:00:00 2001 From: realbigsean Date: Fri, 2 Feb 2024 09:43:49 -0500 Subject: [PATCH 1/8] meta variants --- src/attributes.rs | 2 +- src/lib.rs | 345 +++++++++++++++++++++++++++++++++++++++------- 2 files changed, 296 insertions(+), 51 deletions(-) diff --git a/src/attributes.rs b/src/attributes.rs index e9a04ba3..245492ab 100644 --- a/src/attributes.rs +++ b/src/attributes.rs @@ -21,7 +21,7 @@ impl FromMeta for NestedMetaList { /// List of identifiers implementing `FromMeta`. /// /// Useful for imposing ordering, unlike the `HashMap` options provided by `darling`. -#[derive(Debug)] +#[derive(Debug, Default, Clone)] pub struct IdentList { pub idents: Vec, } diff --git a/src/lib.rs b/src/lib.rs index fa836505..1d8c491e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,9 @@ mod utils; /// Top-level configuration via the `superstruct` attribute. #[derive(Debug, FromMeta)] struct StructOpts { + /// List of meta variant names of the superstruct being derived. + #[darling(default)] + meta_variants: Option, /// List of variant names of the superstruct being derived. variants: IdentList, /// List of attributes to apply to the variant structs. @@ -74,18 +77,13 @@ struct FieldOpts { #[darling(default)] only: Option>, #[darling(default)] + meta_only: Option>, + #[darling(default)] getter: Option, #[darling(default)] partial_getter: Option, } -fn should_skip(flatten: &Override>, key: &Ident) -> bool { - match flatten { - Override::Inherit => false, - Override::Explicit(map) => !map.is_empty() && !map.contains_key(key), - } -} - /// Getter configuration for a specific field #[derive(Debug, Default, FromMeta)] struct GetterOpts { @@ -134,43 +132,96 @@ impl ErrorOpts { struct FieldData { name: Ident, field: Field, - only: Option>, + only_combinations: Vec, getter_opts: GetterOpts, partial_getter_opts: GetterOpts, + is_common: bool, } impl FieldData { fn is_common(&self) -> bool { - self.only.is_none() + self.is_common + } + + /// Checks whether this field should be included in creating + /// partial getters for the given type name. + fn exists_in_meta(&self, type_name: &Ident) -> bool { + let only_metas = self + .only_combinations + .iter() + .filter_map(|only| only.meta_variant.as_ref()) + .map(ToString::to_string) + .collect::>(); + + if only_metas.is_empty() { + return true; + } + only_metas + .iter() + .any(|only| type_name.to_string().ends_with(only)) } } +#[derive(Hash, Eq, PartialEq, Debug, Clone)] +struct VariantKey { + variant: Ident, + meta_variant: Option, +} + #[proc_macro_attribute] pub fn superstruct(args: TokenStream, input: TokenStream) -> TokenStream { let attr_args = parse_macro_input!(args as AttributeArgs); let item = parse_macro_input!(input as ItemStruct); let type_name = &item.ident; - let visibility = item.vis; + let visibility = item.vis.clone(); // Extract the generics to use for the top-level type and all variant structs. let decl_generics = &item.generics; // Generics used for the impl block. - let (impl_generics, ty_generics, where_clause) = &item.generics.split_for_impl(); + let (_, _, where_clause) = &item.generics.split_for_impl(); let opts = StructOpts::from_list(&attr_args).unwrap(); let mut output_items: Vec = vec![]; - let mk_struct_name = |variant_name: &Ident| format_ident!("{}{}", type_name, variant_name); + let mk_struct_name = |variant_key: &VariantKey| { + let VariantKey { + variant, + meta_variant, + } = variant_key; + + if let Some(meta_variant) = meta_variant { + format_ident!("{}{}{}", type_name, meta_variant, variant) + } else { + format_ident!("{}{}", type_name, variant) + } + }; let variant_names = &opts.variants.idents; - let struct_names = variant_names.iter().map(mk_struct_name).collect_vec(); + let meta_variant_names = &opts + .meta_variants + .clone() + .map(|mv| mv.idents.into_iter().map(Some).collect_vec()) + .unwrap_or(vec![None]); + let variant_combinations = variant_names + .iter() + .cloned() + .cartesian_product(meta_variant_names.iter().cloned()) + .map(|(v, mv)| VariantKey { + variant: v, + meta_variant: mv, + }); + + let struct_names = variant_combinations + .clone() + .map(|key| mk_struct_name(&key)) + .collect_vec(); // Vec of field data. let mut fields = vec![]; - // Map from variant to variant fields. + // Map from variant or meta variant to variant fields. let mut variant_fields = - HashMap::<_, _>::from_iter(variant_names.iter().zip(iter::repeat(vec![]))); + HashMap::<_, _>::from_iter(variant_combinations.clone().zip(iter::repeat(vec![]))); for field in &item.fields { let name = field.ident.clone().expect("named fields only"); @@ -193,10 +244,30 @@ pub fn superstruct(args: TokenStream, input: TokenStream) -> TokenStream { || variant_names.clone(), |only| only.keys().cloned().collect_vec(), ); + let field_meta_variants = field_opts.meta_only.as_ref().map_or_else( + || meta_variant_names.clone(), + |meta_only| meta_only.keys().cloned().map(Some).collect_vec(), + ); - for variant_name in field_variants { + let is_common_meta = opts.meta_variants.as_ref().map_or(true, |mv| { + field_opts + .meta_only + .as_ref() + .map_or(true, |field_meta| field_meta.len() == mv.idents.len()) + }); + let is_common = (field_variants.len() == variant_names.len()) && is_common_meta; + + let only_combinations = field_variants + .iter() + .cartesian_product(field_meta_variants.iter()) + .clone(); + + for (variant, meta_variant) in only_combinations.clone() { variant_fields - .get_mut(&variant_name) + .get_mut(&VariantKey { + variant: variant.clone(), + meta_variant: meta_variant.clone(), + }) .expect("invalid variant name in `only`") .push(output_field.clone()); } @@ -204,33 +275,44 @@ pub fn superstruct(args: TokenStream, input: TokenStream) -> TokenStream { // Check field opts if field_opts.only.is_some() && field_opts.getter.is_some() { panic!("can't configure `only` and `getter` on the same field"); + } else if field_opts.meta_only.is_some() && field_opts.getter.is_some() { + panic!("can't configure `meta_only` and `getter` on the same field"); } else if field_opts.only.is_none() && field_opts.partial_getter.is_some() { panic!("can't set `partial_getter` options on common field"); } else if field_opts.flatten.is_some() && field_opts.only.is_some() { panic!("can't set `flatten` and `only` on the same field"); + } else if field_opts.flatten.is_some() && field_opts.meta_only.is_some() { + panic!("can't set `flatten` and `meta_only` on the same field"); } else if field_opts.flatten.is_some() && field_opts.getter.is_some() { panic!("can't set `flatten` and `getter` on the same field"); } else if field_opts.flatten.is_some() && field_opts.partial_getter.is_some() { panic!("can't set `flatten` and `partial_getter` on the same field"); } - let only = field_opts.only.map(|only| only.keys().cloned().collect()); let getter_opts = field_opts.getter.unwrap_or_default(); let partial_getter_opts = field_opts.partial_getter.unwrap_or_default(); if let Some(flatten_opts) = field_opts.flatten { - for variant in variant_names { + for variant_key in variant_combinations.clone() { + let variant = &variant_key.variant; + let meta_variant = variant_key.meta_variant.as_ref(); + let variant_field_index = variant_fields - .get(variant) + .get(&variant_key) .expect("invalid variant name") .iter() .position(|f| f.ident.as_ref() == Some(&name)) .expect("flattened fields are present on all variants"); - if should_skip(&flatten_opts, variant) { + if should_skip( + variant_names, + meta_variant_names, + &flatten_opts, + &variant_key, + ) { // Remove the field from the field map let fields = variant_fields - .get_mut(variant) + .get_mut(&variant_key) .expect("invalid variant name"); fields.remove(variant_field_index); continue; @@ -238,23 +320,44 @@ pub fn superstruct(args: TokenStream, input: TokenStream) -> TokenStream { // Update the struct name for this variant. let mut next_variant_field = output_field.clone(); - match &mut next_variant_field.ty { + + let last_segment_mut_ref = match next_variant_field.ty { Type::Path(ref mut p) => { - let last_segment = &mut p + &mut p .path .segments .last_mut() - .expect("path should have at least one segment"); - let inner_ty_name = last_segment.ident.clone(); - let next_variant_ty_name = format_ident!("{}{}", inner_ty_name, variant); - last_segment.ident = next_variant_ty_name; + .expect("path should have at least one segment") + .ident } _ => panic!("field must be a path"), }; + let (next_variant_ty_name, partial_getter_rename) = + if let Some(meta_variant) = meta_variant { + ( + format_ident!( + "{}{}{}", + last_segment_mut_ref.clone(), + meta_variant, + variant + ), + format_ident!( + "{}_{}_{}", + name, + meta_variant.to_string().to_lowercase(), + variant.to_string().to_lowercase() + ), + ) + } else { + ( + format_ident!("{}{}", last_segment_mut_ref.clone(), variant), + format_ident!("{}_{}", name, variant.to_string().to_lowercase()), + ) + }; + *last_segment_mut_ref = next_variant_ty_name; + // Create a partial getter for the field. - let partial_getter_rename = - format_ident!("{}_{}", name, variant.to_string().to_lowercase()); let partial_getter_opts = GetterOpts { rename: Some(partial_getter_rename), ..<_>::default() @@ -264,14 +367,15 @@ pub fn superstruct(args: TokenStream, input: TokenStream) -> TokenStream { name: name.clone(), field: next_variant_field.clone(), // Make sure the field is only accessible from this variant. - only: Some(vec![variant.clone()]), + only_combinations: vec![variant_key.clone()], getter_opts: <_>::default(), partial_getter_opts, + is_common: false, }); // Update the variant field map let fields = variant_fields - .get_mut(variant) + .get_mut(&variant_key) .expect("invalid variant name"); *fields .get_mut(variant_field_index) @@ -281,9 +385,15 @@ pub fn superstruct(args: TokenStream, input: TokenStream) -> TokenStream { fields.push(FieldData { name, field: output_field, - only, + only_combinations: only_combinations + .map(|(variant, meta_variant)| VariantKey { + variant: variant.clone(), + meta_variant: meta_variant.clone(), + }) + .collect_vec(), getter_opts, partial_getter_opts, + is_common, }); } } @@ -294,13 +404,14 @@ pub fn superstruct(args: TokenStream, input: TokenStream) -> TokenStream { .as_ref() .map_or(&[][..], |attrs| &attrs.metas); - for (variant_name, struct_name) in variant_names.iter().zip(struct_names.iter()) { - let fields = &variant_fields[variant_name]; + for (variant_key, struct_name) in variant_combinations.zip(struct_names.iter()) { + let fields = &variant_fields[&variant_key]; + // TODO: think about how to handle this with meta let specific_struct_attributes = opts .specific_variant_attributes .as_ref() - .and_then(|sv| sv.get(&variant_name)) + .and_then(|sv| sv.get(&variant_key.variant)) .map_or(&[][..], |attrs| &attrs.metas); let variant_code = quote! { @@ -324,6 +435,71 @@ pub fn superstruct(args: TokenStream, input: TokenStream) -> TokenStream { return TokenStream::from_iter(output_items); } + let mut inner_enum_names = vec![]; + + // Generate inner enums if necessary. + for meta_variant in meta_variant_names { + if let Some(meta_variant) = meta_variant { + let inner_enum_name = format_ident!("{}{}", type_name, meta_variant); + inner_enum_names.push(inner_enum_name.clone()); + let inner_struct_names = variant_names + .iter() + .map(|variant_name| format_ident!("{}{}", inner_enum_name, variant_name)) + .collect_vec(); + generate_wrapper_enums( + &inner_enum_name, + &item, + &opts, + &mut output_items, + variant_names, + &inner_struct_names, + &fields, + false, + ); + } + } + + // Generate outer enum. + let variant_names = opts + .meta_variants + .as_ref() + .map(|mv| &mv.idents) + .unwrap_or(variant_names); + let struct_names = &opts + .meta_variants + .as_ref() + .map(|_| inner_enum_names) + .unwrap_or(struct_names); + generate_wrapper_enums( + type_name, + &item, + &opts, + &mut output_items, + variant_names, + struct_names, + &fields, + opts.meta_variants.is_some(), + ); + + TokenStream::from_iter(output_items) +} + +fn generate_wrapper_enums( + type_name: &Ident, + item: &ItemStruct, + opts: &StructOpts, + mut output_items: &mut Vec, + variant_names: &[Ident], + struct_names: &[Ident], + fields: &[FieldData], + is_meta: bool, +) { + let visibility = &item.vis; + // Extract the generics to use for the top-level type and all variant structs. + let decl_generics = &item.generics; + // Generics used for the impl block. + let (impl_generics, ty_generics, where_clause) = &item.generics.split_for_impl(); + // Construct the top-level enum. let top_level_attrs = discard_superstruct_attrs(&item.attrs); let enum_item = quote! { @@ -422,19 +598,22 @@ pub fn superstruct(args: TokenStream, input: TokenStream) -> TokenStream { let getters = fields .iter() .filter(|f| f.is_common()) - .map(|field_data| make_field_getter(type_name, &variant_names, &field_data, None)); + .map(|field_data| make_field_getter(type_name, &variant_names, &field_data, None, is_meta)); let mut_getters = fields .iter() .filter(|f| f.is_common() && !f.getter_opts.no_mut) - .map(|field_data| make_mut_field_getter(type_name, &variant_names, &field_data, None)); + .map(|field_data| { + make_mut_field_getter(type_name, &variant_names, &field_data, None, is_meta) + }); let partial_getters = fields .iter() .filter(|f| !f.is_common()) + .filter(|f| is_meta || f.exists_in_meta(type_name)) .cartesian_product(&[false, true]) .flat_map(|(field_data, mutability)| { - let field_variants = field_data.only.as_ref()?; + let field_variants = &field_data.only_combinations; Some(make_partial_getter( type_name, &field_data, @@ -442,6 +621,7 @@ pub fn superstruct(args: TokenStream, input: TokenStream) -> TokenStream { &opts.partial_getter_error, *mutability, None, + is_meta, )) }); @@ -507,21 +687,24 @@ pub fn superstruct(args: TokenStream, input: TokenStream) -> TokenStream { &variant_names, &field_data, Some(&ref_ty_lifetime), + is_meta, ) }); let ref_partial_getters = fields .iter() .filter(|f| !f.is_common()) + .filter(|f| is_meta || f.exists_in_meta(type_name)) .flat_map(|field_data| { - let field_variants = field_data.only.as_ref()?; + let field_variants = &field_data.only_combinations; Some(make_partial_getter( &ref_ty_name, - &field_data, - &field_variants, + field_data, + field_variants, &opts.partial_getter_error, false, Some(&ref_ty_lifetime), + is_meta, )) }); @@ -554,14 +737,16 @@ pub fn superstruct(args: TokenStream, input: TokenStream) -> TokenStream { &variant_names, &field_data, Some(&ref_mut_ty_lifetime), + is_meta, ) }); let ref_mut_partial_getters = fields .iter() .filter(|f| !f.is_common() && !f.partial_getter_opts.no_mut) + .filter(|f| is_meta || f.exists_in_meta(type_name)) .flat_map(|field_data| { - let field_variants = field_data.only.as_ref()?; + let field_variants = &field_data.only_combinations; Some(make_partial_getter( &ref_mut_ty_name, &field_data, @@ -569,6 +754,7 @@ pub fn superstruct(args: TokenStream, input: TokenStream) -> TokenStream { &opts.partial_getter_error, true, Some(&ref_mut_ty_lifetime), + is_meta, )) }); @@ -614,7 +800,7 @@ pub fn superstruct(args: TokenStream, input: TokenStream) -> TokenStream { } // Generate trait implementations. - for (variant_name, struct_name) in variant_names.iter().zip_eq(&struct_names) { + for (variant_name, struct_name) in variant_names.into_iter().zip_eq(struct_names) { let from_impl = generate_from_variant_trait_impl( type_name, impl_generics, @@ -649,8 +835,6 @@ pub fn superstruct(args: TokenStream, input: TokenStream) -> TokenStream { where_clause, ); output_items.push(ref_from_top_level_impl.into()); - - TokenStream::from_iter(output_items) } /// Generate a getter method for a field. @@ -659,6 +843,7 @@ fn make_field_getter( variant_names: &[Ident], field_data: &FieldData, lifetime: Option<&Lifetime>, + is_meta: bool, ) -> proc_macro2::TokenStream { let field_name = &field_data.name; let field_type = &field_data.field.ty; @@ -670,7 +855,10 @@ fn make_field_getter( } else { quote! { &#lifetime #field_type} }; - let return_expr = if getter_opts.copy { + + let return_expr = if is_meta { + quote! { inner.#field_name() } + } else if getter_opts.copy { quote! { inner.#field_name } } else { quote! { &inner.#field_name } @@ -701,6 +889,7 @@ fn make_mut_field_getter( variant_names: &[Ident], field_data: &FieldData, lifetime: Option<&Lifetime>, + is_meta: bool, ) -> proc_macro2::TokenStream { let field_name = &field_data.name; let field_type = &field_data.field.ty; @@ -709,7 +898,11 @@ fn make_mut_field_getter( let fn_name = format_ident!("{}_mut", getter_opts.rename.as_ref().unwrap_or(field_name)); let return_type = quote! { &#lifetime mut #field_type }; let param = make_self_arg(true, lifetime); - let return_expr = quote! { &mut inner.#field_name }; + let return_expr = if is_meta { + quote! { inner.#fn_name() } + } else { + quote! { &mut inner.#field_name } + }; // Pass-through `cfg` attributes as they affect the existence of this field. let cfg_attrs = get_cfg_attrs(&field_data.field.attrs); @@ -760,11 +953,25 @@ fn make_type_ref( fn make_partial_getter( type_name: &Ident, field_data: &FieldData, - field_variants: &[Ident], + field_variants: &[VariantKey], error_opts: &ErrorOpts, mutable: bool, lifetime: Option<&Lifetime>, + is_meta: bool, ) -> proc_macro2::TokenStream { + let field_variants = field_variants + .iter() + .filter_map(|key| { + if is_meta { + key.meta_variant.clone() + } else { + Some(key.variant.clone()) + } + }) + .unique() + .collect_vec(); + let type_name = type_name.clone(); + let field_name = &field_data.name; let renamed_field = field_data .partial_getter_opts @@ -779,7 +986,9 @@ fn make_partial_getter( let copy = field_data.partial_getter_opts.copy; let self_arg = make_self_arg(mutable, lifetime); let ret_ty = make_type_ref(&field_data.field.ty, mutable, copy, lifetime); - let ret_expr = if mutable { + let ret_expr = if is_meta { + quote! { inner.#fn_name()? } + } else if mutable { quote! { &mut inner.#field_name } } else if copy { quote! { inner.#field_name } @@ -871,3 +1080,39 @@ fn is_attr_with_ident(attr: &Attribute, ident: &str) -> bool { .get_ident() .map_or(false, |attr_ident| attr_ident.to_string() == ident) } + +/// Predicate for determining whether a field should be excluded from a flattened +/// variant combination. +fn should_skip( + variant_names: &[Ident], + meta_variant_names: &[Option], + flatten: &Override>, + variant_key: &VariantKey, +) -> bool { + let variant = &variant_key.variant; + let meta_variant = variant_key.meta_variant.as_ref(); + match flatten { + Override::Inherit => false, + Override::Explicit(map) => { + let contains_variant = map.contains_key(variant); + let contains_meta_variant = meta_variant.map_or(true, |mv| map.contains_key(&mv)); + + let variants_exist = variant_names.iter().any(|v| map.contains_key(v)); + let meta_variants_exist = meta_variant_names + .iter() + .flatten() + .any(|mv| map.contains_key(mv)); + + if contains_variant && !meta_variants_exist { + return false; + } + if contains_meta_variant && !variants_exist { + return false; + } + + let contains_all = contains_variant && contains_meta_variant; + + !map.is_empty() && !contains_all + } + } +} From b643d4a43818bea24f17bcb178d41bcb39124624 Mon Sep 17 00:00:00 2001 From: realbigsean Date: Fri, 2 Feb 2024 09:46:31 -0500 Subject: [PATCH 2/8] add tests --- tests/meta_variant.rs | 136 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 tests/meta_variant.rs diff --git a/tests/meta_variant.rs b/tests/meta_variant.rs new file mode 100644 index 00000000..53071d13 --- /dev/null +++ b/tests/meta_variant.rs @@ -0,0 +1,136 @@ +use superstruct::superstruct; + +#[superstruct( + meta_variants(Read, Write), + variants(Lower, Upper), + variant_attributes(derive(Clone, Debug, PartialEq, Eq)) +)] +#[derive(Clone, Debug, PartialEq, Eq)] +struct InnerMessage { + // Exists on all structs. + pub w: u64, + // Exists on all Read structs. + #[superstruct(meta_only(Read))] + pub x: u64, + // Exists on all LowerCase structs. + #[superstruct(only(Lower))] + pub y: u64, + // Exists only in InnerMessageWriteLower. + #[superstruct(meta_only(Write), only(Upper))] + pub z: u64, +} + +#[test] +fn meta_variant() { + let message_a = InnerMessage::Read(InnerMessageRead::Lower(InnerMessageReadLower { + w: 1, + x: 2, + y: 3, + })); + assert_eq!(*message_a.w(), 1); + assert_eq!(*message_a.x().unwrap(), 2); + assert_eq!(*message_a.y().unwrap(), 3); + assert!(message_a.z().is_err()); + + let message_b = InnerMessage::Read(InnerMessageRead::Upper(InnerMessageReadUpper { + w: 1, + x: 2, + })); + assert_eq!(*message_b.w(), 1); + assert_eq!(*message_b.x().unwrap(), 2); + assert!(message_b.y().is_err()); + assert!(message_b.z().is_err()); + + let message_c = InnerMessage::Write(InnerMessageWrite::Lower(InnerMessageWriteLower { + w: 1, + y: 3, + })); + assert_eq!(*message_c.w(), 1); + assert!(message_c.x().is_err()); + assert_eq!(*message_c.y().unwrap(), 3); + assert!(message_c.z().is_err()); + + let message_d = InnerMessage::Write(InnerMessageWrite::Upper(InnerMessageWriteUpper { + w: 1, + z: 4, + })); + assert_eq!(*message_d.w(), 1); + assert!(message_d.x().is_err()); + assert!(message_d.y().is_err()); + assert_eq!(*message_d.z().unwrap(), 4); +} + +#[superstruct( + meta_variants(Read, Write), + variants(Lower, Upper), + variant_attributes(derive(Debug, PartialEq, Eq)) +)] +#[derive(Debug, PartialEq, Eq)] +struct Message { + // Exists on all variants. + #[superstruct(flatten)] + pub inner_a: InnerMessage, + // Exists on all Upper variants. + #[superstruct(flatten(Upper))] + pub inner_b: InnerMessage, + // Exists on all Read variants. + #[superstruct(flatten(Read))] + pub inner_c: InnerMessage, + // Exists on only the Read + Lower variant. + #[superstruct(flatten(Write, Lower))] + pub inner_d: InnerMessage, +} + +#[test] +fn meta_variant_flatten() { + let inner_a = InnerMessageReadLower { w: 1, x: 2, y: 3 }; + let inner_c = InnerMessageReadLower { w: 4, x: 5, y: 6 }; + let message_e = Message::Read(MessageRead::Lower(MessageReadLower { inner_a, inner_c })); + assert_eq!(message_e.inner_a_read_lower().unwrap().w, 1); + assert!(message_e.inner_a_read_upper().is_err()); + assert!(message_e.inner_a_write_lower().is_err()); + assert!(message_e.inner_a_write_upper().is_err()); + + assert_eq!(message_e.inner_c_read_lower().unwrap().w, 4); + assert!(message_e.inner_c_read_upper().is_err()); + + let inner_a = InnerMessageReadUpper { w: 1, x: 2 }; + let inner_b = InnerMessageReadUpper { w: 3, x: 4 }; + let inner_c = InnerMessageReadUpper { w: 5, x: 6 }; + let message_f = Message::Read(MessageRead::Upper(MessageReadUpper { + inner_a, + inner_b, + inner_c, + })); + assert!(message_f.inner_a_read_lower().is_err()); + assert_eq!(message_f.inner_a_read_upper().unwrap().w, 1); + assert!(message_f.inner_a_write_lower().is_err()); + assert!(message_f.inner_a_write_upper().is_err()); + + assert_eq!(message_f.inner_b_read_upper().unwrap().w, 3); + assert!(message_f.inner_b_write_upper().is_err()); + + assert!(message_f.inner_c_read_lower().is_err()); + assert_eq!(message_f.inner_c_read_upper().unwrap().w, 5); + + let inner_a = InnerMessageWriteLower { w: 1, y: 2 }; + let inner_d = InnerMessageWriteLower { w: 3, y: 4 }; + let message_g = Message::Write(MessageWrite::Lower(MessageWriteLower { inner_a, inner_d })); + assert!(message_g.inner_a_read_lower().is_err()); + assert!(message_g.inner_a_read_upper().is_err()); + assert_eq!(message_g.inner_a_write_lower().unwrap().w, 1); + assert!(message_g.inner_a_write_upper().is_err()); + + assert_eq!(message_g.inner_d_write_lower().unwrap().w, 3); + + let inner_a = InnerMessageWriteUpper { w: 1, z: 2 }; + let inner_b = InnerMessageWriteUpper { w: 3, z: 4 }; + let message_h = Message::Write(MessageWrite::Upper(MessageWriteUpper { inner_a, inner_b })); + assert!(message_h.inner_a_read_lower().is_err()); + assert!(message_h.inner_a_read_upper().is_err()); + assert!(message_h.inner_a_write_lower().is_err()); + assert_eq!(message_h.inner_a_write_upper().unwrap().w, 1); + + assert!(message_h.inner_b_read_upper().is_err()); + assert_eq!(message_h.inner_b_write_upper().unwrap().w, 3); +} From 877edbcba846b2334732ee42f387abc87a68cbe2 Mon Sep 17 00:00:00 2001 From: realbigsean Date: Fri, 2 Feb 2024 10:17:08 -0500 Subject: [PATCH 3/8] add docs --- book/src/codegen/meta-variants.md | 73 +++++++++++++++++++++++++++++++ book/src/config/struct.md | 13 ++++++ 2 files changed, 86 insertions(+) create mode 100644 book/src/codegen/meta-variants.md diff --git a/book/src/codegen/meta-variants.md b/book/src/codegen/meta-variants.md new file mode 100644 index 00000000..af5d366a --- /dev/null +++ b/book/src/codegen/meta-variants.md @@ -0,0 +1,73 @@ +# Meta variant structs and enums + +Meta variants are an optional feature, useful for scenarios where you'd want nested +enums at the top-level. structs will be created for all combinations of `meta_variants` +and `variants`, names in the format `{BaseName}{MetaVariantName}{VariantName}`. +Additionally, enums will be created for each `meta_variant` named `{BaseName}{MetaVariantName}`. + +For example: + +```rust,no_run,no_playground +#[superstruct(meta_variants(Baz, Qux), variants(Foo, Bar))] +struct MyStruct { + name: String, + #[superstruct(only(Foo))] + location: u16, + #[superstruct(meta_only(Baz))] + score: u64, + #[superstruct(only(Bar), meta_only(Qux))] + id: usize, +} +``` + +Here the `BaseName` is `MyStruct` and there are two variants in the meta-enum called +`Baz` and `Qux`. + +The generated enums are: + +```rust,no_run,no_playground +enum MyStruct{ + Baz(MyStructBaz), + Qux(MyStructQux), +} + +enum MyStructBaz{ + Foo(MyStructBazFoo), + Bar(MyStructBazBar), +} + +enum MyStructQux{ + Foo(MyStructQuxFoo), + Bar(MyStructQuxBar), +} +``` + +The generated variant structs are: + +```rust,no_run,no_playground +struct MyStructBazFoo { + name: String, + location: u16, + score: u64, +} + +struct MyStructBazBar { + name: String, + score: u64, +} + +struct MyStructQuxFoo { + name: String, + location: u16, +} + +struct MyStructQuxBar { + name: String, + id: usize, +} +``` + +Note how the `only` attribute still applies, and a new `meta_only` attribute can be used to +control the presence of fields in each meta variant. + +For more information see [Struct attributes](../config/struct.md). diff --git a/book/src/config/struct.md b/book/src/config/struct.md index aa01d4a6..8b701122 100644 --- a/book/src/config/struct.md +++ b/book/src/config/struct.md @@ -110,3 +110,16 @@ Please see the documentation on [Mapping into other types](./codegen/map-macros. for an explanation of how these macros operate. **Format**: one or more `superstruct` type names + +## Meta variants + +``` +#[superstruct(meta_variants(A, B, ...), variants(C, D, ...))] +``` + +Generate a two-dimensional superstruct. +See [meta variant structs](../codegen/meta-variants.md). + +The `meta_variants` attribute is optional. + +**Format**: 1+ comma-separated identifiers. From ff4391ad15ecb1ebf4c041b34b53505dc1faf494 Mon Sep 17 00:00:00 2001 From: realbigsean Date: Fri, 2 Feb 2024 11:03:57 -0500 Subject: [PATCH 4/8] make specific variant work --- src/lib.rs | 17 ++++++-- tests/meta_variant.rs | 94 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 1d8c491e..e228bc4d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -277,7 +277,10 @@ pub fn superstruct(args: TokenStream, input: TokenStream) -> TokenStream { panic!("can't configure `only` and `getter` on the same field"); } else if field_opts.meta_only.is_some() && field_opts.getter.is_some() { panic!("can't configure `meta_only` and `getter` on the same field"); - } else if field_opts.only.is_none() && field_opts.partial_getter.is_some() { + } else if field_opts.only.is_none() + && field_opts.meta_only.is_none() + && field_opts.partial_getter.is_some() + { panic!("can't set `partial_getter` options on common field"); } else if field_opts.flatten.is_some() && field_opts.only.is_some() { panic!("can't set `flatten` and `only` on the same field"); @@ -407,19 +410,27 @@ pub fn superstruct(args: TokenStream, input: TokenStream) -> TokenStream { for (variant_key, struct_name) in variant_combinations.zip(struct_names.iter()) { let fields = &variant_fields[&variant_key]; - // TODO: think about how to handle this with meta let specific_struct_attributes = opts .specific_variant_attributes .as_ref() .and_then(|sv| sv.get(&variant_key.variant)) .map_or(&[][..], |attrs| &attrs.metas); + let specific_struct_attributes_meta = opts + .specific_variant_attributes + .as_ref() + .and_then(|sv| variant_key.meta_variant.and_then(|mv| sv.get(&mv))) + .map_or(&[][..], |attrs| &attrs.metas); + let spatt = specific_struct_attributes + .iter() + .chain(specific_struct_attributes_meta.iter()) + .unique(); let variant_code = quote! { #( #[#universal_struct_attributes] )* #( - #[#specific_struct_attributes] + #[#spatt] )* #visibility struct #struct_name #decl_generics #where_clause { #( diff --git a/tests/meta_variant.rs b/tests/meta_variant.rs index 53071d13..a8db818b 100644 --- a/tests/meta_variant.rs +++ b/tests/meta_variant.rs @@ -134,3 +134,97 @@ fn meta_variant_flatten() { assert!(message_h.inner_b_read_upper().is_err()); assert_eq!(message_h.inner_b_write_upper().unwrap().w, 3); } + +#[test] +fn meta_variants_map_macro() { + #[superstruct( + meta_variants(Juicy, Sour), + variants(Apple, Orange), + variant_attributes(derive(Debug, PartialEq)) + )] + #[derive(Debug, PartialEq)] + pub struct Fruit { + #[superstruct(getter(copy))] + id: u64, + #[superstruct(only(Apple), partial_getter(copy))] + description: &'static str, + #[superstruct(meta_only(Juicy))] + name: &'static str, + } + + fn increment_id(id: Fruit) -> Fruit { + map_fruit!(id, |mut inner, cons| { + *inner.id_mut() += 1; + cons(inner) + }) + } + + fn get_id_via_ref<'a>(fruit_ref: FruitRef<'a>) -> u64 { + map_fruit_ref!(&'a _, fruit_ref, |inner, _| { inner.id() }) + } + + assert_eq!( + increment_id(Fruit::Juicy(FruitJuicy::Orange(FruitJuicyOrange { + id: 10, + name: "orange" + }))) + .id(), + get_id_via_ref( + Fruit::Juicy(FruitJuicy::Orange(FruitJuicyOrange { + id: 11, + name: "orange" + })) + .to_ref() + ) + ); +} + +#[test] +fn meta_variants_exist_specific_attributes() { + #[superstruct( + meta_variants(One, Two), + variants(IsCopy, IsNotCopy), + variant_attributes(derive(Debug, PartialEq, Clone)), + specific_variant_attributes(IsCopy(derive(Copy))) + )] + #[derive(Clone, PartialEq, Debug)] + pub struct Thing { + pub x: u64, + #[superstruct(only(IsNotCopy))] + pub y: String, + } + + fn copy(t: T) -> (T, T) { + (t, t) + } + + let x = ThingOneIsCopy { x: 0 }; + assert_eq!(copy(x), (x, x)); + let x = ThingTwoIsCopy { x: 0 }; + assert_eq!(copy(x), (x, x)); +} + +#[test] +fn meta_variants_have_specific_attributes() { + #[superstruct( + meta_variants(IsCopy, IsNotCopy), + variants(One, Two), + variant_attributes(derive(Debug, PartialEq, Clone)), + specific_variant_attributes(IsCopy(derive(Copy))) + )] + #[derive(Clone, PartialEq, Debug)] + pub struct Ting { + pub x: u64, + #[superstruct(meta_only(IsNotCopy))] + pub y: String, + } + + fn copy(t: T) -> (T, T) { + (t, t) + } + + let x = TingIsCopyOne { x: 0 }; + assert_eq!(copy(x), (x, x)); + let x = TingIsCopyTwo { x: 0 }; + assert_eq!(copy(x), (x, x)); +} From 93a25134d61fa2b6f17ee914f79a14df1ff1953a Mon Sep 17 00:00:00 2001 From: realbigsean Date: Fri, 2 Feb 2024 12:01:39 -0500 Subject: [PATCH 5/8] allow flatten and meta only with single variant --- src/lib.rs | 52 +++++++++++++++++++++++++++------------- tests/meta_variant.rs | 55 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 17 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e228bc4d..5b10f1a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -284,8 +284,6 @@ pub fn superstruct(args: TokenStream, input: TokenStream) -> TokenStream { panic!("can't set `partial_getter` options on common field"); } else if field_opts.flatten.is_some() && field_opts.only.is_some() { panic!("can't set `flatten` and `only` on the same field"); - } else if field_opts.flatten.is_some() && field_opts.meta_only.is_some() { - panic!("can't set `flatten` and `meta_only` on the same field"); } else if field_opts.flatten.is_some() && field_opts.getter.is_some() { panic!("can't set `flatten` and `getter` on the same field"); } else if field_opts.flatten.is_some() && field_opts.partial_getter.is_some() { @@ -300,12 +298,14 @@ pub fn superstruct(args: TokenStream, input: TokenStream) -> TokenStream { let variant = &variant_key.variant; let meta_variant = variant_key.meta_variant.as_ref(); - let variant_field_index = variant_fields + let Some(variant_field_index) = variant_fields .get(&variant_key) .expect("invalid variant name") .iter() .position(|f| f.ident.as_ref() == Some(&name)) - .expect("flattened fields are present on all variants"); + else { + continue; + }; if should_skip( variant_names, @@ -338,20 +338,38 @@ pub fn superstruct(args: TokenStream, input: TokenStream) -> TokenStream { let (next_variant_ty_name, partial_getter_rename) = if let Some(meta_variant) = meta_variant { - ( - format_ident!( - "{}{}{}", - last_segment_mut_ref.clone(), + if let Some(meta_only) = field_opts.meta_only.as_ref() { + assert_eq!( + meta_only.len(), + 1, + "when used in combination with flatten, only + one meta variant specification is allowed" + ); + assert_eq!( + meta_only.keys().next().unwrap(), meta_variant, - variant - ), - format_ident!( - "{}_{}_{}", - name, - meta_variant.to_string().to_lowercase(), - variant.to_string().to_lowercase() - ), - ) + "flattened meta variatn does not match" + ); + ( + format_ident!("{}{}", last_segment_mut_ref.clone(), variant), + format_ident!("{}_{}", name, variant.to_string().to_lowercase()), + ) + } else { + ( + format_ident!( + "{}{}{}", + last_segment_mut_ref.clone(), + meta_variant, + variant + ), + format_ident!( + "{}_{}_{}", + name, + meta_variant.to_string().to_lowercase(), + variant.to_string().to_lowercase() + ), + ) + } } else { ( format_ident!("{}{}", last_segment_mut_ref.clone(), variant), diff --git a/tests/meta_variant.rs b/tests/meta_variant.rs index a8db818b..1ba12e86 100644 --- a/tests/meta_variant.rs +++ b/tests/meta_variant.rs @@ -228,3 +228,58 @@ fn meta_variants_have_specific_attributes() { let x = TingIsCopyTwo { x: 0 }; assert_eq!(copy(x), (x, x)); } + +#[test] +fn meta_only_flatten() { + #[superstruct( + variants(Merge, Capella), + variant_attributes(derive(Debug, PartialEq, Clone)), + specific_variant_attributes(IsCopy(derive(Copy))) + )] + #[derive(Clone, PartialEq, Debug)] + pub struct Payload { + pub transactions: u64, + } + + #[superstruct( + variants(Merge, Capella), + variant_attributes(derive(Debug, PartialEq, Clone)), + specific_variant_attributes(IsCopy(derive(Copy))) + )] + #[derive(Clone, PartialEq, Debug)] + pub struct PayloadHeader { + pub transactions_root: u64, + } + + #[superstruct( + meta_variants(Blinded, Full), + variants(Base, Merge, Capella), + variant_attributes(derive(Debug, PartialEq, Clone)), + specific_variant_attributes(IsCopy(derive(Copy))) + )] + #[derive(Clone, PartialEq, Debug)] + pub struct Block { + #[superstruct(flatten(Merge, Capella), meta_only(Full))] + pub payload: Payload, + #[superstruct(flatten(Merge, Capella), meta_only(Blinded))] + pub payload_header: PayloadHeader, + } + + let block = Block::Full(BlockFull::Merge(BlockFullMerge { + payload: PayloadMerge { transactions: 1 }, + })); + let blinded_block = Block::Blinded(BlockBlinded::Merge(BlockBlindedMerge { + payload_header: PayloadHeaderMerge { + transactions_root: 1, + }, + })); + + assert_eq!(block.payload_merge().unwrap().transactions, 1); + assert_eq!( + blinded_block + .payload_header_merge() + .unwrap() + .transactions_root, + 1 + ); +} From 098f5a8cbc92fca712b182a7599a7144c22ad76d Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 5 Feb 2024 14:30:47 +1100 Subject: [PATCH 6/8] update docs --- book/src/codegen/meta-variants.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/book/src/codegen/meta-variants.md b/book/src/codegen/meta-variants.md index af5d366a..72707822 100644 --- a/book/src/codegen/meta-variants.md +++ b/book/src/codegen/meta-variants.md @@ -2,7 +2,7 @@ Meta variants are an optional feature, useful for scenarios where you'd want nested enums at the top-level. structs will be created for all combinations of `meta_variants` -and `variants`, names in the format `{BaseName}{MetaVariantName}{VariantName}`. +and `variants`, names in the format `{BaseName}{MetaVariantName}{VariantName}`. Additionally, enums will be created for each `meta_variant` named `{BaseName}{MetaVariantName}`. For example: @@ -20,23 +20,23 @@ struct MyStruct { } ``` -Here the `BaseName` is `MyStruct` and there are two variants in the meta-enum called +Here the `BaseName` is `MyStruct` and there are two variants in the meta-enum called `Baz` and `Qux`. The generated enums are: ```rust,no_run,no_playground -enum MyStruct{ +enum MyStruct { Baz(MyStructBaz), Qux(MyStructQux), } -enum MyStructBaz{ +enum MyStructBaz { Foo(MyStructBazFoo), Bar(MyStructBazBar), } -enum MyStructQux{ +enum MyStructQux { Foo(MyStructQuxFoo), Bar(MyStructQuxBar), } From a30709a1c04051efc1955a6a5d8980a6b847afea Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 5 Feb 2024 14:32:29 +1100 Subject: [PATCH 7/8] Remove unused specific_variant_attributes --- tests/meta_variant.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/meta_variant.rs b/tests/meta_variant.rs index 1ba12e86..8cf264f7 100644 --- a/tests/meta_variant.rs +++ b/tests/meta_variant.rs @@ -233,8 +233,7 @@ fn meta_variants_have_specific_attributes() { fn meta_only_flatten() { #[superstruct( variants(Merge, Capella), - variant_attributes(derive(Debug, PartialEq, Clone)), - specific_variant_attributes(IsCopy(derive(Copy))) + variant_attributes(derive(Debug, PartialEq, Clone)) )] #[derive(Clone, PartialEq, Debug)] pub struct Payload { @@ -243,8 +242,7 @@ fn meta_only_flatten() { #[superstruct( variants(Merge, Capella), - variant_attributes(derive(Debug, PartialEq, Clone)), - specific_variant_attributes(IsCopy(derive(Copy))) + variant_attributes(derive(Debug, PartialEq, Clone)) )] #[derive(Clone, PartialEq, Debug)] pub struct PayloadHeader { @@ -254,8 +252,7 @@ fn meta_only_flatten() { #[superstruct( meta_variants(Blinded, Full), variants(Base, Merge, Capella), - variant_attributes(derive(Debug, PartialEq, Clone)), - specific_variant_attributes(IsCopy(derive(Copy))) + variant_attributes(derive(Debug, PartialEq, Clone)) )] #[derive(Clone, PartialEq, Debug)] pub struct Block { From b630651ae1c1ac867ae65b991d3b59a31f8492cc Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 5 Feb 2024 16:32:21 +1100 Subject: [PATCH 8/8] Some clean-ups while reviewing --- src/lib.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5b10f1a3..455b53af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -249,18 +249,18 @@ pub fn superstruct(args: TokenStream, input: TokenStream) -> TokenStream { |meta_only| meta_only.keys().cloned().map(Some).collect_vec(), ); - let is_common_meta = opts.meta_variants.as_ref().map_or(true, |mv| { - field_opts - .meta_only - .as_ref() - .map_or(true, |field_meta| field_meta.len() == mv.idents.len()) - }); - let is_common = (field_variants.len() == variant_names.len()) && is_common_meta; + // Field is common if it is part of every meta variant AND every variant. + let is_common_meta = opts + .meta_variants + .as_ref() + .map_or(true, |struct_meta_variants| { + struct_meta_variants.idents.len() == field_meta_variants.len() + }); + let is_common = field_variants.len() == variant_names.len() && is_common_meta; let only_combinations = field_variants .iter() - .cartesian_product(field_meta_variants.iter()) - .clone(); + .cartesian_product(field_meta_variants.iter()); for (variant, meta_variant) in only_combinations.clone() { variant_fields @@ -268,7 +268,7 @@ pub fn superstruct(args: TokenStream, input: TokenStream) -> TokenStream { variant: variant.clone(), meta_variant: meta_variant.clone(), }) - .expect("invalid variant name in `only`") + .expect("invalid variant name in `only` or `meta_only`") .push(output_field.clone()); } @@ -342,13 +342,13 @@ pub fn superstruct(args: TokenStream, input: TokenStream) -> TokenStream { assert_eq!( meta_only.len(), 1, - "when used in combination with flatten, only + "when used in combination with flatten, only \ one meta variant specification is allowed" ); assert_eq!( meta_only.keys().next().unwrap(), meta_variant, - "flattened meta variatn does not match" + "flattened meta variant does not match" ); ( format_ident!("{}{}", last_segment_mut_ref.clone(), variant),