magnetar/magnetar_sdk/macros/src/lib.rs

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)
}