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}