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 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 let generic_arg = match type_params {
211 PathArguments::AngleBracketed(params) => params.args.first().cloned(),
212 _ => None,
213 };
214
215 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}