439 lines
13 KiB
Rust
439 lines
13 KiB
Rust
use proc_macro::TokenStream;
|
|
use quote::ToTokens;
|
|
use std::collections::HashSet;
|
|
use syn::parse::Parse;
|
|
use syn::punctuated::Punctuated;
|
|
use syn::{
|
|
Expr, ExprLit, ExprPath, GenericArgument, Ident, Lit, Meta, MetaNameValue, PathArguments,
|
|
Token, Type, TypePath,
|
|
};
|
|
|
|
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_decl = fields.iter().map(|f| &f.ty);
|
|
let types_struct = fields.iter().map(|f| &f.ty);
|
|
let types_deps = 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 name = struct_name.to_string();
|
|
|
|
let export_name = Ident::new(
|
|
&format!("export_bindings_{}", struct_name.to_string().to_lowercase()),
|
|
struct_name.span(),
|
|
);
|
|
|
|
let expanded = quote::quote! {
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
pub struct #struct_name {
|
|
#(
|
|
#[serde(flatten)]
|
|
pub #names: #types_struct
|
|
),*
|
|
}
|
|
|
|
impl Packed for #struct_name {
|
|
type Input = #tuple;
|
|
|
|
fn pack_from(from: #tuple) -> Self {
|
|
#struct_name {
|
|
#(
|
|
#names_packed: from.#iota
|
|
),*
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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(#export_path);
|
|
|
|
fn decl() -> String {
|
|
format!(
|
|
"type {} = {};",
|
|
Self::name(),
|
|
vec![#(<#types_decl as TS>::inline()),*].join(" & ")
|
|
)
|
|
}
|
|
|
|
fn name() -> String {
|
|
#name.to_string()
|
|
}
|
|
|
|
fn dependencies() -> Vec<ts_rs::Dependency> {
|
|
vec![
|
|
#(
|
|
<#types_deps as TS>::dependencies()
|
|
),*
|
|
].into_iter().flatten().collect::<Vec<_>>()
|
|
}
|
|
|
|
fn transparent() -> bool {
|
|
false
|
|
}
|
|
}
|
|
};
|
|
|
|
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 request_args = None;
|
|
let mut response_args = 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::Lit(ExprLit {
|
|
lit: Lit::Str(str_lit),
|
|
..
|
|
}) = value
|
|
else {
|
|
panic!("expected a an identifier");
|
|
};
|
|
|
|
let type_tok: TokenStream = str_lit.value().parse().unwrap();
|
|
let req_typ = syn::parse::<TypePath>(type_tok).unwrap();
|
|
|
|
if let Some(seg_last) = req_typ.path.segments.last() {
|
|
if let PathArguments::AngleBracketed(args) = &seg_last.arguments {
|
|
let args_res = args.args.iter().cloned().collect::<Vec<_>>();
|
|
request_args = Some(args_res);
|
|
}
|
|
}
|
|
|
|
request = Some(req_typ);
|
|
}
|
|
"response" => {
|
|
let Expr::Lit(ExprLit {
|
|
lit: Lit::Str(str_lit),
|
|
..
|
|
}) = value
|
|
else {
|
|
panic!("expected a an identifier");
|
|
};
|
|
|
|
let type_tok: TokenStream = str_lit.value().parse().unwrap();
|
|
let res_typ = syn::parse::<TypePath>(type_tok).unwrap();
|
|
if let Some(seg_last) = res_typ.path.segments.last() {
|
|
if let PathArguments::AngleBracketed(args) = &seg_last.arguments {
|
|
let args_res = args.args.iter().cloned().collect::<Vec<_>>();
|
|
response_args = Some(args_res);
|
|
}
|
|
}
|
|
response = Some(res_typ);
|
|
}
|
|
_ => 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 mut endpoint_chars = endpoint.chars();
|
|
let mut path_params = String::from("[");
|
|
while let Some(c) = endpoint_chars.next() {
|
|
if c == ':' {
|
|
if !path_params.ends_with('[') {
|
|
path_params += ", ";
|
|
}
|
|
|
|
path_params += "\"";
|
|
path_params += &endpoint_chars
|
|
.clone()
|
|
.take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
|
|
.collect::<String>();
|
|
path_params += "\"";
|
|
}
|
|
}
|
|
path_params += "]";
|
|
|
|
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 call_name_req = if let Some(ref args) = request_args {
|
|
let args_str = args
|
|
.iter()
|
|
.map(|a| a.into_token_stream().to_string())
|
|
.collect::<Vec<_>>();
|
|
|
|
quote::quote! {
|
|
<#struct_name as Endpoint>::Request::name_with_type_args(vec![#( #args_str.to_string() ),*])
|
|
}
|
|
} else {
|
|
quote::quote! {
|
|
<#struct_name as Endpoint>::Request::name()
|
|
}
|
|
};
|
|
|
|
let req_args_flat = request_args.clone().unwrap_or_default();
|
|
let mut res_args_flat = response_args.clone().unwrap_or_default();
|
|
|
|
let call_name_res = if let Some(ref args) = response_args {
|
|
let args_nested = args
|
|
.iter()
|
|
.map(|a| {
|
|
let normal = Vec::new();
|
|
|
|
let GenericArgument::Type(inner) = a else {
|
|
return normal;
|
|
};
|
|
let Type::Path(path_inner) = inner else {
|
|
return normal;
|
|
};
|
|
let Some(seg) = path_inner.path.segments.last() else {
|
|
return normal;
|
|
};
|
|
let PathArguments::AngleBracketed(angle_inner) = &seg.arguments else {
|
|
return normal;
|
|
};
|
|
angle_inner.args.iter().cloned().collect::<Vec<_>>()
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
res_args_flat.extend(args_nested.iter().flatten().cloned());
|
|
|
|
let arg_tokens_nested = args_nested
|
|
.iter()
|
|
.zip(args)
|
|
.map(|(args, parent)| {
|
|
if !args.is_empty() {
|
|
let names = args.iter().map(|a| a.to_token_stream().to_string());
|
|
quote::quote! {
|
|
<#parent as TS>::name_with_type_args(vec![#( #names.to_string() ),*])
|
|
}
|
|
} else {
|
|
quote::quote! {
|
|
<#parent as TS>::name()
|
|
}
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
quote::quote! {
|
|
<#struct_name as Endpoint>::Response::name_with_type_args(vec![#( #arg_tokens_nested ),*])
|
|
}
|
|
} else {
|
|
quote::quote! {
|
|
<#struct_name as Endpoint>::Response::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_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!(
|
|
"const {} = {{\n \
|
|
endpoint: \"{}\",\n \
|
|
pathParams: {} as {},\n \
|
|
method: \"{}\" as \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"PATCH\",\n \
|
|
request: undefined as unknown as {},\n \
|
|
response: undefined as unknown as {}\n\
|
|
}}
|
|
",
|
|
Self::name(),
|
|
Self::ENDPOINT,
|
|
#path_params,
|
|
#path_params,
|
|
Self::METHOD,
|
|
#call_name_req,
|
|
#call_name_res
|
|
)
|
|
}
|
|
|
|
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>(),
|
|
ts_rs::Dependency::from_ty::<<#struct_name as Endpoint>::Response>(),
|
|
#(
|
|
ts_rs::Dependency::from_ty::<#req_args_flat>(),
|
|
)*
|
|
#(
|
|
ts_rs::Dependency::from_ty::<#res_args_flat>(),
|
|
)*
|
|
].into_iter().flatten().collect::<Vec<_>>()
|
|
}
|
|
|
|
fn transparent() -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
TokenStream::from(expanded)
|
|
}
|