phlow-derive 3.0.0

Proc-macro helpers for declaring phlow extensions and views
Documentation
use proc_macro2::Span;
use quote::quote;
use syn::spanned::Spanned;
use syn::{
    FnArg, Ident, ItemFn, ItemMod, ItemStruct, Pat, ReturnType, Type, TypeReference, parse_quote,
};

#[proc_macro_attribute]
pub fn extensions(
    attr: proc_macro::TokenStream,
    item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    let phlow_type: syn::Result<Type> = syn::parse(attr);
    let phlow_type = match phlow_type {
        Ok(phlow_type) => phlow_type,
        Err(error) => return error.to_compile_error().into(),
    };

    let item_mod: syn::Result<ItemMod> = syn::parse(item);
    let mut item_mod = match item_mod {
        Ok(item_mod) => item_mod,
        Err(error) => return error.to_compile_error().into(),
    };

    let Some((_, items)) = &mut item_mod.content else {
        return syn::Error::new(item_mod.span(), "`#[extensions]` requires an inline module")
            .to_compile_error()
            .into();
    };

    let utilities: syn::Item = parse_quote! {
        mod __utilities {
            #[allow(unused_imports)]
            use super::*;

            #[phlow::annotate::pragma(
                tag = "phlow-printing",
                path_to_annotate = phlow::annotate
            )]
            fn phlow_to_string(object: phlow::ObjectRef<'_>) -> String {
                let object_ref: &#phlow_type = unsafe { object.cast::<#phlow_type>() };
                phlow::to_string!(object_ref)
            }

            #[phlow::annotate::pragma(
                tag = "phlow-type-name",
                path_to_annotate = phlow::annotate
            )]
            fn phlow_type_name() -> &'static str {
                std::any::type_name::<#phlow_type>()
            }

            #[phlow::annotate::pragma(
                tag = "phlow-as-view",
                path_to_annotate = phlow::annotate
            )]
            fn phlow_create_view(method: &phlow::DefiningMethod, object: phlow::ObjectRef<'_>) -> Box<dyn phlow::PhlowView> {
                let object_ref: &#phlow_type = unsafe { object.cast::<#phlow_type>() };
                method.as_view(object_ref)
            }

            #[phlow::annotate::pragma(
                tag = "phlow-defining-methods",
                path_to_annotate = phlow::annotate
            )]
            fn phlow_defining_methods() -> Vec<phlow::DefiningMethod> {
                phlow::view_defining_methods_for_type::<#phlow_type>()
            }
        }
    };
    items.push(utilities);

    (quote! {
        #[phlow::annotate::pragma(tag = "phlow-extensions", path_to_annotate = phlow::annotate, phlow_type = #phlow_type)]
        #item_mod
    })
        .into()
}

