eagle/src/lib.rs

119 lines
3.9 KiB
Rust
Raw Normal View History

2024-06-19 23:25:45 +02:00
/*
Eagle - A library for creating protocols for full-stack Rust applications
Copyright (c) 2024 KodiCraft
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, spanned::Spanned, DeriveInput, Field, Ident};
#[proc_macro_derive(Protocol)]
pub fn derive_protocol(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
// Must be on an enum
let enum_ = match &input.data {
syn::Data::Enum(e) => e,
_ => {
return syn::Error::new(input.span(), "Protocol can only be derived on enums")
.to_compile_error()
.into()
}
};
let name = &input.ident;
let vis = &input.vis;
let mut server_trait = Vec::new();
let mut client_impl = Vec::new();
for variant in &enum_.variants {
// Every variant must have 2 fields
// The first field is the question (serverbound), the second field is the answer (clientbound)
if variant.fields.len() != 2 {
return syn::Error::new(
variant.span(),
"Every variant on a protocol must have exactly 2 fields",
)
.to_compile_error()
.into();
}
let var_name = ident_to_snake_case(&variant.ident);
let mut variant_fields = variant.fields.iter();
let question_field = variant_fields.next().unwrap();
let question_args = field_to_args(question_field);
let answer_type = variant_fields.next().unwrap().ty.clone();
server_trait.push(quote! {
fn #var_name(&mut self, #question_args) -> #answer_type;
});
client_impl.push(quote! {
pub fn #var_name(&mut self, #question_args) -> #answer_type {
::std::unimplemented!()
}
})
}
// Create a trait which the server will have to implement
let server_trait_name = Ident::new(&format!("{}Server", name), name.span());
let server_trait = quote! {
#vis trait #server_trait_name {
#(#server_trait)*
}
};
let client_struct_name = Ident::new(&format!("{}Client", name), name.span());
let client_struct = quote! {
#vis struct #client_struct_name; // TODO: This struct will have some fields to handle the actual connection
impl #client_struct_name {
#(#client_impl)*
}
};
let expanded = quote! {
#server_trait
#client_struct
};
expanded.into()
}
fn ident_to_snake_case(ident: &Ident) -> Ident {
let ident = ident.to_string();
let mut out = String::new();
for (i, c) in ident.chars().enumerate() {
if c.is_uppercase() {
if i != 0 {
out.push('_');
}
out.push(c.to_lowercase().next().unwrap());
} else {
out.push(c);
}
}
Ident::new(&out, ident.span())
}
fn field_to_args(field: &Field) -> proc_macro2::TokenStream {
let type_ = &field.ty;
if let syn::Type::Tuple(tuple) = type_ {
let mut args = Vec::new();
for (i, elem) in tuple.elems.iter().enumerate() {
let arg = Ident::new(&format!("arg{}", i), elem.span());
args.push(quote! { #arg: #elem });
}
quote! { #( #args ), * }
} else {
quote! { arg: #type_ }
}
}