crux_macros/
effect.rs

1use darling::{ast, util, FromDeriveInput, FromField, ToTokens};
2use proc_macro2::{Literal, TokenStream};
3use proc_macro_error::{abort_call_site, OptionExt};
4use quote::{format_ident, quote};
5use std::collections::BTreeMap;
6use syn::{DeriveInput, GenericArgument, Ident, PathArguments, Type};
7
8#[derive(FromDeriveInput, Debug)]
9#[darling(attributes(effect), supports(struct_named))]
10struct EffectStructReceiver {
11    ident: Ident,
12    name: Option<Ident>,
13    data: ast::Data<util::Ignored, EffectFieldReceiver>,
14}
15
16#[derive(FromField, Debug)]
17#[darling(attributes(effect))]
18pub struct EffectFieldReceiver {
19    ident: Option<Ident>,
20    ty: Type,
21    #[darling(default)]
22    skip: bool,
23}
24
25struct Field {
26    capability: Type,
27    variant: Ident,
28    event: Type,
29    skip: bool,
30}
31
32impl From<&EffectFieldReceiver> for Field {
33    fn from(f: &EffectFieldReceiver) -> Self {
34        let (capability, variant, event) = split_on_generic(&f.ty);
35        Field {
36            capability,
37            variant,
38            event,
39            skip: f.skip,
40        }
41    }
42}
43
44impl ToTokens for EffectStructReceiver {
45    fn to_tokens(&self, tokens: &mut TokenStream) {
46        let ident = &self.ident;
47
48        let (effect_name, ffi_effect_name, ffi_effect_rename) = match self.name {
49            Some(ref name) => {
50                let ffi_ef_name = format_ident!("{}Ffi", name);
51                let ffi_ef_rename = Literal::string(&name.to_string());
52
53                (quote!(#name), quote!(#ffi_ef_name), quote!(#ffi_ef_rename))
54            }
55            None => (quote!(Effect), quote!(EffectFfi), quote!("Effect")),
56        };
57
58        let fields = self
59            .data
60            .as_ref()
61            .take_struct()
62            .expect_or_abort("should be a struct")
63            .fields;
64
65        let fields: BTreeMap<Ident, Field> = fields
66            .into_iter()
67            .map(|f| (f.ident.clone().unwrap(), f.into()))
68            .collect();
69
70        let events: Vec<_> = fields.values().map(|Field { event, .. }| event).collect();
71        if !events
72            .windows(2)
73            .all(|win| win[0].to_token_stream().to_string() == win[1].to_token_stream().to_string())
74        {
75            abort_call_site!("all fields should be generic over the same event type");
76        }
77        let event = events
78            .first()
79            .expect_or_abort("Capabilities struct has no fields");
80
81        let mut variants = Vec::new();
82        let mut with_context_fields = Vec::new();
83        let mut ffi_variants = Vec::new();
84        let mut match_arms = Vec::new();
85        let mut filters = Vec::new();
86
87        for (
88            field_name,
89            Field {
90                capability,
91                variant,
92                event,
93                skip,
94            },
95        ) in fields.iter()
96        {
97            if *skip {
98                let msg = format!("Requesting effects from capability \"{variant}\" is impossible because it was skipped",);
99                with_context_fields.push(quote! {
100                    #field_name: #capability::new(context.specialize(|_| unreachable!(#msg)))
101                });
102            } else {
103                with_context_fields.push(quote! {
104                    #field_name: #capability::new(context.specialize(#effect_name::#variant))
105                });
106
107                variants.push(quote! {
108                    #variant(::crux_core::Request<<#capability<#event> as ::crux_core::capability::Capability<#event>>::Operation>)
109                });
110
111                ffi_variants.push(quote! { #variant(<#capability<#event> as ::crux_core::capability::Capability<#event>>::Operation) });
112
113                match_arms.push(quote! { #effect_name::#variant(request) => request.serialize(#ffi_effect_name::#variant) });
114
115                let filter_fn = format_ident!("is_{}", field_name);
116                let map_fn = format_ident!("into_{}", field_name);
117                let expect_fn = format_ident!("expect_{}", field_name);
118                let name_as_str = field_name.to_string();
119                filters.push(quote! {
120                    impl #effect_name {
121                        pub fn #filter_fn(&self) -> bool {
122                            if let #effect_name::#variant(_) = self {
123                                true
124                            } else {
125                                false
126                            }
127                        }
128                        pub fn #map_fn(self) -> Option<crux_core::Request<<#capability<#event> as ::crux_core::capability::Capability<#event>>::Operation>> {
129                            if let #effect_name::#variant(request) = self {
130                                Some(request)
131                            } else {
132                                None
133                            }
134                        }
135                        #[track_caller]
136                        pub fn #expect_fn(self) -> crux_core::Request<<#capability<#event> as ::crux_core::capability::Capability<#event>>::Operation> {
137                            if let #effect_name::#variant(request) = self {
138                                request
139                            } else {
140                                panic!("not a {} effect", #name_as_str)
141                            }
142                        }
143                    }
144                    impl From<crux_core::Request<<#capability<#event> as ::crux_core::capability::Capability<#event>>::Operation>> for #effect_name {
145                        fn from(value: crux_core::Request<<#capability<#event> as ::crux_core::capability::Capability<#event>>::Operation>) -> Self {
146                            Self::#variant(value)
147                        }
148                    }
149                });
150            }
151        }
152
153        tokens.extend(quote! {
154            #[derive(Debug)]
155            pub enum #effect_name {
156                #(#variants ,)*
157            }
158
159            #[derive(::serde::Serialize, ::serde::Deserialize)]
160            #[serde(rename = #ffi_effect_rename)]
161            pub enum #ffi_effect_name {
162                #(#ffi_variants ,)*
163            }
164
165            impl ::crux_core::Effect for #effect_name {
166                type Ffi = #ffi_effect_name;
167
168                fn serialize(self) -> (Self::Ffi, ::crux_core::bridge::ResolveSerialized) {
169                    match self {
170                        #(#match_arms ,)*
171                    }
172                }
173            }
174
175            impl ::crux_core::WithContext<#event, #effect_name> for #ident {
176                fn new_with_context(context: ::crux_core::capability::ProtoContext<#effect_name, #event>) -> #ident {
177                    #ident {
178                        #(#with_context_fields ,)*
179                    }
180                }
181            }
182
183            #(#filters)*
184        })
185    }
186}
187
188pub(crate) fn effect_impl(input: &DeriveInput) -> TokenStream {
189    let input = match EffectStructReceiver::from_derive_input(input) {
190        Ok(v) => v,
191        Err(e) => {
192            return e.write_errors();
193        }
194    };
195
196    quote!(#input)
197}
198
199fn split_on_generic(ty: &Type) -> (Type, Ident, Type) {
200    let ty = ty.clone();
201    match ty {
202        Type::Path(mut path) if path.qself.is_none() => {
203            // Get the last segment of the path where the generic parameter should be
204
205            let last = path.path.segments.last_mut().expect("type has no segments");
206            let type_name = last.ident.clone();
207            let type_params = std::mem::take(&mut last.arguments);
208
209            // It should have only one angle-bracketed param
210            let generic_arg = match type_params {
211                PathArguments::AngleBracketed(params) => params.args.first().cloned(),
212                _ => None,
213            };
214
215            // This argument must be a type
216            match generic_arg {
217                Some(GenericArgument::Type(t2)) => Some((Type::Path(path), type_name, t2)),
218                _ => None,
219            }
220        }
221        _ => None,
222    }
223    .expect_or_abort("capabilities should be generic over a single event type")
224}
225
226#[cfg(test)]
227mod tests {
228    use darling::{FromDeriveInput, FromMeta, ToTokens};
229    use quote::quote;
230    use syn::{parse_str, Type};
231
232    use crate::effect::EffectStructReceiver;
233
234    use super::split_on_generic;
235
236    #[test]
237    fn defaults() {
238        let input = r#"
239            #[derive(Effect)]
240            pub struct Capabilities {
241                pub render: Render<Event>,
242            }
243        "#;
244        let input = parse_str(input).unwrap();
245        let input = EffectStructReceiver::from_derive_input(&input).unwrap();
246
247        let actual = quote!(#input);
248
249        insta::assert_snapshot!(pretty_print(&actual), @r##"
250        #[derive(Debug)]
251        pub enum Effect {
252            Render(
253                ::crux_core::Request<
254                    <Render<Event> as ::crux_core::capability::Capability<Event>>::Operation,
255                >,
256            ),
257        }
258        #[derive(::serde::Serialize, ::serde::Deserialize)]
259        #[serde(rename = "Effect")]
260        pub enum EffectFfi {
261            Render(<Render<Event> as ::crux_core::capability::Capability<Event>>::Operation),
262        }
263        impl ::crux_core::Effect for Effect {
264            type Ffi = EffectFfi;
265            fn serialize(self) -> (Self::Ffi, ::crux_core::bridge::ResolveSerialized) {
266                match self {
267                    Effect::Render(request) => request.serialize(EffectFfi::Render),
268                }
269            }
270        }
271        impl ::crux_core::WithContext<Event, Effect> for Capabilities {
272            fn new_with_context(
273                context: ::crux_core::capability::ProtoContext<Effect, Event>,
274            ) -> Capabilities {
275                Capabilities {
276                    render: Render::new(context.specialize(Effect::Render)),
277                }
278            }
279        }
280        impl Effect {
281            pub fn is_render(&self) -> bool {
282                if let Effect::Render(_) = self { true } else { false }
283            }
284            pub fn into_render(
285                self,
286            ) -> Option<
287                crux_core::Request<
288                    <Render<Event> as ::crux_core::capability::Capability<Event>>::Operation,
289                >,
290            > {
291                if let Effect::Render(request) = self { Some(request) } else { None }
292            }
293            #[track_caller]
294            pub fn expect_render(
295                self,
296            ) -> crux_core::Request<
297                <Render<Event> as ::crux_core::capability::Capability<Event>>::Operation,
298            > {
299                if let Effect::Render(request) = self {
300                    request
301                } else {
302                    panic!("not a {} effect", "render")
303                }
304            }
305        }
306        impl From<
307            crux_core::Request<
308                <Render<Event> as ::crux_core::capability::Capability<Event>>::Operation,
309            >,
310        > for Effect {
311            fn from(
312                value: crux_core::Request<
313                    <Render<Event> as ::crux_core::capability::Capability<Event>>::Operation,
314                >,
315            ) -> Self {
316                Self::Render(value)
317            }
318        }
319        "##);
320    }
321
322    #[test]
323    fn effect_skip() {
324        let input = r#"
325            #[derive(Effect)]
326            pub struct Capabilities {
327                pub render: Render<Event>,
328                #[effect(skip)]
329                pub compose: Compose<Event>,
330            }
331        "#;
332        let input = parse_str(input).unwrap();
333        let input = EffectStructReceiver::from_derive_input(&input).unwrap();
334
335        let actual = quote!(#input);
336
337        insta::assert_snapshot!(pretty_print(&actual), @r##"
338        #[derive(Debug)]
339        pub enum Effect {
340            Render(
341                ::crux_core::Request<
342                    <Render<Event> as ::crux_core::capability::Capability<Event>>::Operation,
343                >,
344            ),
345        }
346        #[derive(::serde::Serialize, ::serde::Deserialize)]
347        #[serde(rename = "Effect")]
348        pub enum EffectFfi {
349            Render(<Render<Event> as ::crux_core::capability::Capability<Event>>::Operation),
350        }
351        impl ::crux_core::Effect for Effect {
352            type Ffi = EffectFfi;
353            fn serialize(self) -> (Self::Ffi, ::crux_core::bridge::ResolveSerialized) {
354                match self {
355                    Effect::Render(request) => request.serialize(EffectFfi::Render),
356                }
357            }
358        }
359        impl ::crux_core::WithContext<Event, Effect> for Capabilities {
360            fn new_with_context(
361                context: ::crux_core::capability::ProtoContext<Effect, Event>,
362            ) -> Capabilities {
363                Capabilities {
364                    compose: Compose::new(
365                        context
366                            .specialize(|_| {
367                                unreachable!(
368                                    "Requesting effects from capability \"Compose\" is impossible because it was skipped"
369                                )
370                            }),
371                    ),
372                    render: Render::new(context.specialize(Effect::Render)),
373                }
374            }
375        }
376        impl Effect {
377            pub fn is_render(&self) -> bool {
378                if let Effect::Render(_) = self { true } else { false }
379            }
380            pub fn into_render(
381                self,
382            ) -> Option<
383                crux_core::Request<
384                    <Render<Event> as ::crux_core::capability::Capability<Event>>::Operation,
385                >,
386            > {
387                if let Effect::Render(request) = self { Some(request) } else { None }
388            }
389            #[track_caller]
390            pub fn expect_render(
391                self,
392            ) -> crux_core::Request<
393                <Render<Event> as ::crux_core::capability::Capability<Event>>::Operation,
394            > {
395                if let Effect::Render(request) = self {
396                    request
397                } else {
398                    panic!("not a {} effect", "render")
399                }
400            }
401        }
402        impl From<
403            crux_core::Request<
404                <Render<Event> as ::crux_core::capability::Capability<Event>>::Operation,
405            >,
406        > for Effect {
407            fn from(
408                value: crux_core::Request<
409                    <Render<Event> as ::crux_core::capability::Capability<Event>>::Operation,
410                >,
411            ) -> Self {
412                Self::Render(value)
413            }
414        }
415        "##);
416    }
417
418    #[test]
419    fn full() {
420        let input = r#"
421            #[derive(Effect)]
422            #[effect(name = "MyEffect")]
423            pub struct MyCapabilities {
424                pub http: crux_http::Http<MyEvent>,
425                pub key_value: KeyValue<MyEvent>,
426                pub platform: Platform<MyEvent>,
427                pub render: Render<MyEvent>,
428                pub time: Time<MyEvent>,
429            }
430        "#;
431        let input = parse_str(input).unwrap();
432        let input = EffectStructReceiver::from_derive_input(&input).unwrap();
433
434        let actual = quote!(#input);
435
436        insta::assert_snapshot!(pretty_print(&actual), @r##"
437        #[derive(Debug)]
438        pub enum MyEffect {
439            Http(
440                ::crux_core::Request<
441                    <crux_http::Http<
442                        MyEvent,
443                    > as ::crux_core::capability::Capability<MyEvent>>::Operation,
444                >,
445            ),
446            KeyValue(
447                ::crux_core::Request<
448                    <KeyValue<
449                        MyEvent,
450                    > as ::crux_core::capability::Capability<MyEvent>>::Operation,
451                >,
452            ),
453            Platform(
454                ::crux_core::Request<
455                    <Platform<
456                        MyEvent,
457                    > as ::crux_core::capability::Capability<MyEvent>>::Operation,
458                >,
459            ),
460            Render(
461                ::crux_core::Request<
462                    <Render<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
463                >,
464            ),
465            Time(
466                ::crux_core::Request<
467                    <Time<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
468                >,
469            ),
470        }
471        #[derive(::serde::Serialize, ::serde::Deserialize)]
472        #[serde(rename = "MyEffect")]
473        pub enum MyEffectFfi {
474            Http(
475                <crux_http::Http<
476                    MyEvent,
477                > as ::crux_core::capability::Capability<MyEvent>>::Operation,
478            ),
479            KeyValue(
480                <KeyValue<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
481            ),
482            Platform(
483                <Platform<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
484            ),
485            Render(<Render<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation),
486            Time(<Time<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation),
487        }
488        impl ::crux_core::Effect for MyEffect {
489            type Ffi = MyEffectFfi;
490            fn serialize(self) -> (Self::Ffi, ::crux_core::bridge::ResolveSerialized) {
491                match self {
492                    MyEffect::Http(request) => request.serialize(MyEffectFfi::Http),
493                    MyEffect::KeyValue(request) => request.serialize(MyEffectFfi::KeyValue),
494                    MyEffect::Platform(request) => request.serialize(MyEffectFfi::Platform),
495                    MyEffect::Render(request) => request.serialize(MyEffectFfi::Render),
496                    MyEffect::Time(request) => request.serialize(MyEffectFfi::Time),
497                }
498            }
499        }
500        impl ::crux_core::WithContext<MyEvent, MyEffect> for MyCapabilities {
501            fn new_with_context(
502                context: ::crux_core::capability::ProtoContext<MyEffect, MyEvent>,
503            ) -> MyCapabilities {
504                MyCapabilities {
505                    http: crux_http::Http::new(context.specialize(MyEffect::Http)),
506                    key_value: KeyValue::new(context.specialize(MyEffect::KeyValue)),
507                    platform: Platform::new(context.specialize(MyEffect::Platform)),
508                    render: Render::new(context.specialize(MyEffect::Render)),
509                    time: Time::new(context.specialize(MyEffect::Time)),
510                }
511            }
512        }
513        impl MyEffect {
514            pub fn is_http(&self) -> bool {
515                if let MyEffect::Http(_) = self { true } else { false }
516            }
517            pub fn into_http(
518                self,
519            ) -> Option<
520                crux_core::Request<
521                    <crux_http::Http<
522                        MyEvent,
523                    > as ::crux_core::capability::Capability<MyEvent>>::Operation,
524                >,
525            > {
526                if let MyEffect::Http(request) = self { Some(request) } else { None }
527            }
528            #[track_caller]
529            pub fn expect_http(
530                self,
531            ) -> crux_core::Request<
532                <crux_http::Http<
533                    MyEvent,
534                > as ::crux_core::capability::Capability<MyEvent>>::Operation,
535            > {
536                if let MyEffect::Http(request) = self {
537                    request
538                } else {
539                    panic!("not a {} effect", "http")
540                }
541            }
542        }
543        impl From<
544            crux_core::Request<
545                <crux_http::Http<
546                    MyEvent,
547                > as ::crux_core::capability::Capability<MyEvent>>::Operation,
548            >,
549        > for MyEffect {
550            fn from(
551                value: crux_core::Request<
552                    <crux_http::Http<
553                        MyEvent,
554                    > as ::crux_core::capability::Capability<MyEvent>>::Operation,
555                >,
556            ) -> Self {
557                Self::Http(value)
558            }
559        }
560        impl MyEffect {
561            pub fn is_key_value(&self) -> bool {
562                if let MyEffect::KeyValue(_) = self { true } else { false }
563            }
564            pub fn into_key_value(
565                self,
566            ) -> Option<
567                crux_core::Request<
568                    <KeyValue<
569                        MyEvent,
570                    > as ::crux_core::capability::Capability<MyEvent>>::Operation,
571                >,
572            > {
573                if let MyEffect::KeyValue(request) = self { Some(request) } else { None }
574            }
575            #[track_caller]
576            pub fn expect_key_value(
577                self,
578            ) -> crux_core::Request<
579                <KeyValue<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
580            > {
581                if let MyEffect::KeyValue(request) = self {
582                    request
583                } else {
584                    panic!("not a {} effect", "key_value")
585                }
586            }
587        }
588        impl From<
589            crux_core::Request<
590                <KeyValue<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
591            >,
592        > for MyEffect {
593            fn from(
594                value: crux_core::Request<
595                    <KeyValue<
596                        MyEvent,
597                    > as ::crux_core::capability::Capability<MyEvent>>::Operation,
598                >,
599            ) -> Self {
600                Self::KeyValue(value)
601            }
602        }
603        impl MyEffect {
604            pub fn is_platform(&self) -> bool {
605                if let MyEffect::Platform(_) = self { true } else { false }
606            }
607            pub fn into_platform(
608                self,
609            ) -> Option<
610                crux_core::Request<
611                    <Platform<
612                        MyEvent,
613                    > as ::crux_core::capability::Capability<MyEvent>>::Operation,
614                >,
615            > {
616                if let MyEffect::Platform(request) = self { Some(request) } else { None }
617            }
618            #[track_caller]
619            pub fn expect_platform(
620                self,
621            ) -> crux_core::Request<
622                <Platform<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
623            > {
624                if let MyEffect::Platform(request) = self {
625                    request
626                } else {
627                    panic!("not a {} effect", "platform")
628                }
629            }
630        }
631        impl From<
632            crux_core::Request<
633                <Platform<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
634            >,
635        > for MyEffect {
636            fn from(
637                value: crux_core::Request<
638                    <Platform<
639                        MyEvent,
640                    > as ::crux_core::capability::Capability<MyEvent>>::Operation,
641                >,
642            ) -> Self {
643                Self::Platform(value)
644            }
645        }
646        impl MyEffect {
647            pub fn is_render(&self) -> bool {
648                if let MyEffect::Render(_) = self { true } else { false }
649            }
650            pub fn into_render(
651                self,
652            ) -> Option<
653                crux_core::Request<
654                    <Render<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
655                >,
656            > {
657                if let MyEffect::Render(request) = self { Some(request) } else { None }
658            }
659            #[track_caller]
660            pub fn expect_render(
661                self,
662            ) -> crux_core::Request<
663                <Render<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
664            > {
665                if let MyEffect::Render(request) = self {
666                    request
667                } else {
668                    panic!("not a {} effect", "render")
669                }
670            }
671        }
672        impl From<
673            crux_core::Request<
674                <Render<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
675            >,
676        > for MyEffect {
677            fn from(
678                value: crux_core::Request<
679                    <Render<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
680                >,
681            ) -> Self {
682                Self::Render(value)
683            }
684        }
685        impl MyEffect {
686            pub fn is_time(&self) -> bool {
687                if let MyEffect::Time(_) = self { true } else { false }
688            }
689            pub fn into_time(
690                self,
691            ) -> Option<
692                crux_core::Request<
693                    <Time<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
694                >,
695            > {
696                if let MyEffect::Time(request) = self { Some(request) } else { None }
697            }
698            #[track_caller]
699            pub fn expect_time(
700                self,
701            ) -> crux_core::Request<
702                <Time<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
703            > {
704                if let MyEffect::Time(request) = self {
705                    request
706                } else {
707                    panic!("not a {} effect", "time")
708                }
709            }
710        }
711        impl From<
712            crux_core::Request<
713                <Time<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
714            >,
715        > for MyEffect {
716            fn from(
717                value: crux_core::Request<
718                    <Time<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
719                >,
720            ) -> Self {
721                Self::Time(value)
722            }
723        }
724        "##);
725    }
726
727    #[test]
728    #[should_panic]
729    fn should_panic_when_multiple_event_types() {
730        let input = r#"
731            #[derive(Effect)]
732            pub struct Capabilities {
733                pub render: Render<MyEvent>,
734                pub time: Time<YourEvent>,
735            }
736        "#;
737        let input = parse_str(input).unwrap();
738        let input = EffectStructReceiver::from_derive_input(&input).unwrap();
739
740        let mut actual = quote!();
741        input.to_tokens(&mut actual);
742    }
743
744    fn pretty_print(ts: &proc_macro2::TokenStream) -> String {
745        let file = syn::parse_file(&ts.to_string()).unwrap();
746        prettyplease::unparse(&file)
747    }
748
749    #[test]
750    fn split_event_types_preserves_path() {
751        let ty = Type::from_string("crux_core::render::Render<Event>").unwrap();
752
753        let (actual_type, actual_ident, actual_event) = split_on_generic(&ty);
754
755        assert_eq!(
756            quote!(#actual_type).to_string(),
757            quote!(crux_core::render::Render).to_string()
758        );
759
760        assert_eq!(
761            quote!(#actual_ident).to_string(),
762            quote!(Render).to_string()
763        );
764
765        assert_eq!(quote!(#actual_event).to_string(), quote!(Event).to_string());
766    }
767}