magicstatemachines/macros/state_union.rs
1/// Defines a named union of concrete state markers.
2///
3/// A state union is useful when several states support the same read-only API,
4/// or when a method may return one of several concrete states. The macro
5/// generates three public concepts:
6///
7/// - a marker ZST such as `Online`;
8/// - a sealed membership trait such as `InOnline`, implemented for the listed
9/// concrete states and for the union-erased marker itself;
10/// - a value-carrying enum such as `OnlineEnum<Storage, T>`, whose variants
11/// hold the concrete `State<Storage, T, Connected>` /
12/// `State<Storage, T, Authenticated>` values;
13/// - an [`In<Online>`](crate::In) implementation for every member, which is the
14/// generic form used by helpers that take the union marker as a type
15/// parameter;
16/// - a [`StateUnionDiscriminant`](crate::StateUnionDiscriminant)
17/// implementation tying `Online` to `OnlineEnum`.
18///
19/// The generated marker and membership trait are useful at the type level:
20///
21/// ```
22/// use magicstatemachines::{
23/// ConcreteStateKind, In, StateMarker, StateUnion, StateUnionDiscriminant,
24/// States, UnionStateKind,
25/// };
26///
27/// States! {
28/// Connected;
29/// Authenticated;
30/// }
31///
32/// StateUnion!(Online: Connected | Authenticated);
33///
34/// fn accepts_generated_trait<T: InOnline>() {}
35/// fn accepts_generic_trait<T: In<Online>>() {}
36/// fn assert_union_marker<T: StateMarker<Kind = UnionStateKind>>() {}
37/// fn assert_concrete_marker<T: StateMarker<Kind = ConcreteStateKind>>() {}
38///
39/// accepts_generated_trait::<Connected>();
40/// accepts_generic_trait::<Authenticated>();
41/// assert_union_marker::<Online>();
42/// assert_concrete_marker::<Connected>();
43/// ```
44///
45/// For `StateUnion!(Online: Connected | Authenticated)`, APIs can write
46/// `impl InOnline` when they need "any online state":
47///
48/// ```ignore
49/// use magicstatemachines::{SRef, State};
50/// use test_def::InOnline;
51///
52/// impl Connection {
53/// fn endpoint<S>(self: &State<S, Self, impl InOnline>) -> &str
54/// where
55/// S: SRef,
56/// {
57/// &self.endpoint
58/// }
59/// }
60/// ```
61///
62/// When runtime branching is needed, convert a concrete member into the
63/// generated enum through [`EnumExt`](crate::EnumExt), or first convert to a
64/// [`DiscriminatedState`](crate::DiscriminatedState) with
65/// [`In::into_discriminated`](crate::In::into_discriminated) and then call
66/// [`DiscriminatedState::discriminate`](crate::DiscriminatedState):
67///
68/// ```ignore
69/// use magicstatemachines::{DiscriminatedState, EnumExt, State};
70/// use test_def::{Online, OnlineEnum};
71///
72/// fn handle_online<S>(
73/// state: State<S, Connection, impl InOnline>,
74/// ) -> DiscriminatedState<S, Connection, Online>
75/// where
76/// S: magicstatemachines::SRef,
77/// {
78/// <_>::into_discriminated(state)
79/// }
80///
81/// match handle_online(state).discriminate() {
82/// OnlineEnum::Connected(connected) => {
83/// // `connected` is State<S, Connection, Connected>.
84/// }
85/// OnlineEnum::Authenticated(authenticated) => {
86/// // `authenticated` is State<S, Connection, Authenticated>.
87/// }
88/// }
89///
90/// // Equivalent convenience form when the marker value is in scope:
91/// let online_enum = Online.into_enum(state);
92/// ```
93///
94/// In a match, every enum variant contains a concrete state again. That is why
95/// a branch can call methods that require `Connected` or `Authenticated`
96/// specifically, while the enum as a whole dereferences through the union-erased
97/// state.
98///
99/// A discriminated union state can also be converted back into a union-typed
100/// state with the enum's generated `into_erased()` method. That is useful when
101/// matching temporarily recovers a concrete variant but the API should return
102/// the broader union type again.
103///
104/// The supported forms are:
105///
106/// - `StateUnion!(Online: Connected | Authenticated)` creates marker `Online`,
107/// trait `InOnline`, and enum `OnlineEnum`.
108/// - `StateUnion!(Online: Parent, Connected | Authenticated)` additionally
109/// makes `InOnline` extend `InParent`.
110/// - `StateUnion!(Online, enum CustomOnline: Connected | Authenticated)`
111/// creates marker `Online`, trait `InOnline`, and enum `CustomOnline`.
112/// - `StateUnion!(enum OnlineEnum: Connected | Authenticated)` creates only an
113/// enum. This is useful when you need a discriminating value but do not want
114/// to publish a named union marker.
115///
116/// Super-unions are written before the comma. In this example every `Online`
117/// state is also an `AllMarker` state, so `InOnline` extends `InAllMarker`:
118///
119/// ```ignore
120/// StateUnion!(AllMarker: Disconnected | Connected | Authenticated);
121/// StateUnion!(Online: AllMarker, Connected | Authenticated);
122/// ```
123///
124/// Generated membership traits are sealed and cannot be implemented
125/// downstream:
126///
127/// ```compile_fail
128/// use magicstatemachines::StateUnion;
129///
130/// struct Connected;
131/// struct Other;
132///
133/// StateUnion!(Online: Connected);
134/// impl InOnline for Other {}
135/// ```
136///
137/// A joint-state transition exists only when every member supports the same
138/// target and function signature. [`StateMachineImpl!`](macro@crate::StateMachineImpl)
139/// adds one more requirement for the `transition!(const ...)` form: every
140/// member must also share the same effect body. If bodies differ, use
141/// `transition!(dyn Online state)` instead so the concrete variant is
142/// discriminated before the effect is selected.
143///
144/// This is the type-level reason a static union transition can fail at compile
145/// time:
146///
147/// ```compile_fail
148/// use magicstatemachines::{StateUnion, StateUnionState, Transition};
149///
150/// struct Machine;
151/// struct Connected;
152/// struct Authenticated;
153/// struct Disconnected;
154///
155/// impl Transition<Connected, Disconnected> for Machine {}
156/// StateUnion!(Online: Connected | Authenticated);
157///
158/// fn requires_disconnect<From>()
159/// where
160/// Machine: Transition<From, Disconnected>,
161/// {}
162///
163/// requires_disconnect::<StateUnionState<Online>>();
164/// ```
165///
166/// `DiscriminatedState<Storage, T, Online>` carries the concrete variant in
167/// its storage. Calling `discriminate()` recovers the generated enum when
168/// runtime branching is needed. The marker also names that enum through
169/// [`StateUnionDiscriminant`](crate::StateUnionDiscriminant), so
170/// `<Online as StateUnionDiscriminant>::Enum<Storage, T>` is
171/// `OnlineEnum<Storage, T>`.
172#[macro_export]
173macro_rules! StateUnion {
174 (
175 $name:ident:
176 $first:ident $(| $state:ident)* $(,)?
177 ) => {
178 $crate::__private::paste! {
179 $crate::__StateUnion!(
180 @trait $name [] [enum [<$name Enum>]]:
181 $first $(| $state)*
182 );
183 $crate::__StateUnion!(
184 @enum [<$name Enum>] $name:
185 $first $(| $state)*
186 );
187 }
188 };
189 (
190 $name:ident, enum $enum_name:ident:
191 $first:ident $(| $state:ident)* $(,)?
192 ) => {
193 $crate::__StateUnion!(@trait $name [] [enum $enum_name]: $first $(| $state)*);
194 $crate::__private::paste! {
195 $crate::__StateUnion!(
196 @enum $enum_name $name:
197 $first $(| $state)*
198 );
199 }
200 };
201 (
202 $name:ident:
203 $first_super:ident $(+ $supertrait:ident)*,
204 $first:ident $(| $state:ident)* $(,)?
205 ) => {
206 $crate::__private::paste! {
207 $crate::__StateUnion!(
208 @trait $name [$first_super $(, $supertrait)*] [enum [<$name Enum>]]:
209 $first $(| $state)*
210 );
211 $crate::__StateUnion!(
212 @enum [<$name Enum>] $name:
213 $first $(| $state)*
214 );
215 }
216 };
217 (
218 $name:ident:
219 $first_super:ident $(+ $supertrait:ident)*,
220 enum $enum_name:ident:
221 $first:ident $(| $state:ident)* $(,)?
222 ) => {
223 $crate::__StateUnion!(
224 @trait $name [$first_super $(, $supertrait)*] [enum $enum_name]:
225 $first $(| $state)*
226 );
227 $crate::__private::paste! {
228 $crate::__StateUnion!(
229 @enum $enum_name $name:
230 $first $(| $state)*
231 );
232 }
233 };
234 (
235 enum $enum_name:ident:
236 $first:ident $(| $state:ident)* $(,)?
237 ) => {
238 $crate::__StateUnion!(@standalone_enum $enum_name: $first $(| $state)*);
239 };
240}