From 4732c9fc30a81c565db221f066b464fe3ba0fa4f Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 19 Jun 2023 12:44:11 +1000 Subject: [PATCH] Add Bimapping (#1) --- metastruct_macro/src/lib.rs | 32 +++++++++ metastruct_macro/src/mapping.rs | 103 +++++++++++++++++++++++++++- metastruct_macro/tests/bimapping.rs | 74 ++++++++++++++++++++ 3 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 metastruct_macro/tests/bimapping.rs diff --git a/metastruct_macro/src/lib.rs b/metastruct_macro/src/lib.rs index ad5fb52..93bbe7b 100644 --- a/metastruct_macro/src/lib.rs +++ b/metastruct_macro/src/lib.rs @@ -23,6 +23,25 @@ struct MappingOpts { groups: Option, } +#[derive(Debug, FromMeta)] +struct BiMappingOpts { + other_type: Ident, + #[darling(default)] + self_by_value: bool, + #[darling(default)] + self_mutable: bool, + #[darling(default)] + other_by_value: bool, + #[darling(default)] + other_mutable: bool, + #[darling(default)] + exclude: Option, + #[darling(default)] + fallible: bool, + #[darling(default)] + groups: Option, +} + #[derive(Debug, FromMeta)] struct NumFieldsOpts { #[darling(default)] @@ -51,6 +70,8 @@ struct FieldOpts { struct StructOpts { #[darling(default)] mappings: HashMap, + #[darling(default)] + bimappings: HashMap, // FIXME(sproul): the `Ident` is kind of useless here, consider writing a custom FromMeta #[darling(default)] num_fields: HashMap, @@ -105,6 +126,17 @@ pub fn metastruct(args: TokenStream, input: TokenStream) -> TokenStream { )); } + // Generate bi-mapping macros. + for (mapping_macro_name, mapping_opts) in &opts.bimappings { + output_items.push(mapping::generate_bimapping_macro( + mapping_macro_name, + type_name, + &fields, + &field_opts, + mapping_opts, + )); + } + // Generate `NumFields` implementations. for (_, num_fields_opts) in &opts.num_fields { output_items.push(num_fields::generate_num_fields_impl( diff --git a/metastruct_macro/src/mapping.rs b/metastruct_macro/src/mapping.rs index d844385..b167a2d 100644 --- a/metastruct_macro/src/mapping.rs +++ b/metastruct_macro/src/mapping.rs @@ -1,4 +1,5 @@ -use crate::{exclude::calculate_excluded_fields, FieldOpts, MappingOpts}; +use crate::{exclude::calculate_excluded_fields, BiMappingOpts, FieldOpts, MappingOpts}; +use itertools::Itertools; use proc_macro::TokenStream; use quote::quote; use syn::{Ident, Type}; @@ -74,3 +75,103 @@ pub(crate) fn generate_mapping_macro( } .into() } + +pub(crate) fn generate_bimapping_macro( + macro_name: &Ident, + left_type_name: &Ident, + left_fields: &[(Ident, Type)], + left_field_opts: &[FieldOpts], + mapping_opts: &BiMappingOpts, +) -> TokenStream { + let right_type_name = &mapping_opts.other_type; + let exclude_idents = calculate_excluded_fields( + &mapping_opts.exclude, + &mapping_opts.groups, + left_fields, + left_field_opts, + ); + let (left_selected_fields, right_selected_fields, left_selected_field_types): ( + Vec<_>, + Vec<_>, + Vec<_>, + ) = left_fields + .iter() + .filter(|(field_name, _)| !exclude_idents.contains(&field_name)) + .map(|(field_name, left_type)| { + let right_field_name = Ident::new(&format!("{field_name}_r"), field_name.span()); + (field_name, right_field_name, left_type) + }) + .multiunzip(); + + assert!( + !mapping_opts.self_by_value || !mapping_opts.self_mutable, + "self cannot be mapped both by value and by mutable reference" + ); + assert!( + !mapping_opts.other_by_value || !mapping_opts.other_mutable, + "other cannot be mapped both by value and by mutable reference" + ); + let (left_field_ref, left_field_ref_typ) = if mapping_opts.self_by_value { + (quote! {}, quote! {}) + } else if mapping_opts.self_mutable { + (quote! { ref mut }, quote! { &'_ mut }) + } else { + (quote! { ref }, quote! { &'_ }) + }; + let (right_field_ref, right_field_ref_typ) = if mapping_opts.other_by_value { + (quote! {}, quote! {}) + } else if mapping_opts.other_mutable { + (quote! { ref mut }, quote! { &'_ mut }) + } else { + (quote! { ref }, quote! { &'_ }) + }; + + let mapping_function_types = left_selected_field_types + .iter() + .map(|field_type| { + quote! { &mut dyn FnMut(usize, #left_field_ref_typ #field_type, #right_field_ref_typ _) -> _ } + }) + .collect::>(); + + let function_call_exprs = left_selected_fields + .iter() + .zip(&right_selected_fields) + .map(|(left_field, right_field)| { + if mapping_opts.fallible { + quote! { __metastruct_f(__metastruct_i, #left_field, #right_field)? } + } else { + quote! { __metastruct_f(__metastruct_i, #left_field, #right_field) } + } + }) + .collect::>(); + + quote! { + #[macro_export] + macro_rules! #macro_name { + ($left:expr, $right:expr, $f:expr) => { + match ($left, $right) { + (#left_type_name { + #( + #left_field_ref #left_selected_fields, + )* + .. + }, + #right_type_name { + #( + #left_selected_fields: #right_field_ref #right_selected_fields, + )* + .. + }) => { + let mut __metastruct_i: usize = 0; + #( + let __metastruct_f: #mapping_function_types = &mut $f; + #function_call_exprs; + __metastruct_i += 1; + )* + } + } + } + } + } + .into() +} diff --git a/metastruct_macro/tests/bimapping.rs b/metastruct_macro/tests/bimapping.rs new file mode 100644 index 0000000..b550f97 --- /dev/null +++ b/metastruct_macro/tests/bimapping.rs @@ -0,0 +1,74 @@ +use metastruct_macro::metastruct; + +#[metastruct(bimappings( + bimap_foo(other_type = "Foo", self_mutable, other_by_value), + bimap_foo_into_foo(other_type = "IntoFoo", self_mutable, other_by_value) +))] +#[derive(Debug, Clone, PartialEq)] +pub struct Foo { + a: u64, + b: u64, + #[metastruct(exclude_from(copy))] + c: String, +} + +pub struct MyString(String); + +impl From for String { + fn from(m: MyString) -> Self { + m.0 + } +} + +/// Type that has fields that can be converted to Foo's fields using `Into` +pub struct IntoFoo { + a: u32, + b: u32, + c: MyString, +} + +#[test] +fn bimap_self() { + let mut x_foo = Foo { + a: 0, + b: 1, + c: "X".to_string(), + }; + let y_foo = Foo { + a: 1000, + b: 2000, + c: "Y".to_string(), + }; + + bimap_foo!(&mut x_foo, y_foo.clone(), |_, x, y| { + *x = y; + }); + + assert_eq!(x_foo, y_foo); +} + +#[test] +fn bimap_into() { + let mut x_foo = Foo { + a: 0, + b: 1, + c: "X".to_string(), + }; + let y_foo = IntoFoo { + a: 1000, + b: 2000, + c: MyString("Y".to_string()), + }; + + fn set_from, U>(x: &mut T, y: U) { + *x = y.into(); + } + + bimap_foo_into_foo!(&mut x_foo, y_foo, |_, x, y| { + set_from(x, y); + }); + + assert_eq!(x_foo.a, 1000); + assert_eq!(x_foo.b, 2000); + assert_eq!(x_foo.c, "Y"); +}