use quote::ToTokens;
use syn::spanned::Spanned;
fn ident_name(ident: &syn::Ident) -> String {
let ident = ident.to_string();
ident.strip_prefix("r#").map(String::from).unwrap_or(ident)
}
fn struct_field_ident(ident: &syn::Ident) -> syn::Ident {
syn::Ident::new(&format!("{}_name", ident_name(ident)), ident.span())
}
fn try_derive_form_input_names(
input: &syn::DeriveInput,
) -> Result<proc_macro2::TokenStream, syn::Error> {
let ident = &input.ident;
let mod_ident = {
let mut mod_ident = String::new();
for (index, c) in ident.to_string().char_indices() {
if c.is_uppercase() {
if index > 0 {
mod_ident.push('_');
}
mod_ident.extend(c.to_lowercase());
} else {
mod_ident.push(c);
}
}
syn::Ident::new(&mod_ident, ident.span())
};
let syn::Data::Struct(syn::DataStruct {
struct_token: _,
fields,
semi_token: _,
}) = &input.data
else {
return Err(syn::Error::new(input.span(), "Must be a struct"));
};
let fields = fields
.iter()
.map(|field| {
field
.ident
.as_ref()
.ok_or_else(|| syn::Error::new(field.span(), "Fields must be named"))
.map(|field_ident| (struct_field_ident(field_ident), ident_name(field_ident)))
})
.collect::<Result<Vec<_>, _>>()?;
let form_struct_definition = fields
.iter()
.map(|(ident, _)| quote::quote! { pub #ident: &'static str });
let form_struct_declaration = fields
.iter()
.map(|(ident, name)| quote::quote! { #ident: #name });
let vis = &input.vis;
Ok(quote::quote! {
#vis mod #mod_ident {
pub struct Form {
#(#form_struct_definition,)*
}
pub const FORM: Form = Form {
#(#form_struct_declaration,)*
};
}
})
}
#[proc_macro_derive(FormInputNames, attributes(helper))]
pub fn derive_form_input_names(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = syn::parse_macro_input!(input as syn::DeriveInput);
match try_derive_form_input_names(&input) {
Ok(tokens) => tokens.into(),
Err(error) => error.into_compile_error().into(),
}
}
fn maybe_extract_attributes<T: deluxe::HasAttributes, R: deluxe::ExtractAttributes<T>>(
obj: &mut T,
) -> deluxe::Result<Option<R>> {
obj.attrs()
.iter()
.any(|attribute| R::path_matches(attribute.path()))
.then(|| deluxe::extract_attributes(obj))
.transpose()
}
struct ActionFormInput {
field_attributes: proc_macro2::TokenStream,
ident: syn::Ident,
rename: Option<syn::Expr>,
form_name: syn::Ident,
ty: syn::Type,
}
mod optional_attribute {
pub fn parse_meta_item_named<T: syn::parse::Parse>(
input: syn::parse::ParseStream,
_name: &str,
_span: proc_macro2::Span,
) -> syn::Result<Option<Option<T>>> {
Ok(Some(if input.is_empty() {
None
} else {
let lookahead = input.lookahead1();
Some(if lookahead.peek(syn::token::Paren) {
let content;
syn::parenthesized!(content in input);
content.parse()?
} else if lookahead.peek(syn::token::Eq) {
input.parse::<syn::token::Eq>()?;
input.parse()?
} else {
return Err(lookahead.error());
})
}))
}
}
#[derive(deluxe::ExtractAttributes)]
#[deluxe(attributes(action))]
struct ActionAttribute {
form: Option<syn::Type>,
#[deluxe(default, with = optional_attribute)]
custom: Option<Option<syn::Path>>,
}
enum ActionType {
Regular {
form: Option<syn::Type>,
form_inputs: Vec<ActionFormInput>,
},
Custom {
form: Option<syn::Path>,
},
}
impl ActionType {
fn form_parameter(&self, common_form: &syn::Type) -> Option<proc_macro2::TokenStream> {
match self {
Self::Regular { form, form_inputs } => {
let form = form.as_ref().unwrap_or(common_form);
let form_field_names = form_inputs
.iter()
.map(|ActionFormInput { ident, .. }| ident)
.collect::<Vec<_>>();
Some(quote::quote! { #form(Form { #(#form_field_names,)* }) })
}
Self::Custom { .. } => None,
}
}
}
struct Action {
ident: syn::Ident,
action_type: ActionType,
other_arguments: Vec<syn::Ident>,
all_arguments: Vec<syn::Ident>,
}
impl Action {
fn extract(
ActionAttribute { form, custom }: ActionAttribute,
f: &mut syn::ItemFn,
) -> syn::Result<Self> {
let mut form_inputs = Vec::new();
let mut other_arguments = Vec::new();
let mut all_arguments = Vec::new();
for (index, input) in f.sig.inputs.iter_mut().enumerate() {
match input {
syn::FnArg::Receiver(_) => {
return Err(syn::Error::new(input.span(), r#""self" is not allowed"#));
}
syn::FnArg::Typed(syn::PatType {
attrs,
pat,
colon_token: _,
ty,
}) => {
#[derive(deluxe::ExtractAttributes)]
#[deluxe(attributes = [form_input])]
struct FormAttrs {
#[deluxe(default)]
rename: Option<syn::Expr>,
#[deluxe(rename = field_attribute, default, append)]
field_attributes: Vec<proc_macro2::TokenStream>,
}
fn pat_ident(pat: &syn::Pat) -> Option<syn::Ident> {
if let syn::Pat::Ident(syn::PatIdent { ident, .. }) = pat {
Some(ident.clone())
} else {
None
}
}
if let Some(FormAttrs {
rename,
field_attributes,
}) = maybe_extract_attributes(attrs)?
{
let field_attributes = field_attributes
.into_iter()
.map(|field_attribute| quote::quote! { #[#field_attribute] })
.collect();
let ident = pat_ident(pat).ok_or_else(|| {
syn::Error::new(
pat.span(),
"parameters tagged with #[form_input] must be identifiers",
)
})?;
let form_name = struct_field_ident(&ident);
all_arguments.push(ident.clone());
form_inputs.push(ActionFormInput {
field_attributes,
ident,
rename,
form_name,
ty: ty.as_ref().clone(),
});
} else {
let ident = pat_ident(pat).unwrap_or_else(|| {
syn::Ident::new(&format!("arg_{index}"), pat.span())
});
all_arguments.push(ident.clone());
other_arguments.push(ident);
}
}
}
}
let custom_span = custom.span();
let action_type = match custom {
Some(form) => {
let [] = form_inputs.as_slice() else {
return Err(syn::Error::new(
custom_span,
r##"Actions with the "custom" attribute must not have "#[form_input]"s"##,
));
};
ActionType::Custom { form }
}
None => ActionType::Regular { form, form_inputs },
};
Ok(Self {
ident: f.sig.ident.clone(),
action_type,
other_arguments,
all_arguments,
})
}
fn query(&self) -> String {
format!("/{}", self.ident)
}
fn struct_declaration(&self) -> Option<proc_macro2::TokenStream> {
match &self.action_type {
ActionType::Regular {
form: _,
form_inputs,
} => {
let form_fields = form_inputs.iter().map(
|ActionFormInput {
field_attributes,
ident,
rename,
form_name: _,
ty,
}| {
let rename = rename
.as_ref()
.map(|name| quote::quote! { #[serde(rename = #name)] });
quote::quote! { #field_attributes #rename #ident: #ty }
},
);
Some(quote::quote! {
#[derive(serde::Deserialize)]
struct Form {
#(#form_fields,)*
}
})
}
ActionType::Custom { .. } => None,
}
}
}
struct Actions {
actions: Vec<Action>,
}
impl Actions {
fn extract(items: &mut [syn::Item]) -> syn::Result<Self> {
let mut actions = Vec::new();
for item in items {
let syn::Item::Fn(f) = item else {
continue;
};
let Some(action_attribute) = maybe_extract_attributes(f)? else {
continue;
};
actions.push(Action::extract(action_attribute, f)?);
}
Ok(Self { actions })
}
}
#[derive(deluxe::ParseMetaItem)]
struct AxumActionAttributes {
#[deluxe(default = syn::parse_quote!(axum::extract::Form) )]
form: syn::Type,
#[deluxe(default = syn::Ident::new("actions_handler", proc_macro2::Span::call_site()))]
handler: syn::Ident,
}
#[derive(deluxe::ParseMetaItem)]
struct PicoserveActionAttributes {
#[deluxe(default)]
path_parameters: Vec<syn::Type>,
#[deluxe(default = syn::parse_quote!(picoserve::extract::Form) )]
form: syn::Type,
#[deluxe(default = syn::Ident::new("ActionsHandler", proc_macro2::Span::call_site()))]
handler: syn::Ident,
}
mod optional_struct {
pub fn parse_meta_item_named<T: deluxe::ParseMetaItem>(
input: syn::parse::ParseStream,
_name: &str,
span: proc_macro2::Span,
) -> deluxe::Result<Option<T>> {
deluxe_core::parse_helpers::parse_named_meta_item(input, span).map(Some)
}
}
#[derive(deluxe::ParseMetaItem)]
struct ActionAttributes {
#[deluxe(default)]
state: Option<syn::Type>,
#[deluxe(default, with = optional_struct)]
axum: Option<AxumActionAttributes>,
#[deluxe(default, with = optional_struct)]
picoserve: Option<PicoserveActionAttributes>,
}
fn axum_handler(
state: &Option<syn::Type>,
AxumActionAttributes { form, handler }: AxumActionAttributes,
actions: &[Action],
) -> syn::Result<syn::ItemFn> {
let state_argument = state.as_ref().map(|state| {
quote::quote! {
axum::extract::State(state): axum::extract::State<#state>,
}
});
let action_cases = actions.iter().map(
|action @ Action {
ident,
action_type,
other_arguments,
all_arguments,
}| {
let query = action.query();
let struct_declaration = action.struct_declaration();
let form_parameter = action_type.form_parameter(&form);
let action_call = quote::quote! {
|#(#other_arguments,)* #form_parameter| async move {
#ident ( #(#all_arguments,)* ).await.into_response()
}
};
let state_value = if state.is_some() {
quote::quote! { state }
} else {
quote::quote! { () }
};
quote::quote! {
Some(#query) => {
#struct_declaration
Handler::call(
#action_call,
request,
#state_value,
)
.await
},
}
},
);
Ok(syn::parse_quote! {
async fn #handler(
#state_argument
axum::extract::RawQuery(query): axum::extract::RawQuery,
request: axum::extract::Request,
) -> axum::response::Response {
use axum::{handler::Handler, response::IntoResponse};
match html_form_actions::query_action(query.as_deref()) {
#(#action_cases)*
_ => (axum::http::StatusCode::NOT_FOUND, "Action Not Found").into_response(),
}
}
})
}
fn picoserve_handler(
state: &Option<syn::Type>,
PicoserveActionAttributes {
path_parameters,
form,
handler,
}: PicoserveActionAttributes,
actions: &[Action],
) -> (syn::ItemStruct, syn::ItemImpl) {
let generic_state_name = quote::quote! {State};
let state_generics = state
.is_none()
.then(|| quote::quote! { < #generic_state_name > });
let state = state
.as_ref()
.map_or(generic_state_name, ToTokens::into_token_stream);
let path_parameter_names = path_parameters
.iter()
.enumerate()
.map(|(index, ty)| syn::Ident::new(&format!("path_parameter_{index}"), ty.span()))
.collect::<Vec<_>>();
let action_cases = actions.iter().map(
|action @ Action {
ident,
action_type,
other_arguments,
all_arguments,
}| {
let query = action.query();
let struct_declaration = action.struct_declaration();
let form_parameter = action_type.form_parameter(&form);
let action_call = quote::quote! {
|#(#other_arguments,)* #form_parameter| async move {
#ident ( #(#all_arguments,)*).await
}
};
let path_parameter_list = match path_parameter_names.as_slice() {
[] => quote::quote! { picoserve::routing::NoPathParameters },
[name] => quote::quote! { picoserve::routing::OnePathParameter(#name) },
list => quote::quote! { picoserve::routing::ManyPathParameters((#(#list,)*)) },
};
quote::quote! {
Some(query) if query == #query => {
#struct_declaration
picoserve::routing::RequestHandlerFunction::call_handler_func(
&#action_call,
state,
#path_parameter_list,
request,
response_writer,
)
.await
}
}
},
);
let impl_item = syn::parse_quote! {
impl #state_generics picoserve::routing::RequestHandlerService<#state, (#(#path_parameters,)*)> for #handler {
async fn call_request_handler_service<
R: picoserve::io::Read,
W: picoserve::response::ResponseWriter<Error = R::Error>,
>(
&self,
state: &#state,
(#(#path_parameter_names,)*) : (#(#path_parameters,)*),
request: picoserve::request::Request<'_, R>,
response_writer: W,
) -> Result<picoserve::ResponseSent, W::Error> {
use picoserve::response::IntoResponse;
match html_form_actions::query_action(request.parts.query().map(|query| query.0)) {
#(#action_cases)*
_ => {
(
picoserve::response::StatusCode::NOT_FOUND,
"Action Not Found",
)
.write_to(request.body_connection.finalize().await?, response_writer)
.await
}
}
}
}
};
(syn::parse_quote! { struct #handler; }, impl_item)
}
fn try_actions(
attribute_tokens: proc_macro::TokenStream,
tokens: proc_macro::TokenStream,
) -> syn::Result<proc_macro::TokenStream> {
let ActionAttributes {
state,
axum,
picoserve,
} = deluxe::parse(attribute_tokens)?;
let module = syn::parse(tokens)?;
let syn::ItemMod {
attrs,
vis,
unsafety,
mod_token,
ident,
content: Some((brace, mut items)),
semi,
} = module
else {
return Err(syn::Error::new(
module.span(),
"The module must have content",
));
};
let Actions { actions } = Actions::extract(&mut items)?;
let action_modules = actions.iter().map(
|Action {
ident,
action_type,
other_arguments: _,
all_arguments: _,
}| {
let action = format!("?/{ident}");
let form_inputs = match action_type {
ActionType::Regular {
form: _,
form_inputs,
} => form_inputs.as_slice(),
ActionType::Custom { .. } => &[],
};
let custom_form_names = match action_type {
ActionType::Regular { .. } => None,
ActionType::Custom { form } => form.as_ref().and_then(|names| {
let form_name = &names.segments.last()?.ident;
let form_name_ident =
syn::Ident::new(&format!("{form_name}_form_input_names"), form_name.span());
Some((form_name_ident, names))
}),
};
let form_struct_field_definitions = form_inputs
.iter()
.map(|input| {
let form_name = &input.form_name;
quote::quote! { pub(super) #form_name: &'static str }
})
.chain(
custom_form_names
.as_ref()
.map(|(ident, names)| quote::quote! { pub(super) #ident: #names::Form }),
);
let form_struct_field_declarations = form_inputs
.iter()
.map(
|ActionFormInput {
field_attributes: _,
ident,
rename,
form_name,
ty: _,
}| {
let name = rename.as_ref().map_or_else(
|| ident_name(ident).to_token_stream(),
quote::ToTokens::to_token_stream,
);
quote::quote! { #form_name: #name }
},
)
.chain(
custom_form_names.map(|(ident, names)| quote::quote! { #ident: #names::FORM }),
);
syn::Item::Mod(syn::parse_quote! {
mod #ident {
pub(super)struct Form {
pub(super) action: &'static str,
#(#form_struct_field_definitions,)*
}
pub(super) const FORM: Form = Form {
action: #action,
#(#form_struct_field_declarations,)*
};
}
})
},
);
items.extend(action_modules);
if let Some(axum) = axum {
items.push(syn::Item::Fn(axum_handler(&state, axum, &actions)?));
}
if let Some(picoserve) = picoserve {
let (service, service_impl) = picoserve_handler(&state, picoserve, &actions);
items.extend([syn::Item::Struct(service), syn::Item::Impl(service_impl)]);
}
Ok(syn::ItemMod {
attrs,
vis,
unsafety,
mod_token,
ident,
content: Some((brace, items)),
semi,
}
.to_token_stream()
.into())
}
#[proc_macro_attribute]
pub fn actions(
attribute_tokens: proc_macro::TokenStream,
tokens: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
try_actions(attribute_tokens, tokens)
.map_or_else(|error| error.into_compile_error().into(), From::from)
}