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()
}