crux_macros/
effect.rs

1use heck::ToSnakeCase;
2use proc_macro2::{Span, TokenStream};
3use quote::{format_ident, quote};
4use syn::{Ident, ItemEnum, Type};
5
6struct Effect {
7    ident: Ident,
8    operation: Type,
9}
10
11#[allow(clippy::too_many_lines)]
12pub fn effect_impl(args: Option<Ident>, input: ItemEnum) -> TokenStream {
13    let enum_ident = &input.ident;
14    let has_typegen_attr = match args {
15        Some(x) if x == format_ident!("typegen") => true,
16        None => false,
17        _ => panic!("did you mean typegen?"),
18    };
19    let enum_ident_str = enum_ident.to_string();
20
21    let mut ffi_enum = input.clone();
22    ffi_enum.ident = format_ident!("{}Ffi", enum_ident);
23    ffi_enum.attrs = vec![];
24    let ffi_enum_ident = &ffi_enum.ident;
25
26    let effects = input.variants.into_iter().map(|variant| {
27        let ident = variant.ident;
28        let operation = variant
29            .fields
30            .into_iter()
31            .next()
32            .expect("each variant is expected to be a tuple with one field")
33            .ty;
34        Effect { ident, operation }
35    });
36
37    let effect_variants = effects.clone().map(|effect| {
38        let effect_ident = &effect.ident;
39        let operation = &effect.operation;
40        quote! {
41            #effect_ident(::crux_core::Request<#operation>)
42        }
43    });
44
45    let match_arms = effects.clone().map(|effect| {
46        let effect_ident = &effect.ident;
47        quote! {
48            #enum_ident::#effect_ident(request) => request.serialize(#ffi_enum_ident::#effect_ident)
49        }
50    });
51
52    let from_impls = effects.clone().map(|effect| {
53        let effect_ident = &effect.ident;
54        let operation = &effect.operation;
55        quote! {
56            impl From<::crux_core::Request<#operation>> for #enum_ident {
57                fn from(value: ::crux_core::Request<#operation>) -> Self {
58                    Self::#effect_ident(value)
59                }
60            }
61
62            impl TryFrom<#enum_ident> for ::crux_core::Request<#operation> {
63                type Error = #enum_ident;
64
65                fn try_from(value: #enum_ident) -> Result<Self, Self::Error> {
66                    if let #enum_ident::#effect_ident(value) = value {
67                        Ok(value)
68                    } else {
69                        Err(value)
70                    }
71                }
72            }
73        }
74    });
75
76    let filters = effects.clone().map(|effect| {
77        let effect_ident = &effect.ident;
78        let effect_ident_str = effect.ident.to_string();
79        let effect_ident_snake = effect_ident_str.to_snake_case();
80        let operation = &effect.operation;
81        let filter_fn = Ident::new(&format!("is_{effect_ident_snake}"), Span::call_site());
82        let map_fn = Ident::new(&format!("into_{effect_ident_snake}"), Span::call_site());
83        let expect_fn = Ident::new(&format!("expect_{effect_ident_snake}"), Span::call_site());
84        quote! {
85            impl #enum_ident {
86                pub fn #filter_fn(&self) -> bool {
87                    if let #enum_ident::#effect_ident(_) = self {
88                        true
89                    } else {
90                        false
91                    }
92                }
93                pub fn #map_fn(self) -> Option<::crux_core::Request<#operation>> {
94                    if let #enum_ident::#effect_ident(request) = self {
95                        Some(request)
96                    } else {
97                        None
98                    }
99                }
100                #[track_caller]
101                pub fn #expect_fn(self) -> ::crux_core::Request<#operation> {
102                    if let #enum_ident::#effect_ident(request) = self {
103                        request
104                    } else {
105                        panic!("not a {} effect", #effect_ident_str)
106                    }
107                }
108            }
109        }
110    });
111
112    let type_gen = if has_typegen_attr {
113        let effect_gen = effects.map(|effect| {
114            let operation = &effect.operation;
115
116            quote! {
117                #operation::register_types(generator)?;
118            }
119        });
120        quote! {
121            #[cfg(feature = "typegen")]
122            impl crux_core::typegen::Export for #enum_ident {
123                fn register_types(generator: &mut ::crux_core::typegen::TypeGen) -> ::crux_core::typegen::Result {
124                    use ::crux_core::capability::{Capability, Operation};
125                    #(#effect_gen)*
126                    generator.register_type::<#ffi_enum_ident>()?;
127                    generator.register_type::<::crux_core::bridge::Request<#ffi_enum_ident>>()?;
128
129                    Ok(())
130                }
131            }
132        }
133    } else {
134        quote! {}
135    };
136
137    quote! {
138        #[derive(Debug)]
139        pub enum #enum_ident {
140            #(#effect_variants ,)*
141        }
142
143        #[derive(::serde::Serialize, ::serde::Deserialize)]
144        #[serde(rename = #enum_ident_str)]
145        #ffi_enum
146
147        impl crux_core::Effect for #enum_ident {
148            type Ffi = #ffi_enum_ident;
149            fn serialize(self) -> (Self::Ffi, crux_core::bridge::ResolveSerialized) {
150                match self {
151                    #(#match_arms ,)*
152                }
153            }
154        }
155
156        #(#from_impls)*
157
158        #(#filters)*
159
160        #type_gen
161
162    }
163}
164
165#[cfg(test)]
166mod test {
167    use syn::parse_quote;
168
169    use crate::pretty_print;
170
171    use super::*;
172
173    #[test]
174    #[should_panic(expected = "did you mean typegen?")]
175    fn bad_args() {
176        let args = Some(format_ident!("typo"));
177        let input = parse_quote! {
178            pub enum Effect {
179                Render(RenderOperation),
180            }
181        };
182
183        effect_impl(args, input);
184    }
185
186    #[test]
187    fn single_with_typegen() {
188        let args = Some(format_ident!("typegen"));
189        let input = parse_quote! {
190            pub enum Effect {
191                Render(RenderOperation),
192            }
193        };
194
195        let actual = effect_impl(args, input);
196
197        insta::assert_snapshot!(pretty_print(&actual), @r###"
198        #[derive(Debug)]
199        pub enum Effect {
200            Render(::crux_core::Request<RenderOperation>),
201        }
202        #[derive(::serde::Serialize, ::serde::Deserialize)]
203        #[serde(rename = "Effect")]
204        pub enum EffectFfi {
205            Render(RenderOperation),
206        }
207        impl crux_core::Effect for Effect {
208            type Ffi = EffectFfi;
209            fn serialize(self) -> (Self::Ffi, crux_core::bridge::ResolveSerialized) {
210                match self {
211                    Effect::Render(request) => request.serialize(EffectFfi::Render),
212                }
213            }
214        }
215        impl From<::crux_core::Request<RenderOperation>> for Effect {
216            fn from(value: ::crux_core::Request<RenderOperation>) -> Self {
217                Self::Render(value)
218            }
219        }
220        impl TryFrom<Effect> for ::crux_core::Request<RenderOperation> {
221            type Error = Effect;
222            fn try_from(value: Effect) -> Result<Self, Self::Error> {
223                if let Effect::Render(value) = value { Ok(value) } else { Err(value) }
224            }
225        }
226        impl Effect {
227            pub fn is_render(&self) -> bool {
228                if let Effect::Render(_) = self { true } else { false }
229            }
230            pub fn into_render(self) -> Option<::crux_core::Request<RenderOperation>> {
231                if let Effect::Render(request) = self { Some(request) } else { None }
232            }
233            #[track_caller]
234            pub fn expect_render(self) -> ::crux_core::Request<RenderOperation> {
235                if let Effect::Render(request) = self {
236                    request
237                } else {
238                    panic!("not a {} effect", "Render")
239                }
240            }
241        }
242        #[cfg(feature = "typegen")]
243        impl crux_core::typegen::Export for Effect {
244            fn register_types(
245                generator: &mut ::crux_core::typegen::TypeGen,
246            ) -> ::crux_core::typegen::Result {
247                use ::crux_core::capability::{Capability, Operation};
248                RenderOperation::register_types(generator)?;
249                generator.register_type::<EffectFfi>()?;
250                generator.register_type::<::crux_core::bridge::Request<EffectFfi>>()?;
251                Ok(())
252            }
253        }
254        "###);
255    }
256
257    #[test]
258    fn single_with_new_name() {
259        let args = Some(format_ident!("typegen"));
260        let input = parse_quote! {
261            pub enum MyEffect {
262                Render(RenderOperation),
263            }
264        };
265
266        let actual = effect_impl(args, input);
267
268        insta::assert_snapshot!(pretty_print(&actual), @r#"
269        #[derive(Debug)]
270        pub enum MyEffect {
271            Render(::crux_core::Request<RenderOperation>),
272        }
273        #[derive(::serde::Serialize, ::serde::Deserialize)]
274        #[serde(rename = "MyEffect")]
275        pub enum MyEffectFfi {
276            Render(RenderOperation),
277        }
278        impl crux_core::Effect for MyEffect {
279            type Ffi = MyEffectFfi;
280            fn serialize(self) -> (Self::Ffi, crux_core::bridge::ResolveSerialized) {
281                match self {
282                    MyEffect::Render(request) => request.serialize(MyEffectFfi::Render),
283                }
284            }
285        }
286        impl From<::crux_core::Request<RenderOperation>> for MyEffect {
287            fn from(value: ::crux_core::Request<RenderOperation>) -> Self {
288                Self::Render(value)
289            }
290        }
291        impl TryFrom<MyEffect> for ::crux_core::Request<RenderOperation> {
292            type Error = MyEffect;
293            fn try_from(value: MyEffect) -> Result<Self, Self::Error> {
294                if let MyEffect::Render(value) = value { Ok(value) } else { Err(value) }
295            }
296        }
297        impl MyEffect {
298            pub fn is_render(&self) -> bool {
299                if let MyEffect::Render(_) = self { true } else { false }
300            }
301            pub fn into_render(self) -> Option<::crux_core::Request<RenderOperation>> {
302                if let MyEffect::Render(request) = self { Some(request) } else { None }
303            }
304            #[track_caller]
305            pub fn expect_render(self) -> ::crux_core::Request<RenderOperation> {
306                if let MyEffect::Render(request) = self {
307                    request
308                } else {
309                    panic!("not a {} effect", "Render")
310                }
311            }
312        }
313        #[cfg(feature = "typegen")]
314        impl crux_core::typegen::Export for MyEffect {
315            fn register_types(
316                generator: &mut ::crux_core::typegen::TypeGen,
317            ) -> ::crux_core::typegen::Result {
318                use ::crux_core::capability::{Capability, Operation};
319                RenderOperation::register_types(generator)?;
320                generator.register_type::<MyEffectFfi>()?;
321                generator.register_type::<::crux_core::bridge::Request<MyEffectFfi>>()?;
322                Ok(())
323            }
324        }
325        "#);
326    }
327
328    #[test]
329    fn single_without_typegen() {
330        let input = parse_quote! {
331            pub enum Effect {
332                Render(RenderOperation),
333            }
334        };
335
336        let actual = effect_impl(None, input);
337
338        insta::assert_snapshot!(pretty_print(&actual), @r###"
339        #[derive(Debug)]
340        pub enum Effect {
341            Render(::crux_core::Request<RenderOperation>),
342        }
343        #[derive(::serde::Serialize, ::serde::Deserialize)]
344        #[serde(rename = "Effect")]
345        pub enum EffectFfi {
346            Render(RenderOperation),
347        }
348        impl crux_core::Effect for Effect {
349            type Ffi = EffectFfi;
350            fn serialize(self) -> (Self::Ffi, crux_core::bridge::ResolveSerialized) {
351                match self {
352                    Effect::Render(request) => request.serialize(EffectFfi::Render),
353                }
354            }
355        }
356        impl From<::crux_core::Request<RenderOperation>> for Effect {
357            fn from(value: ::crux_core::Request<RenderOperation>) -> Self {
358                Self::Render(value)
359            }
360        }
361        impl TryFrom<Effect> for ::crux_core::Request<RenderOperation> {
362            type Error = Effect;
363            fn try_from(value: Effect) -> Result<Self, Self::Error> {
364                if let Effect::Render(value) = value { Ok(value) } else { Err(value) }
365            }
366        }
367        impl Effect {
368            pub fn is_render(&self) -> bool {
369                if let Effect::Render(_) = self { true } else { false }
370            }
371            pub fn into_render(self) -> Option<::crux_core::Request<RenderOperation>> {
372                if let Effect::Render(request) = self { Some(request) } else { None }
373            }
374            #[track_caller]
375            pub fn expect_render(self) -> ::crux_core::Request<RenderOperation> {
376                if let Effect::Render(request) = self {
377                    request
378                } else {
379                    panic!("not a {} effect", "Render")
380                }
381            }
382        }
383        "###);
384    }
385
386    #[test]
387    fn multiple_with_typegen() {
388        let args = Some(format_ident!("typegen"));
389        let input = parse_quote! {
390            pub enum Effect {
391                Render(RenderOperation),
392                Http(HttpRequest),
393            }
394        };
395
396        let actual = effect_impl(args, input);
397
398        insta::assert_snapshot!(pretty_print(&actual), @r###"
399        #[derive(Debug)]
400        pub enum Effect {
401            Render(::crux_core::Request<RenderOperation>),
402            Http(::crux_core::Request<HttpRequest>),
403        }
404        #[derive(::serde::Serialize, ::serde::Deserialize)]
405        #[serde(rename = "Effect")]
406        pub enum EffectFfi {
407            Render(RenderOperation),
408            Http(HttpRequest),
409        }
410        impl crux_core::Effect for Effect {
411            type Ffi = EffectFfi;
412            fn serialize(self) -> (Self::Ffi, crux_core::bridge::ResolveSerialized) {
413                match self {
414                    Effect::Render(request) => request.serialize(EffectFfi::Render),
415                    Effect::Http(request) => request.serialize(EffectFfi::Http),
416                }
417            }
418        }
419        impl From<::crux_core::Request<RenderOperation>> for Effect {
420            fn from(value: ::crux_core::Request<RenderOperation>) -> Self {
421                Self::Render(value)
422            }
423        }
424        impl TryFrom<Effect> for ::crux_core::Request<RenderOperation> {
425            type Error = Effect;
426            fn try_from(value: Effect) -> Result<Self, Self::Error> {
427                if let Effect::Render(value) = value { Ok(value) } else { Err(value) }
428            }
429        }
430        impl From<::crux_core::Request<HttpRequest>> for Effect {
431            fn from(value: ::crux_core::Request<HttpRequest>) -> Self {
432                Self::Http(value)
433            }
434        }
435        impl TryFrom<Effect> for ::crux_core::Request<HttpRequest> {
436            type Error = Effect;
437            fn try_from(value: Effect) -> Result<Self, Self::Error> {
438                if let Effect::Http(value) = value { Ok(value) } else { Err(value) }
439            }
440        }
441        impl Effect {
442            pub fn is_render(&self) -> bool {
443                if let Effect::Render(_) = self { true } else { false }
444            }
445            pub fn into_render(self) -> Option<::crux_core::Request<RenderOperation>> {
446                if let Effect::Render(request) = self { Some(request) } else { None }
447            }
448            #[track_caller]
449            pub fn expect_render(self) -> ::crux_core::Request<RenderOperation> {
450                if let Effect::Render(request) = self {
451                    request
452                } else {
453                    panic!("not a {} effect", "Render")
454                }
455            }
456        }
457        impl Effect {
458            pub fn is_http(&self) -> bool {
459                if let Effect::Http(_) = self { true } else { false }
460            }
461            pub fn into_http(self) -> Option<::crux_core::Request<HttpRequest>> {
462                if let Effect::Http(request) = self { Some(request) } else { None }
463            }
464            #[track_caller]
465            pub fn expect_http(self) -> ::crux_core::Request<HttpRequest> {
466                if let Effect::Http(request) = self {
467                    request
468                } else {
469                    panic!("not a {} effect", "Http")
470                }
471            }
472        }
473        #[cfg(feature = "typegen")]
474        impl crux_core::typegen::Export for Effect {
475            fn register_types(
476                generator: &mut ::crux_core::typegen::TypeGen,
477            ) -> ::crux_core::typegen::Result {
478                use ::crux_core::capability::{Capability, Operation};
479                RenderOperation::register_types(generator)?;
480                HttpRequest::register_types(generator)?;
481                generator.register_type::<EffectFfi>()?;
482                generator.register_type::<::crux_core::bridge::Request<EffectFfi>>()?;
483                Ok(())
484            }
485        }
486        "###);
487    }
488
489    #[test]
490    fn multiple_without_typegen() {
491        let input = parse_quote! {
492            pub enum Effect {
493                Render(RenderOperation),
494                Http(HttpRequest),
495            }
496        };
497
498        let actual = effect_impl(None, input);
499
500        insta::assert_snapshot!(pretty_print(&actual), @r###"
501        #[derive(Debug)]
502        pub enum Effect {
503            Render(::crux_core::Request<RenderOperation>),
504            Http(::crux_core::Request<HttpRequest>),
505        }
506        #[derive(::serde::Serialize, ::serde::Deserialize)]
507        #[serde(rename = "Effect")]
508        pub enum EffectFfi {
509            Render(RenderOperation),
510            Http(HttpRequest),
511        }
512        impl crux_core::Effect for Effect {
513            type Ffi = EffectFfi;
514            fn serialize(self) -> (Self::Ffi, crux_core::bridge::ResolveSerialized) {
515                match self {
516                    Effect::Render(request) => request.serialize(EffectFfi::Render),
517                    Effect::Http(request) => request.serialize(EffectFfi::Http),
518                }
519            }
520        }
521        impl From<::crux_core::Request<RenderOperation>> for Effect {
522            fn from(value: ::crux_core::Request<RenderOperation>) -> Self {
523                Self::Render(value)
524            }
525        }
526        impl TryFrom<Effect> for ::crux_core::Request<RenderOperation> {
527            type Error = Effect;
528            fn try_from(value: Effect) -> Result<Self, Self::Error> {
529                if let Effect::Render(value) = value { Ok(value) } else { Err(value) }
530            }
531        }
532        impl From<::crux_core::Request<HttpRequest>> for Effect {
533            fn from(value: ::crux_core::Request<HttpRequest>) -> Self {
534                Self::Http(value)
535            }
536        }
537        impl TryFrom<Effect> for ::crux_core::Request<HttpRequest> {
538            type Error = Effect;
539            fn try_from(value: Effect) -> Result<Self, Self::Error> {
540                if let Effect::Http(value) = value { Ok(value) } else { Err(value) }
541            }
542        }
543        impl Effect {
544            pub fn is_render(&self) -> bool {
545                if let Effect::Render(_) = self { true } else { false }
546            }
547            pub fn into_render(self) -> Option<::crux_core::Request<RenderOperation>> {
548                if let Effect::Render(request) = self { Some(request) } else { None }
549            }
550            #[track_caller]
551            pub fn expect_render(self) -> ::crux_core::Request<RenderOperation> {
552                if let Effect::Render(request) = self {
553                    request
554                } else {
555                    panic!("not a {} effect", "Render")
556                }
557            }
558        }
559        impl Effect {
560            pub fn is_http(&self) -> bool {
561                if let Effect::Http(_) = self { true } else { false }
562            }
563            pub fn into_http(self) -> Option<::crux_core::Request<HttpRequest>> {
564                if let Effect::Http(request) = self { Some(request) } else { None }
565            }
566            #[track_caller]
567            pub fn expect_http(self) -> ::crux_core::Request<HttpRequest> {
568                if let Effect::Http(request) = self {
569                    request
570                } else {
571                    panic!("not a {} effect", "Http")
572                }
573            }
574        }
575        "###);
576    }
577}