extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse::Parse, parse_macro_input, punctuated::Punctuated, Expr, FnArg, Ident, ItemFn, Meta, Pat,
PatType, Token,
};
#[derive(Default)]
struct PageArgs {
render: Option<String>,
}
impl Parse for PageArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut args = Self::default();
if input.is_empty() {
return Ok(args);
}
let vars = Punctuated::<Meta, Token![,]>::parse_terminated(input)?;
for var in vars {
if let Meta::NameValue(meta) = var {
if meta.path.is_ident("render") {
if let Expr::Lit(expr_lit) = &meta.value {
if let syn::Lit::Str(lit_str) = &expr_lit.lit {
args.render = Some(lit_str.value());
}
}
}
}
}
Ok(args)
}
}
#[derive(Default)]
struct StaticPropsArgs {
revalidate: Option<u64>,
}
impl Parse for StaticPropsArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut args = Self::default();
if input.is_empty() {
return Ok(args);
}
let vars = Punctuated::<Meta, Token![,]>::parse_terminated(input)?;
for var in vars {
if let Meta::NameValue(meta) = var {
if meta.path.is_ident("revalidate") {
if let Expr::Lit(expr_lit) = &meta.value {
if let syn::Lit::Int(lit_int) = &expr_lit.lit {
if let Ok(value) = lit_int.base10_parse::<u64>() {
args.revalidate = Some(value);
}
}
}
}
}
}
Ok(args)
}
}
#[proc_macro_attribute]
pub fn page(attr: TokenStream, item: TokenStream) -> TokenStream {
let args = parse_macro_input!(attr as PageArgs);
let input = parse_macro_input!(item as ItemFn);
let render_strategy = args.render.unwrap_or_else(|| "server".to_string());
let fn_name = &input.sig.ident;
let fn_vis = &input.vis;
let fn_block = &input.block;
let fn_generics = &input.sig.generics;
let fn_inputs = &input.sig.inputs;
let fn_output = &input.sig.output;
let route_path = fn_name_to_route_path(fn_name);
let _path_params = extract_path_params(fn_inputs);
let expanded = quote! {
#[allow(non_camel_case_types)]
#fn_vis struct #fn_name;
impl #fn_name {
#fn_vis fn new() -> Self {
println!("Registering page: {} with render strategy: {}", #route_path, #render_strategy);
Self
}
#fn_vis #fn_generics fn call(#fn_inputs) #fn_output {
#fn_block
}
}
#[allow(non_upper_case_globals)]
#fn_vis static #fn_name: () = {
let _ = #fn_name::new();
()
};
};
TokenStream::from(expanded)
}
#[proc_macro_attribute]
pub fn api(attr: TokenStream, item: TokenStream) -> TokenStream {
let _args = parse_macro_input!(attr as PageArgs);
let input = parse_macro_input!(item as ItemFn);
let fn_name = &input.sig.ident;
let fn_vis = &input.vis;
let fn_block = &input.block;
let fn_generics = &input.sig.generics;
let fn_inputs = &input.sig.inputs;
let fn_output = &input.sig.output;
let route_path = fn_name_to_route_path(fn_name);
let _path_params = extract_path_params(fn_inputs);
let expanded = quote! {
#[allow(non_camel_case_types)]
#fn_vis struct #fn_name;
impl #fn_name {
#fn_vis fn new() -> Self {
println!("Registering API: {}", #route_path);
Self
}
#fn_vis #fn_generics fn call(#fn_inputs) #fn_output {
#fn_block
}
}
#[allow(non_upper_case_globals)]
#fn_vis static #fn_name: () = {
let _ = #fn_name::new();
()
};
};
TokenStream::from(expanded)
}
#[proc_macro_attribute]
pub fn config(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as syn::ItemStruct);
let struct_name = &input.ident;
let expanded = quote! {
#input
impl #struct_name {
pub fn load() -> Self {
println!("Loading configuration for {}", stringify!(#struct_name));
Self::default()
}
}
impl Default for #struct_name {
fn default() -> Self {
Self {
..Default::default()
}
}
}
};
TokenStream::from(expanded)
}
#[proc_macro_attribute]
pub fn static_props(attr: TokenStream, item: TokenStream) -> TokenStream {
let args = parse_macro_input!(attr as StaticPropsArgs);
let input = parse_macro_input!(item as ItemFn);
let fn_name = &input.sig.ident;
let fn_vis = &input.vis;
let fn_block = &input.block;
let fn_inputs = &input.sig.inputs;
let revalidate = match args.revalidate {
Some(seconds) => quote! { Some(#seconds) },
None => quote! { None },
};
let expanded = quote! {
#[allow(non_upper_case_globals)]
#fn_vis static #fn_name: ::densha::prelude::StaticPropsFunction = ::densha::prelude::StaticPropsFunction {
name: stringify!(#fn_name),
revalidate: #revalidate,
func: |ctx| async move {
let params = ::densha::prelude::parse_params_from_context(&ctx)
.expect(&format!("Failed to parse parameters for {}", stringify!(#fn_name)));
async fn implementation(#fn_inputs) -> ::anyhow::Result<::serde_json::Value> {
#fn_block
}
implementation(params).await
},
};
};
TokenStream::from(expanded)
}
#[proc_macro_attribute]
pub fn static_paths(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let fn_name = &input.sig.ident;
let fn_vis = &input.vis;
let fn_block = &input.block;
let expanded = quote! {
#[allow(non_upper_case_globals)]
#fn_vis static #fn_name: ::densha::prelude::StaticPathsFunction = ::densha::prelude::StaticPathsFunction {
name: stringify!(#fn_name),
func: || async move {
async fn implementation() -> ::anyhow::Result<::std::vec::Vec<::serde_json::Value>> {
#fn_block
}
implementation().await
},
};
};
TokenStream::from(expanded)
}
fn fn_name_to_route_path(ident: &Ident) -> String {
let name = ident.to_string();
if name == "index" {
return "/".to_string();
}
let segments: Vec<&str> = name.split('_').collect();
let path = segments.join("/");
format!("/{}", path)
}
fn extract_path_params(
inputs: &syn::punctuated::Punctuated<FnArg, syn::token::Comma>,
) -> Vec<String> {
let mut params = Vec::new();
for input in inputs {
match input {
FnArg::Typed(PatType { pat, .. }) => {
if let Pat::Ident(pat_ident) = &**pat {
let param_name = pat_ident.ident.to_string();
params.push(param_name);
}
}
_ => {}
}
}
params
}