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}