#[proc_macro_attribute]
pub fn view(
    _attr: proc_macro::TokenStream,
    item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    let item_fn: syn::Result<ItemFn> = syn::parse(item);
    let mut item_fn = match item_fn {
        Ok(item_fn) => item_fn,
        Err(error) => return error.to_compile_error().into(),
    };

    if item_fn.sig.inputs.len() != 2 {
        return syn::Error::new(
            item_fn.sig.inputs.span(),
            "Must have exactly two arguments: `&T` and `impl ProtoView<T>`",
        )
        .to_compile_error()
        .into();
    }

    let receiver_argument = &mut item_fn.sig.inputs[0];
    let receiver_name;
    let receiver_type;
    match receiver_argument {
        FnArg::Receiver(ty) => {
            return syn::Error::new(ty.span(), "First argument must be `&T`")
                .to_compile_error()
                .into();
        }
        FnArg::Typed(pat_type) => {
            receiver_name = match pat_type.pat.as_ref() {
                Pat::Ident(pat_ident) => pat_ident.ident.clone(),
                _ => {
                    return syn::Error::new(
                        pat_type.pat.span(),
                        "First argument pattern must be an identifier",
                    )
                    .to_compile_error()
                    .into();
                }
            };
            receiver_type = match pat_type.ty.as_ref() {
                Type::Reference(TypeReference {
                    mutability: None,
                    elem,
                    ..
                }) => elem.as_ref().clone(),
                _ => {
                    return syn::Error::new(pat_type.ty.span(), "First argument must be `&T`")
                        .to_compile_error()
                        .into();
                }
            };
            *pat_type.ty = syn::parse2(quote! { &dyn std::any::Any }).unwrap();
        }
    };

    let argument = &mut item_fn.sig.inputs[1];
    let argument_generic_type;
    let argument_name;
    match argument {
        FnArg::Receiver(ty) => {
            return syn::Error::new(ty.span(), "Second argument must be `impl ProtoView<T>`")
                .to_compile_error()
                .into();
        }
        FnArg::Typed(pat_type) => match pat_type.ty.as_mut() {
            Type::ImplTrait(impl_trait) => {
                argument_generic_type = impl_trait.bounds.clone();
                argument_name = match pat_type.pat.as_ref() {
                    Pat::Ident(pat_ident) => pat_ident.ident.clone(),
                    _ => {
                        return syn::Error::new(
                            pat_type.pat.span(),
                            "Second argument pattern must be an identifier",
                        )
                        .to_compile_error()
                        .into();
                    }
                };
                *pat_type.ty = syn::parse2(quote! { phlow::DefiningMethod }).unwrap();
            }
            _ => {
                return syn::Error::new(
                    pat_type.span(),
                    "Second argument must be `impl ProtoView<T>`",
                )
                .to_compile_error()
                .into();
            }
        },
    };

    let fn_return = &item_fn.sig.output;
    let new_return = match fn_return {
        ReturnType::Default => {
            return syn::Error::new(fn_return.span(), "Must return `impl dyn PhlowView`")
                .to_compile_error()
                .into();
        }
        ReturnType::Type(arrow, ty) => match ty.as_ref() {
            Type::ImplTrait(impl_trait) => {
                let bounds = &impl_trait.bounds;
                syn::parse2::<ReturnType>(quote! { #arrow Box<dyn #bounds> }).unwrap()
            }
            _ => {
                return syn::Error::new(ty.span(), "Must be `impl dyn ProtoView<T>`")
                    .to_compile_error()
                    .into();
            }
        },
    };
    item_fn.sig.output = new_return;

    let body = &item_fn.block;

    let new_block: syn::Block = syn::parse2(quote! {
        {
            use phlow::IntoView;
            let #receiver_name: &#receiver_type =
                #receiver_name
                    .downcast_ref::<#receiver_type>()
                    .expect(concat!("Expected object of type ", stringify!(#receiver_type)));
            let #argument_name: Box<dyn #argument_generic_type> =
                Box::new(phlow::PhlowProtoView::compiled(#argument_name));
            #body.into_view()
        }
    })
    .unwrap();
    *item_fn.block = new_block;

    (quote! {
        #[phlow::annotate::pragma(tag = "phlow-view", path_to_annotate = phlow::annotate)]
        #item_fn
    })
    .into()
}

#[proc_macro]
pub fn environment(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let generated_path = environment_source_path(proc_macro::Span::call_site());
    let generated_path = syn::LitStr::new(generated_path.as_str(), proc_macro2::Span::call_site());
    let options = match EnvironmentOptions::parse(input) {
        Ok(options) => options,
        Err(error) => return error.to_compile_error().into(),
    };
    let link_macro = if options.generate_link_macro {
        quote! {
            #[macro_export]
            macro_rules! __phlow_generated_link_macro {
                () => {
                    const _: () = {
                        #[used]
                        static __PHLOW_GENERATED_LINK: fn() = $crate::__ensure_linked;
                    };
                };
            }

            pub use __phlow_generated_link_macro as link;
        }
    } else {
        quote! {}
    };

    quote! {
        include!(concat!(env!("OUT_DIR"), "/annotate/", #generated_path));
        #[doc(hidden)]
        #[inline(never)]
        pub fn __ensure_linked() {
            __annotate::__ensure_linked();
        }
        #link_macro
        #[::phlow::ctor::ctor(crate_path = ::phlow::ctor)]
        fn annotate_register_global_environment() {
            phlow::annotate::register_environment(
                concat!(file!(), "-", module_path!()),
                &__annotate::ENVIRONMENT,
            );
        }
    }
    .into()
}

struct EnvironmentOptions {
    generate_link_macro: bool,
}

impl EnvironmentOptions {
    fn parse(input: proc_macro::TokenStream) -> syn::Result<Self> {
        if input.is_empty() {
            return Ok(Self {
                generate_link_macro: true,
            });
        }

        let ident = syn::parse::<Ident>(input)?;
        if ident == "no_link_macro" {
            Ok(Self {
                generate_link_macro: false,
            })
        } else {
            Err(syn::Error::new(
                ident.span(),
                "Expected `no_link_macro` or no arguments",
            ))
        }
    }
}

fn environment_source_path(span: proc_macro::Span) -> String {
    let source_path = std::path::PathBuf::from(span.file());
    let manifest_root = std::path::PathBuf::from(
        std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
            .file_name()
            .unwrap(),
    );

    if source_path.is_absolute()
        && let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR")
    {
        let manifest_dir = std::path::PathBuf::from(manifest_dir);
        if let Ok(relative_path) = source_path.strip_prefix(&manifest_dir) {
            return manifest_root
                .join(relative_path)
                .to_string_lossy()
                .replace('\\', "/");
        }
    }

    if source_path
        .components()
        .next()
        .map(|component| component.as_os_str() == manifest_root.as_os_str())
        .unwrap_or(false)
    {
        return source_path.to_string_lossy().replace('\\', "/");
    }

    manifest_root
        .join(source_path)
        .to_string_lossy()
        .replace('\\', "/")
}

#[proc_macro_derive(RawView)]
pub fn derive_raw_view(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let item_struct: syn::Result<ItemStruct> = syn::parse(item);
    let item_struct = match item_struct {
        Ok(item_struct) => item_struct,
        Err(error) => return error.to_compile_error().into(),
    };

    let struct_ident = &item_struct.ident;

    let extensions_mod_ident = syn::Ident::new(
        &format!(
            "{}_derive_raw_view",
            struct_ident.to_string().to_lowercase(),
        ),
        Span::call_site(),
    );

    let expanded = quote! {
        #[phlow::extensions(#struct_ident)]
        mod #extensions_mod_ident {
            use super::*;
            use phlow::{InfoRow, PhlowView, ProtoView, to_string};

            #[phlow::view]
            fn raw_for(value: &#struct_ident, view: impl ProtoView<#struct_ident>) -> impl PhlowView {
                view.info()
                    .title("Raw")
                    .priority(100)
                    .row(|row| {
                        row.named_str("name")
                            .item_ref(|value| &value.name)
                            .text(|item| to_string!(item))
                    })
                    .row(|row| {
                        row.named_str("age")
                            .item_ref(|value| &value.age)
                            .text(|item| to_string!(item))
                    })
                    .row(|row| {
                        row.named_str("address")
                            .item_ref(|value| &value.address)
                            .text(|item| to_string!(item))
                    })
            }
        }
    };

    expanded.into()
}