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>, 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 let last = path.path.segments.last_mut().expect("type has no segments");
85 let type_params = std::mem::take(&mut last.arguments);
86
87 let generic_arg = match type_params {
89 PathArguments::AngleBracketed(params) => params.args.first().cloned(),
90 _ => None,
91 };
92
93 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}