crux_macros/
export.rs

1use darling::{ast, util, FromDeriveInput, FromField, ToTokens};
2use proc_macro2::TokenStream;
3use proc_macro_error::OptionExt;
4use quote::{format_ident, quote};
5use syn::{DeriveInput, GenericArgument, Ident, PathArguments, Type};
6
7#[derive(FromDeriveInput, Debug)]
8#[darling(attributes(effect), supports(struct_named))]
9struct ExportStructReceiver {
10    name: Option<Ident>, // also used by the effect derive macro to name the effect
11    data: ast::Data<util::Ignored, ExportFieldReceiver>,
12}
13
14#[derive(FromField, Debug)]
15#[darling(attributes(effect))]
16pub struct ExportFieldReceiver {
17    ty: Type,
18    #[darling(default)]
19    skip: bool,
20}
21
22impl ToTokens for ExportStructReceiver {
23    fn to_tokens(&self, tokens: &mut TokenStream) {
24        let effect_name = self.name.clone().unwrap_or_else(|| format_ident!("Effect"));
25        let ffi_export_name = match self.name {
26            Some(ref name) => {
27                let ffi_ef_name = format_ident!("{}Ffi", name);
28
29                quote!(#ffi_ef_name)
30            }
31            None => quote!(EffectFfi),
32        };
33
34        let fields: Vec<&ExportFieldReceiver> = self
35            .data
36            .as_ref()
37            .take_struct()
38            .expect_or_abort("should be a struct")
39            .fields
40            .into_iter()
41            .filter(|e| !e.skip)
42            .collect();
43
44        let mut output_type_exports = Vec::new();
45
46        for (capability, event) in fields.iter().map(|f| split_on_generic(&f.ty)) {
47            output_type_exports.push(quote! {
48                <#capability::<#event> as Capability<#event>>::Operation::register_types(generator)?;
49            });
50        }
51
52        tokens.extend(quote! {
53            impl ::crux_core::typegen::Export for #effect_name {
54                fn register_types(generator: &mut ::crux_core::typegen::TypeGen) -> ::crux_core::typegen::Result {
55                    use ::crux_core::capability::{Capability, Operation};
56                    #(#output_type_exports)*
57                    generator.register_type::<#ffi_export_name>()?;
58                    generator.register_type::<::crux_core::bridge::Request<#ffi_export_name>>()?;
59
60                    Ok(())
61                }
62            }
63        })
64    }
65}
66
67pub(crate) fn export_impl(input: &DeriveInput) -> TokenStream {
68    let input = match ExportStructReceiver::from_derive_input(input) {
69        Ok(v) => v,
70        Err(e) => {
71            return e.write_errors();
72        }
73    };
74
75    quote!(#input)
76}
77
78fn split_on_generic(ty: &Type) -> (Type, Type) {
79    let ty = ty.clone();
80    match ty {
81        Type::Path(mut path) if path.qself.is_none() => {
82            // Get the last segment of the path where the generic parameter should be
83
84            let last = path.path.segments.last_mut().expect("type has no segments");
85            let type_params = std::mem::take(&mut last.arguments);
86
87            // It should have only one angle-bracketed param
88            let generic_arg = match type_params {
89                PathArguments::AngleBracketed(params) => params.args.first().cloned(),
90                _ => None,
91            };
92
93            // This argument must be a type
94            match generic_arg {
95                Some(GenericArgument::Type(t2)) => Some((Type::Path(path), t2)),
96                _ => None,
97            }
98        }
99        _ => None,
100    }
101    .expect_or_abort("capabilities should be generic over a single event type")
102}
103
104#[cfg(test)]
105mod tests {
106    use darling::{FromDeriveInput, FromMeta};
107    use quote::quote;
108    use syn::{parse_str, Type};
109
110    use crate::export::ExportStructReceiver;
111
112    use super::split_on_generic;
113
114    #[test]
115    fn defaults() {
116        let input = r#"
117            #[derive(Export)]
118            pub struct Capabilities {
119                pub render: Render<Event>,
120            }
121        "#;
122        let input = parse_str(input).unwrap();
123        let input = ExportStructReceiver::from_derive_input(&input).unwrap();
124
125        let actual = quote!(#input);
126
127        insta::assert_snapshot!(pretty_print(&actual), @r"
128        impl ::crux_core::typegen::Export for Effect {
129            fn register_types(
130                generator: &mut ::crux_core::typegen::TypeGen,
131            ) -> ::crux_core::typegen::Result {
132                use ::crux_core::capability::{Capability, Operation};
133                <Render<Event> as Capability<Event>>::Operation::register_types(generator)?;
134                generator.register_type::<EffectFfi>()?;
135                generator.register_type::<::crux_core::bridge::Request<EffectFfi>>()?;
136                Ok(())
137            }
138        }
139        ");
140    }
141
142    #[test]
143    fn split_event_types_preserves_path() {
144        let ty = Type::from_string("crux_core::render::Render<Event>").unwrap();
145
146        let (actual_type, actual_event) = split_on_generic(&ty);
147
148        assert_eq!(
149            quote!(#actual_type).to_string(),
150            quote!(crux_core::render::Render).to_string()
151        );
152
153        assert_eq!(quote!(#actual_event).to_string(), quote!(Event).to_string());
154    }
155
156    #[test]
157    fn export_macro_respects_an_skip_attr() {
158        let input = r#"
159            #[derive(Export)]
160            pub struct MyCapabilities {
161                pub http: crux_http::Http<MyEvent>,
162                pub key_value: KeyValue<MyEvent>,
163                pub platform: Platform<MyEvent>,
164                pub render: Render<MyEvent>,
165                #[effect(skip)]
166                pub time: Time<MyEvent>,
167            }
168        "#;
169        let input = parse_str(input).unwrap();
170        let input = ExportStructReceiver::from_derive_input(&input).unwrap();
171
172        let actual = quote!(#input);
173
174        insta::assert_snapshot!(pretty_print(&actual), @r"
175        impl ::crux_core::typegen::Export for Effect {
176            fn register_types(
177                generator: &mut ::crux_core::typegen::TypeGen,
178            ) -> ::crux_core::typegen::Result {
179                use ::crux_core::capability::{Capability, Operation};
180                <crux_http::Http<
181                    MyEvent,
182                > as Capability<MyEvent>>::Operation::register_types(generator)?;
183                <KeyValue<
184                    MyEvent,
185                > as Capability<MyEvent>>::Operation::register_types(generator)?;
186                <Platform<
187                    MyEvent,
188                > as Capability<MyEvent>>::Operation::register_types(generator)?;
189                <Render<MyEvent> as Capability<MyEvent>>::Operation::register_types(generator)?;
190                generator.register_type::<EffectFfi>()?;
191                generator.register_type::<::crux_core::bridge::Request<EffectFfi>>()?;
192                Ok(())
193            }
194        }
195        ");
196    }
197
198    #[test]
199    fn full() {
200        let input = r#"
201            #[derive(Export)]
202            pub struct MyCapabilities {
203                pub http: crux_http::Http<MyEvent>,
204                pub key_value: KeyValue<MyEvent>,
205                pub platform: Platform<MyEvent>,
206                pub render: Render<MyEvent>,
207                pub time: Time<MyEvent>,
208            }
209        "#;
210        let input = parse_str(input).unwrap();
211        let input = ExportStructReceiver::from_derive_input(&input).unwrap();
212
213        let actual = quote!(#input);
214
215        insta::assert_snapshot!(pretty_print(&actual), @r"
216        impl ::crux_core::typegen::Export for Effect {
217            fn register_types(
218                generator: &mut ::crux_core::typegen::TypeGen,
219            ) -> ::crux_core::typegen::Result {
220                use ::crux_core::capability::{Capability, Operation};
221                <crux_http::Http<
222                    MyEvent,
223                > as Capability<MyEvent>>::Operation::register_types(generator)?;
224                <KeyValue<
225                    MyEvent,
226                > as Capability<MyEvent>>::Operation::register_types(generator)?;
227                <Platform<
228                    MyEvent,
229                > as Capability<MyEvent>>::Operation::register_types(generator)?;
230                <Render<MyEvent> as Capability<MyEvent>>::Operation::register_types(generator)?;
231                <Time<MyEvent> as Capability<MyEvent>>::Operation::register_types(generator)?;
232                generator.register_type::<EffectFfi>()?;
233                generator.register_type::<::crux_core::bridge::Request<EffectFfi>>()?;
234                Ok(())
235            }
236        }
237        ");
238    }
239
240    #[test]
241    fn export_macro_respects_an_effect_name_override() {
242        let input = r#"
243            #[derive(Export, Effect)]
244            #[effect(name = "MyEffect")]
245            pub struct Capabilities {
246                render: Render<Event>,
247            }
248        "#;
249
250        let input = parse_str(input).unwrap();
251        let input = ExportStructReceiver::from_derive_input(&input).unwrap();
252
253        let actual = quote!(#input);
254
255        insta::assert_snapshot!(pretty_print(&actual), @r"
256        impl ::crux_core::typegen::Export for MyEffect {
257            fn register_types(
258                generator: &mut ::crux_core::typegen::TypeGen,
259            ) -> ::crux_core::typegen::Result {
260                use ::crux_core::capability::{Capability, Operation};
261                <Render<Event> as Capability<Event>>::Operation::register_types(generator)?;
262                generator.register_type::<MyEffectFfi>()?;
263                generator.register_type::<::crux_core::bridge::Request<MyEffectFfi>>()?;
264                Ok(())
265            }
266        }
267        ");
268    }
269
270    fn pretty_print(ts: &proc_macro2::TokenStream) -> String {
271        let file = syn::parse_file(&ts.to_string()).unwrap();
272        prettyplease::unparse(&file)
273    }
274}