238 lines
6.8 KiB
Rust
238 lines
6.8 KiB
Rust
|
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};
|
||
|
|
||
|
struct Field {
|
||
|
name: Ident,
|
||
|
ty: Ident,
|
||
|
}
|
||
|
|
||
|
struct PackInput {
|
||
|
struct_name: syn::Ident,
|
||
|
fields: Vec<Field>,
|
||
|
}
|
||
|
|
||
|
impl Parse for PackInput {
|
||
|
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||
|
let struct_name = input.parse()?;
|
||
|
let _ = input.parse::<Token![,]>()?;
|
||
|
|
||
|
let mut fields = Vec::new();
|
||
|
|
||
|
loop {
|
||
|
let ty = input.parse()?;
|
||
|
let _alias_keyword = input.parse::<Token![as]>()?;
|
||
|
let name = input.parse()?;
|
||
|
|
||
|
fields.push(Field { name, ty });
|
||
|
|
||
|
if input.is_empty() {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
let _ = input.parse::<Token![&]>()?;
|
||
|
}
|
||
|
|
||
|
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 export_path = format!("bindings/packed/{}.ts", struct_name);
|
||
|
|
||
|
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
|
||
|
),*
|
||
|
}
|
||
|
};
|
||
|
|
||
|
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::<MetaNameValue, Token![,]>::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 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_bindings_getuserbyid() {
|
||
|
#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<Dependency> {
|
||
|
vec![
|
||
|
Dependency::from_ty::<<#struct_name as Endpoint>::Request>().unwrap(),
|
||
|
Dependency::from_ty::<<#struct_name as Endpoint>::Response>().unwrap(),
|
||
|
]
|
||
|
}
|
||
|
|
||
|
fn transparent() -> bool {
|
||
|
false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
};
|
||
|
|
||
|
TokenStream::from(expanded)
|
||
|
}
|