use proc_macro::TokenStream; use std::collections::HashSet; use syn::parse::Parse; use syn::punctuated::Punctuated; use syn::{Expr, ExprLit, ExprPath, Ident, Lit, Meta, MetaNameValue, Token, Type}; struct Field { name: Ident, ty: Type, } struct PackInput { struct_name: syn::Ident, fields: Vec, } impl Parse for PackInput { fn parse(input: syn::parse::ParseStream) -> syn::Result { let struct_name = input.parse()?; let _ = input.parse::()?; let mut fields = Vec::new(); loop { let ty = input.parse()?; let _alias_keyword = input.parse::()?; let name = input.parse()?; fields.push(Field { name, ty }); if input.is_empty() { break; } let _ = input.parse::()?; } Ok(PackInput { struct_name, fields, }) } } #[proc_macro] pub fn pack(item: TokenStream) -> TokenStream { let parsed = syn::parse_macro_input!(item as PackInput); let struct_name = &parsed.struct_name; let fields = &parsed.fields; let names = fields.iter().map(|f| &f.name); let types = fields.iter().map(|f| &f.ty); let types_packed = fields.iter().map(|f| &f.ty); let names_packed = fields.iter().map(|f| &f.name); let iota = (0..fields.len()).map(syn::Index::from); let export_path = format!("bindings/packed/{}.ts", struct_name); let tuple = quote::quote! { (#(#types_packed,)*) }; let expanded = quote::quote! { #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[ts(export, export_to = #export_path)] pub struct #struct_name { #( #[serde(flatten)] pub #names: #types ),* } impl Packed for #struct_name { type Input = #tuple; fn pack_from(from: #tuple) -> Self { #struct_name { #( #names_packed: from.#iota ),* } } } }; TokenStream::from(expanded) } #[proc_macro_derive(Endpoint, attributes(name, endpoint, method, request, response))] pub fn derive_endpoint(item: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(item as syn::DeriveInput); let struct_name = input.ident; assert!(input.generics.params.is_empty(), "generics not supported"); let where_clause = input.generics.where_clause; assert!(where_clause.is_none(), "where clauses not supported"); let data = match input.data { syn::Data::Struct(data) => data, _ => panic!("only unit structs are supported"), }; assert!( matches!(data.fields, syn::Fields::Unit), "only unit structs are supported" ); let mut name = struct_name.to_string(); let mut endpoint = None; let mut method = None; let mut request = None; let mut response = None; let mut found = HashSet::new(); let struct_attrs = input.attrs; for struct_attr in struct_attrs { if !struct_attr.meta.path().is_ident("endpoint") { continue; } if struct_attr.style != syn::AttrStyle::Outer { panic!("expected outer attribute"); } let Meta::List(list) = struct_attr.meta else { panic!("expected a list of attributes"); }; let nested = list .parse_args_with(Punctuated::::parse_terminated) .expect("expected a list of attributes"); for kvp in nested { let key = kvp .path .get_ident() .expect("expected a single identifier") .to_owned(); let value = kvp.value; if found.contains(&key) { panic!("duplicate attribute: {}", key); } match key.to_string().as_ref() { "name" => { let Expr::Lit(ExprLit { lit: Lit::Str(new_name), .. }) = value else { panic!("expected a string literal"); }; name = new_name.value(); } "endpoint" => { let Expr::Lit(ExprLit { lit: Lit::Str(endpoint_val), .. }) = value else { panic!("expected a string literal"); }; endpoint = Some(endpoint_val.value()); } "method" => { let Expr::Path(ExprPath { path, .. }) = value else { panic!("expected a an identifier"); }; method = Some(path); } "request" => { let Expr::Path(ExprPath { path, .. }) = value else { panic!("expected a an identifier"); }; request = Some(path); } "response" => { let Expr::Path(ExprPath { path, .. }) = value else { panic!("expected an identifier") }; response = Some(path); } _ => panic!("unknown attribute: {}", key), } found.insert(key); } } let endpoint = endpoint.expect("missing endpoint attribute"); let method = method.expect("missing method attribute"); let request = request.expect("missing request attribute"); let response = response.expect("missing response attribute"); let ts_path = format!("bindings/endpoints/{}.ts", name); let export_name = Ident::new( &format!("export_bindings_{}", struct_name.to_string().to_lowercase()), struct_name.span(), ); let expanded = quote::quote! { impl Default for #struct_name { fn default() -> Self { Self } } impl Endpoint for #struct_name { const NAME: &'static str = #name; const ENDPOINT: &'static str = #endpoint; const METHOD: Method = #method; type Request = #request; type Response = #response; } #[cfg(test)] #[test] fn #export_name() { #struct_name::export().expect("could not export type"); } impl TS for #struct_name { const EXPORT_TO: Option<&'static str> = Some(#ts_path); fn decl() -> String { format!( "interface {} {{\n \ endpoint: \"{}\";\n \ method: \"{}\";\n \ request: {};\n \ response: {};\n\ }} ", Self::name(), Self::ENDPOINT, Self::METHOD, <#struct_name as Endpoint>::Request::name(), <#struct_name as Endpoint>::Response::name() ) } fn name() -> String { #struct_name::NAME.to_string() } fn dependencies() -> Vec { vec![ ts_rs::Dependency::from_ty::<<#struct_name as Endpoint>::Request>().unwrap(), ts_rs::Dependency::from_ty::<<#struct_name as Endpoint>::Response>().unwrap(), ] } fn transparent() -> bool { false } } }; TokenStream::from(expanded) }