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