262 lines
7.5 KiB
Rust
262 lines
7.5 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, Type};
|
|
|
|
struct Field {
|
|
name: Ident,
|
|
ty: Type,
|
|
}
|
|
|
|
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 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::<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 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<ts_rs::Dependency> {
|
|
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)
|
|
}
|