working example

This commit is contained in:
Jonas Rabenstein 2026-03-05 01:57:37 +01:00
commit e69bcfc23d
18 changed files with 1290 additions and 252 deletions

View file

@ -1,21 +1,67 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn, FnArg, Pat, PatType, PatIdent, Type, LitStr};
use syn::{parse_macro_input, ItemFn, LitStr};
use syn::{punctuated::Punctuated, token::Comma};
fn extract_path_args(inputs: &Punctuated<syn::FnArg, Comma>) -> Vec<(syn::Ident, syn::Type)> {
let mut args = Vec::new();
for arg in inputs {
#[derive(Copy, Clone, PartialEq)]
enum Class {
Query,
Path,
Body,
}
impl Class {
fn classify(pat_type: &syn::PatType, default: Self) -> Self {
let mut result = None;
for a in pat_type.attrs.iter() {
let class = if a.path().is_ident("path") {
Some(Self::Path)
} else if a.path().is_ident("query") {
Some(Self::Query)
} else if a.path().is_ident("body") {
Some(Self::Body)
} else {
None
};
if class.is_some() {
if result.is_some() && result != class {
panic!("can only have one class!");
}
result = class;
}
}
if let Some(result) = result {
result
} else {
default
}
}
}
fn extract_args(inputs: &Punctuated<syn::FnArg, Comma>, class: Class, default: Class) -> (Vec<syn::Ident>, Vec<syn::Type>, Vec<proc_macro2::TokenStream>) {
let mut idents = Vec::new();
let mut types = Vec::new();
let mut attrs = Vec::new();
for arg in inputs.iter().skip(1) {
if let syn::FnArg::Typed(pat_type) = arg {
if pat_type.attrs.iter().any(|a| a.path().is_ident("path")) {
//if pat_type.attrs.iter().any(|a| a.path().is_ident(path)) {
if Class::classify(&pat_type, default) == class {
if let syn::Pat::Ident(pat_ident) = &*pat_type.pat {
args.push((pat_ident.ident.clone(), (*pat_type.ty).clone()));
idents.push(pat_ident.ident.clone());
types.push((*pat_type.ty).clone());
let meta = pat_type.attrs.iter()
.filter(|a| !["path", "query", "body"].iter().any(|p| a.path().is_ident(p)));
let meta = quote! {
#( #meta )*
};
attrs.push(meta);
}
}
}
}
args
(idents, types, attrs)
}
fn generate_endpoint(attr: TokenStream, item: TokenStream, method: &str) -> TokenStream {
@ -26,24 +72,50 @@ fn generate_endpoint(attr: TokenStream, item: TokenStream, method: &str) -> Toke
let vis = &item_fn.vis;
let generics = &item_fn.sig.generics;
let path_args = extract_path_args(&item_fn.sig.inputs);
let path_idents: Vec<_> = path_args.iter().map(|(id, _)| id).collect();
let path_types: Vec<_> = path_args.iter().map(|(_, ty)| ty).collect();
let default = Class::Body;
let (path_idents, path_types, path_attrs) = extract_args(&item_fn.sig.inputs, Class::Path, default);
let (query_idents, query_types, query_attrs) = extract_args(&item_fn.sig.inputs, Class::Query, default);
let (body_idents, body_types, body_attrs) = extract_args(&item_fn.sig.inputs, Class::Body, default);
let ret_type = match &item_fn.sig.output {
syn::ReturnType::Default => quote! { () },
syn::ReturnType::Type(_, ty) => quote! { #ty },
};
let queries = query_idents.len();
let expanded = quote! {
#[bon::builder]
#vis async fn #fn_name #generics(
#[builder(finish_fn)] client: restson::RestClient,
#( #[builder(finish_fn)] #path_idents: #path_types, )*
#[builder(finish_fn)] client: &restson::RestClient,
#( #[builder(finish_fn)] #path_attrs #path_idents: #path_types, )*
#( #query_attrs #query_idents: #query_types, )*
#( #body_attrs #body_idents: #body_types, )*
) -> Result<#ret_type, restson::Error> {
let path = format!(#path_lit, #( #path_idents = #path_idents, )* );
todo!("Replace this with client.{} call", #method)
let mut query = Vec::with_capacity(#queries);
#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct Q {
#(
#query_attrs #query_idents: #query_types,
)*
}
let q = Q { #( #query_idents, )* };
let s = serde_qs::to_string(&q).expect("serde_qs serialization");
for pair in s.split('&') {
let mut kv = pair.splitn(2, '=');
match (kv.next(), kv.next()) {
(Some(k), Some(v)) => query.push((k, v)),
(Some(k), None) => query.push((k, "")),
_ => panic!("should never happen!"),
}
}
todo!("client.{}({path}, {query:?})", #method)
}
};