Skip to content

Commit

Permalink
Add Bimapping (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelsproul authored Jun 19, 2023
1 parent fef24f9 commit 4732c9f
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 1 deletion.
32 changes: 32 additions & 0 deletions metastruct_macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,25 @@ struct MappingOpts {
groups: Option<IdentList>,
}

#[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<IdentList>,
#[darling(default)]
fallible: bool,
#[darling(default)]
groups: Option<IdentList>,
}

#[derive(Debug, FromMeta)]
struct NumFieldsOpts {
#[darling(default)]
Expand Down Expand Up @@ -51,6 +70,8 @@ struct FieldOpts {
struct StructOpts {
#[darling(default)]
mappings: HashMap<Ident, MappingOpts>,
#[darling(default)]
bimappings: HashMap<Ident, BiMappingOpts>,
// FIXME(sproul): the `Ident` is kind of useless here, consider writing a custom FromMeta
#[darling(default)]
num_fields: HashMap<Ident, NumFieldsOpts>,
Expand Down Expand Up @@ -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(
Expand Down
103 changes: 102 additions & 1 deletion metastruct_macro/src/mapping.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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::<Vec<_>>();

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::<Vec<_>>();

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()
}
74 changes: 74 additions & 0 deletions metastruct_macro/tests/bimapping.rs
Original file line number Diff line number Diff line change
@@ -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<MyString> 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<T: From<U>, 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");
}

0 comments on commit 4732c9f

Please sign in to comment.