Skip to content

Commit 3b58be0

Browse files
Generate mapping macros (#20)
* Generate mapping macros * Docs and tests
1 parent 36aba30 commit 3b58be0

File tree

12 files changed

+574
-3
lines changed

12 files changed

+574
-3
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ itertools = "0.10.1"
1919
proc-macro2 = "1.0.32"
2020
quote = "1.0.10"
2121
syn = "1.0.82"
22+
smallvec = "1.8.0"
2223

2324
[dev-dependencies]
2425
serde = { version = "1.0.130", features = ["derive"] }

book/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- [Variant structs](./codegen/variant-structs.md)
77
- [Top-level enum](./codegen/enum.md)
88
- [`Ref` and `RefMut`](./codegen/ref-and-refmut.md)
9+
- [Mapping macros](./codegen/map-macros.md)
910
- [Configuration](./config.md)
1011
- [Struct attributes](./config/struct.md)
1112
- [Field attributes](./config/field.md)

book/src/codegen.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ You should visit each of the sub-pages in order to understand how the generated
77
1. [Variant structs](./codegen/variant-structs.md).
88
2. [Top-level enum](./codegen/enum.md).
99
3. [`Ref` and `RefMut`](./codegen/ref-and-refmut.md).
10+
4. [Mapping macros](./codegen/map-macros.md).
1011

1112
## Example
1213

book/src/codegen/map-macros.md

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# Mapping macros
2+
3+
To facilitate code that is generic over all variants of a `superstruct`, we generate several
4+
_mapping macros_ with names like `map_foo!` and `map_foo_into_bar!`.
5+
6+
## Mapping into `Self`
7+
8+
For every top-level enum we generate a mapping macro that matches on values of
9+
`Self` and is equipped with a variant constructor for `Self`.
10+
11+
Consider the following type:
12+
13+
```rust
14+
#[superstruct(variants(First, Second)]
15+
struct Foo {
16+
x: u8,
17+
#[only(Second)]
18+
y: u8
19+
}
20+
```
21+
22+
The mapping macro for `Foo` will be:
23+
24+
```rust
25+
macro_rules! map_foo {
26+
($value:expr, $f:expr) => {
27+
match $value {
28+
Foo::First(inner) => f(inner, Foo::First),
29+
Foo::Second(inner) => f(inner, Foo::Second),
30+
}
31+
}
32+
}
33+
```
34+
35+
i.e. `map_foo!` is a macro taking two arguments:
36+
37+
* `value`: an expression which must be of type `Foo`.
38+
* `f`: a function expression, which takes two arguments `|inner, constructor|` where:
39+
* `inner` is an instance of a variant struct, e.g. `FooFirst`. Note that
40+
its type changes between branches!
41+
* `constructor` is a function from the selected variant struct type to `Foo`. Its type
42+
also changes between branches, and would be e.g. `fn(FooFirst) -> Foo` in the case
43+
of the `First` branch.
44+
45+
Example usage looks like this:
46+
47+
```rust
48+
impl Foo {
49+
fn increase_x(self) -> Self {
50+
map_foo!(self, |inner, constructor| {
51+
inner.x += 1;
52+
constructor(inner)
53+
})
54+
}
55+
}
56+
```
57+
58+
Although the type of `inner` could be `FooFirst` or `FooSecond`, both have an `x` field, so it is
59+
legal to increment it. The `constructor` is then used to re-construct an instance of `Foo` by
60+
injecting the updated `inner` value. If an invalid closure is provided then the type errors may
61+
be quite opaque. On the other hand, if your code type-checks while using `map!` then you can rest
62+
assured that it is valid (`superstruct` doesn't use any `unsafe` blocks or do any spicy casting).
63+
64+
> Tip: You don't need to use the constructor argument if you are implementing a straight-forward
65+
> projection on `Self`. Although in some cases you may need to provide a type
66+
> hint to the compiler, like `let _ = constructor(inner)`.
67+
68+
## Mapping from `Ref` and `RefMut`
69+
70+
Mapping macros for `Ref` and `RefMut` are also generated. They take an extra lifetime argument
71+
(supplied as a reference to `_`) as their first argument, which must correspond to the lifetime
72+
on the `Ref`/`RefMut` type.
73+
74+
Example usage for `Foo`:
75+
76+
```rust
77+
impl Foo {
78+
fn get_x<'a>(&'a self) -> &'a u64 {
79+
map_foo_ref!(&'a _, self, |inner, _| {
80+
&inner.x
81+
})
82+
}
83+
}
84+
```
85+
86+
## Mapping into other types
87+
88+
Mappings can also be generated between two `superstruct`s with identically named variants.
89+
90+
These mapping macros are available for the top-level enum, `Ref` and `RefMut`, and take the same
91+
number of arguments. The only difference is that the constructor will be the constructor for the
92+
type being mapped _into_.
93+
94+
The name of the mapping macro is `map_X_into_Y!` where `X` is the snake-cased
95+
`Self` type and `Y` is the snake-cased target type.
96+
97+
Example:
98+
99+
```rust
100+
#[superstruct(
101+
variants(A, B),
102+
variant_attributes(derive(Debug, PartialEq, Clone)),
103+
map_into(Thing2),
104+
map_ref_into(Thing2Ref),
105+
map_ref_mut_into(Thing2RefMut)
106+
)]
107+
#[derive(Debug, PartialEq, Clone)]
108+
pub struct Thing1 {
109+
#[superstruct(only(A), partial_getter(rename = "thing2a"))]
110+
thing2: Thing2A,
111+
#[superstruct(only(B), partial_getter(rename = "thing2b"))]
112+
thing2: Thing2B,
113+
}
114+
115+
#[superstruct(variants(A, B), variant_attributes(derive(Debug, PartialEq, Clone)))]
116+
#[derive(Debug, PartialEq, Clone)]
117+
pub struct Thing2 {
118+
x: u64,
119+
}
120+
121+
fn thing1_to_thing2(thing1: Thing1) -> Thing2 {
122+
map_thing1_into_thing2!(thing1, |inner, cons| { cons(inner.thing2) })
123+
}
124+
125+
fn thing1_ref_to_thing2_ref<'a>(thing1: Thing1Ref<'a>) -> Thing2Ref<'a> {
126+
map_thing1_ref_into_thing2_ref!(&'a _, thing1, |inner, cons| { cons(&inner.thing2) })
127+
}
128+
129+
fn thing1_ref_mut_to_thing2_ref_mut<'a>(thing1: Thing1RefMut<'a>) -> Thing2RefMut<'a> {
130+
map_thing1_ref_mut_into_thing2_ref_mut!(&'a _, thing1, |inner, cons| {
131+
cons(&mut inner.thing2)
132+
})
133+
}
134+
```
135+
136+
## Naming
137+
138+
Type names are converted from `CamelCase` to `snake_case` on a best-effort basis. E.g.
139+
140+
* `SignedBeaconBlock` -> `map_signed_beacon_block!`
141+
* `NetworkDht` -> `map_network_dht!`
142+
143+
The current algorithm is quite simplistic and may produce strange names if it encounters
144+
repeated capital letters. Please open an issue on GitHub if you have suggestions on how to
145+
improve this!
146+
147+
## Limitations
148+
149+
* Presently only pure mapping functions are supported. The type-hinting hacks make it hard to
150+
support proper closures.
151+
* Sometimes type-hints are required, e.g. `let _ = constructor(inner)`.
152+
* Macros are scoped per-module, so you need to be more mindful of name collisions than when
153+
defining regular types.

book/src/config/struct.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,18 @@ Provide a list of attributes to be applied verbatim to the generated `RefMut` ty
8383

8484
Disable generation of the top-level enum, and all code except the
8585
[variant structs](../codegen/variant-structs.md).
86+
87+
## Map Into
88+
89+
```
90+
#[map_into(ty1, ty2, ..)]
91+
#[map_ref_into(ty1, ty2, ..)]
92+
#[map_ref_mut_into(ty1, ty2, ..)]
93+
```
94+
95+
Generate mapping macros from the top-level enum, the `Ref` type or the `RefMut` type as appropriate.
96+
97+
Please see the documentation on [Mapping into other types](./codegen/map-macros.md#mapping-into-other-types)
98+
for an explanation of how these macros operate.
99+
100+
**Format**: one or more `superstruct` type names

src/lib.rs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use attributes::{IdentList, NestedMetaList};
22
use darling::FromMeta;
33
use itertools::Itertools;
4+
use macros::generate_all_map_macros;
45
use proc_macro::TokenStream;
5-
use proc_macro2::Span;
6+
use proc_macro2::{Span, TokenStream as TokenStream2};
67
use quote::{format_ident, quote, ToTokens};
78
use std::collections::HashMap;
89
use std::iter::{self, FromIterator};
@@ -12,6 +13,9 @@ use syn::{
1213
};
1314

1415
mod attributes;
16+
mod macros;
17+
mod naming;
18+
mod utils;
1519

1620
/// Top-level configuration via the `superstruct` attribute.
1721
#[derive(Debug, FromMeta)]
@@ -36,6 +40,18 @@ struct StructOpts {
3640
/// Turn off the generation of the top-level enum that binds the variants together.
3741
#[darling(default)]
3842
no_enum: bool,
43+
/// Turn off the generation of the map macros.
44+
#[darling(default)]
45+
no_map_macros: bool,
46+
/// List of other superstruct types to generate (owned) mappings into.
47+
#[darling(default)]
48+
map_into: Option<IdentList>,
49+
/// List of other superstruct types to generate mappings into from Ref.
50+
#[darling(default)]
51+
map_ref_into: Option<IdentList>,
52+
/// List of other superstruct types to generate mappings into from RefMut.
53+
#[darling(default)]
54+
map_ref_mut_into: Option<IdentList>,
3955
}
4056

4157
/// Field-level configuration.
@@ -472,6 +488,34 @@ pub fn superstruct(args: TokenStream, input: TokenStream) -> TokenStream {
472488
};
473489
output_items.push(ref_mut_impl_block.into());
474490

491+
// Generate the mapping macros if enabled.
492+
if !opts.no_map_macros && !opts.no_enum {
493+
let num_generics = decl_generics.params.len();
494+
generate_all_map_macros(
495+
&type_name,
496+
&ref_ty_name,
497+
&ref_mut_ty_name,
498+
num_generics,
499+
&struct_names,
500+
variant_names,
501+
&opts,
502+
&mut output_items,
503+
);
504+
} else {
505+
assert!(
506+
opts.map_into.is_none(),
507+
"`map_into` is set but map macros are disabled"
508+
);
509+
assert!(
510+
opts.map_ref_into.is_none(),
511+
"`map_ref_into` is set but map macros are disabled"
512+
);
513+
assert!(
514+
opts.map_ref_mut_into.is_none(),
515+
"`map_ref_mut_into` is set but map macros are disabled"
516+
);
517+
}
518+
475519
TokenStream::from_iter(output_items)
476520
}
477521

0 commit comments

Comments
 (0)