Skip to main content

phlow_derive/
lib.rs

1use proc_macro2::Span;
2use quote::quote;
3use syn::spanned::Spanned;
4use syn::{
5    FnArg, Ident, ItemFn, ItemMod, ItemStruct, Pat, ReturnType, Type, TypeReference, parse_quote,
6};
7
8#[proc_macro_attribute]
9pub fn extensions(
10    attr: proc_macro::TokenStream,
11    item: proc_macro::TokenStream,
12) -> proc_macro::TokenStream {
13    let phlow_type: syn::Result<Type> = syn::parse(attr);
14    let phlow_type = match phlow_type {
15        Ok(phlow_type) => phlow_type,
16        Err(error) => return error.to_compile_error().into(),
17    };
18
19    let item_mod: syn::Result<ItemMod> = syn::parse(item);
20    let mut item_mod = match item_mod {
21        Ok(item_mod) => item_mod,
22        Err(error) => return error.to_compile_error().into(),
23    };
24
25    let Some((_, items)) = &mut item_mod.content else {
26        return syn::Error::new(item_mod.span(), "`#[extensions]` requires an inline module")
27            .to_compile_error()
28            .into();
29    };
30
31    let utilities: syn::Item = parse_quote! {
32        mod __utilities {
33            #[allow(unused_imports)]
34            use super::*;
35
36            #[phlow::annotate::pragma(
37                tag = "phlow-printing",
38                path_to_annotate = phlow::annotate
39            )]
40            fn phlow_to_string(object: phlow::ObjectRef<'_>) -> String {
41                let object_ref: &#phlow_type = unsafe { object.cast::<#phlow_type>() };
42                phlow::to_string!(object_ref)
43            }
44
45            #[phlow::annotate::pragma(
46                tag = "phlow-type-name",
47                path_to_annotate = phlow::annotate
48            )]
49            fn phlow_type_name() -> &'static str {
50                std::any::type_name::<#phlow_type>()
51            }
52
53            #[phlow::annotate::pragma(
54                tag = "phlow-as-view",
55                path_to_annotate = phlow::annotate
56            )]
57            fn phlow_create_view(method: &phlow::DefiningMethod, object: phlow::ObjectRef<'_>) -> Box<dyn phlow::PhlowView> {
58                let object_ref: &#phlow_type = unsafe { object.cast::<#phlow_type>() };
59                method.as_view(object_ref)
60            }
61
62            #[phlow::annotate::pragma(
63                tag = "phlow-defining-methods",
64                path_to_annotate = phlow::annotate
65            )]
66            fn phlow_defining_methods() -> Vec<phlow::DefiningMethod> {
67                phlow::view_defining_methods_for_type::<#phlow_type>()
68            }
69        }
70    };
71    items.push(utilities);
72
73    (quote! {
74        #[phlow::annotate::pragma(tag = "phlow-extensions", path_to_annotate = phlow::annotate, phlow_type = #phlow_type)]
75        #item_mod
76    })
77        .into()
78}
79
80#[proc_macro_attribute]
81pub fn view(
82    _attr: proc_macro::TokenStream,
83    item: proc_macro::TokenStream,
84) -> proc_macro::TokenStream {
85    let item_fn: syn::Result<ItemFn> = syn::parse(item);
86    let mut item_fn = match item_fn {
87        Ok(item_fn) => item_fn,
88        Err(error) => return error.to_compile_error().into(),
89    };
90
91    if item_fn.sig.inputs.len() != 2 {
92        return syn::Error::new(
93            item_fn.sig.inputs.span(),
94            "Must have exactly two arguments: `&T` and `impl ProtoView<T>`",
95        )
96        .to_compile_error()
97        .into();
98    }
99
100    let receiver_argument = &mut item_fn.sig.inputs[0];
101    let receiver_name;
102    let receiver_type;
103    match receiver_argument {
104        FnArg::Receiver(ty) => {
105            return syn::Error::new(ty.span(), "First argument must be `&T`")
106                .to_compile_error()
107                .into();
108        }
109        FnArg::Typed(pat_type) => {
110            receiver_name = match pat_type.pat.as_ref() {
111                Pat::Ident(pat_ident) => pat_ident.ident.clone(),
112                _ => {
113                    return syn::Error::new(
114                        pat_type.pat.span(),
115                        "First argument pattern must be an identifier",
116                    )
117                    .to_compile_error()
118                    .into();
119                }
120            };
121            receiver_type = match pat_type.ty.as_ref() {
122                Type::Reference(TypeReference {
123                    mutability: None,
124                    elem,
125                    ..
126                }) => elem.as_ref().clone(),
127                _ => {
128                    return syn::Error::new(pat_type.ty.span(), "First argument must be `&T`")
129                        .to_compile_error()
130                        .into();
131                }
132            };
133            *pat_type.ty = syn::parse2(quote! { &dyn std::any::Any }).unwrap();
134        }
135    };
136
137    let argument = &mut item_fn.sig.inputs[1];
138    let argument_generic_type;
139    let argument_name;
140    match argument {
141        FnArg::Receiver(ty) => {
142            return syn::Error::new(ty.span(), "Second argument must be `impl ProtoView<T>`")
143                .to_compile_error()
144                .into();
145        }
146        FnArg::Typed(pat_type) => match pat_type.ty.as_mut() {
147            Type::ImplTrait(impl_trait) => {
148                argument_generic_type = impl_trait.bounds.clone();
149                argument_name = match pat_type.pat.as_ref() {
150                    Pat::Ident(pat_ident) => pat_ident.ident.clone(),
151                    _ => {
152                        return syn::Error::new(
153                            pat_type.pat.span(),
154                            "Second argument pattern must be an identifier",
155                        )
156                        .to_compile_error()
157                        .into();
158                    }
159                };
160                *pat_type.ty = syn::parse2(quote! { phlow::DefiningMethod }).unwrap();
161            }
162            _ => {
163                return syn::Error::new(
164                    pat_type.span(),
165                    "Second argument must be `impl ProtoView<T>`",
166                )
167                .to_compile_error()
168                .into();
169            }
170        },
171    };
172
173    let fn_return = &item_fn.sig.output;
174    let new_return = match fn_return {
175        ReturnType::Default => {
176            return syn::Error::new(fn_return.span(), "Must return `impl dyn PhlowView`")
177                .to_compile_error()
178                .into();
179        }
180        ReturnType::Type(arrow, ty) => match ty.as_ref() {
181            Type::ImplTrait(impl_trait) => {
182                let bounds = &impl_trait.bounds;
183                syn::parse2::<ReturnType>(quote! { #arrow Box<dyn #bounds> }).unwrap()
184            }
185            _ => {
186                return syn::Error::new(ty.span(), "Must be `impl dyn ProtoView<T>`")
187                    .to_compile_error()
188                    .into();
189            }
190        },
191    };
192    item_fn.sig.output = new_return;
193
194    let body = &item_fn.block;
195
196    let new_block: syn::Block = syn::parse2(quote! {
197        {
198            use phlow::IntoView;
199            let #receiver_name: &#receiver_type =
200                #receiver_name
201                    .downcast_ref::<#receiver_type>()
202                    .expect(concat!("Expected object of type ", stringify!(#receiver_type)));
203            let #argument_name: Box<dyn #argument_generic_type> =
204                Box::new(phlow::PhlowProtoView::compiled(#argument_name));
205            #body.into_view()
206        }
207    })
208    .unwrap();
209    *item_fn.block = new_block;
210
211    (quote! {
212        #[phlow::annotate::pragma(tag = "phlow-view", path_to_annotate = phlow::annotate)]
213        #item_fn
214    })
215    .into()
216}
217
218#[proc_macro]
219pub fn environment(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
220    let generated_path = environment_source_path(proc_macro::Span::call_site());
221    let generated_path = syn::LitStr::new(generated_path.as_str(), proc_macro2::Span::call_site());
222    let options = match EnvironmentOptions::parse(input) {
223        Ok(options) => options,
224        Err(error) => return error.to_compile_error().into(),
225    };
226    let link_macro = if options.generate_link_macro {
227        quote! {
228            #[macro_export]
229            macro_rules! __phlow_generated_link_macro {
230                () => {
231                    const _: () = {
232                        #[used]
233                        static __PHLOW_GENERATED_LINK: fn() = $crate::__ensure_linked;
234                    };
235                };
236            }
237
238            pub use __phlow_generated_link_macro as link;
239        }
240    } else {
241        quote! {}
242    };
243
244    quote! {
245        include!(concat!(env!("OUT_DIR"), "/annotate/", #generated_path));
246        #[doc(hidden)]
247        #[inline(never)]
248        pub fn __ensure_linked() {
249            __annotate::__ensure_linked();
250        }
251        #link_macro
252        #[::phlow::ctor::ctor(crate_path = ::phlow::ctor)]
253        fn annotate_register_global_environment() {
254            phlow::annotate::register_environment(
255                concat!(file!(), "-", module_path!()),
256                &__annotate::ENVIRONMENT,
257            );
258        }
259    }
260    .into()
261}
262
263struct EnvironmentOptions {
264    generate_link_macro: bool,
265}
266
267impl EnvironmentOptions {
268    fn parse(input: proc_macro::TokenStream) -> syn::Result<Self> {
269        if input.is_empty() {
270            return Ok(Self {
271                generate_link_macro: true,
272            });
273        }
274
275        let ident = syn::parse::<Ident>(input)?;
276        if ident == "no_link_macro" {
277            Ok(Self {
278                generate_link_macro: false,
279            })
280        } else {
281            Err(syn::Error::new(
282                ident.span(),
283                "Expected `no_link_macro` or no arguments",
284            ))
285        }
286    }
287}
288
289fn environment_source_path(span: proc_macro::Span) -> String {
290    let source_path = std::path::PathBuf::from(span.file());
291    let manifest_root = std::path::PathBuf::from(
292        std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
293            .file_name()
294            .unwrap(),
295    );
296
297    if source_path.is_absolute()
298        && let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR")
299    {
300        let manifest_dir = std::path::PathBuf::from(manifest_dir);
301        if let Ok(relative_path) = source_path.strip_prefix(&manifest_dir) {
302            return manifest_root
303                .join(relative_path)
304                .to_string_lossy()
305                .replace('\\', "/");
306        }
307    }
308
309    if source_path
310        .components()
311        .next()
312        .map(|component| component.as_os_str() == manifest_root.as_os_str())
313        .unwrap_or(false)
314    {
315        return source_path.to_string_lossy().replace('\\', "/");
316    }
317
318    manifest_root
319        .join(source_path)
320        .to_string_lossy()
321        .replace('\\', "/")
322}
323
324#[proc_macro_derive(RawView)]
325pub fn derive_raw_view(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
326    let item_struct: syn::Result<ItemStruct> = syn::parse(item);
327    let item_struct = match item_struct {
328        Ok(item_struct) => item_struct,
329        Err(error) => return error.to_compile_error().into(),
330    };
331
332    let struct_ident = &item_struct.ident;
333
334    let extensions_mod_ident = syn::Ident::new(
335        &format!(
336            "{}_derive_raw_view",
337            struct_ident.to_string().to_lowercase(),
338        ),
339        Span::call_site(),
340    );
341
342    let expanded = quote! {
343        #[phlow::extensions(#struct_ident)]
344        mod #extensions_mod_ident {
345            use super::*;
346            use phlow::{InfoRow, PhlowView, ProtoView, to_string};
347
348            #[phlow::view]
349            fn raw_for(value: &#struct_ident, view: impl ProtoView<#struct_ident>) -> impl PhlowView {
350                view.info()
351                    .title("Raw")
352                    .priority(100)
353                    .row(|row| {
354                        row.named_str("name")
355                            .item_ref(|value| &value.name)
356                            .text(|item| to_string!(item))
357                    })
358                    .row(|row| {
359                        row.named_str("age")
360                            .item_ref(|value| &value.age)
361                            .text(|item| to_string!(item))
362                    })
363                    .row(|row| {
364                        row.named_str("address")
365                            .item_ref(|value| &value.address)
366                            .text(|item| to_string!(item))
367                    })
368            }
369        }
370    };
371
372    expanded.into()
373}