/*
Eagle - A simple library for RPC in Rust
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 .
*/
//! # Eagle - A simple library for RPC in Rust
//!
//!
Eagle is still in early development. This documentation is subject to change and may not be entirely accurate.
//!
//! Eagle is a library for building RPC protocols in Rust. It uses a macro
//! to transform your protocol definition into the necessary code to allow
//! communication between a server and a client.
//!
//! Eagle uses [`tokio`](https://tokio.rs) as its async runtime and
//! [`ron`](https://crates.io/crates/ron) for serialization.
//!
//! ## Usage
//! `eagle` is designed to be used to create your own protocol crate. We
//! recommend creating a new cargo workspace for your project with a shared
//! crate which will contain your protocol definition and individual crates
//! for the server and client.
//!
//! In your shared crate, you can define your protocol using the [`Protocol`]
//! derive macro. This will generate the necessary code for the server and
//! client to communicate.
//!
//! ```rust
//! use eagle::Protocol;
//!
//! #[derive(Protocol)]
//! pub enum Example {
//! Add((i32, i32), i32),
//! Length(String, usize),
//! /* ... */
//! }
//! ```
//!
//! The [`Protocol`] derive macro will generate all the necessary code, including
//! your handler trait, your server struct, and your client struct.
//!
//! On your server, you will need to implement the handler trait. This trait
//! describes the functions that the client can request from the server.
//!
//! ```rust
//! # use eagle::Protocol;
//! # #[derive(Protocol)]
//! # pub enum Example {
//! # Add((i32, i32), i32),
//! # Length(String, usize),
//! # /* ... */
//! # }
//! #
//! #[derive(Clone)]
//! pub struct Handler;
//! impl ExampleServerHandler for Handler {
//! async fn add(&mut self, a: i32, b: i32) -> i32 {
//! a + b
//! }
//! async fn length(&mut self, s: String) -> usize {
//! s.len()
//! }
//! /* ... */
//! }
//! ```
//!
//! To start the server, you simply need to use the generated server struct and
//! pass it your handler.
//!
//! ```no_run
//! # use eagle::Protocol;
//! # #[derive(Protocol)]
//! # pub enum Example {
//! # Add((i32, i32), i32),
//! # Length(String, usize),
//! # /* ... */
//! # }
//! #
//! # #[derive(Clone)]
//! # pub struct Handler;
//! # impl ExampleServerHandler for Handler {
//! # async fn add(&mut self, a: i32, b: i32) -> i32 {
//! # a + b
//! # }
//! # async fn length(&mut self, s: String) -> usize {
//! # s.len()
//! # }
//! # }
//! #
//! # tokio_test::block_on(async {
//! let handler = Handler;
//! let address = "127.0.0.1:12345"; // Or, if using the 'unix' feature, "/tmp/eagle.sock"
//! let server_task = tokio::spawn(ExampleServer::bind(handler, address));
//! # });
//! ```
//!
//! Please note the usage of `tokio::spawn`. This is because the `bind` function
//! will not return until the server is closed. You can use the `abort` method
//! on the task to close the server.
//!
//! On the client side, you can simply use the generated client struct to connect
//! to the server and begin sending queries.
//!
//! ```no_run
//! # use eagle::Protocol;
//! # #[derive(Protocol)]
//! # pub enum Example {
//! # Add((i32, i32), i32),
//! # Length(String, usize),
//! # /* ... */
//! # }
//! #
//! # #[derive(Clone)]
//! # pub struct Handler;
//! # impl ExampleServerHandler for Handler {
//! # async fn add(&mut self, a: i32, b: i32) -> i32 {
//! # a + b
//! # }
//! # async fn length(&mut self, s: String) -> usize {
//! # s.len()
//! # }
//! # }
//! #
//! # tokio_test::block_on(async {
//! # let handler = Handler;
//! let address = "127.0.0.1:12345"; // Or, if using the 'unix' feature, "/tmp/eagle.sock"
//! # let server_task = tokio::spawn(ExampleServer::bind(handler, address));
//! let client = ExampleClient::connect(address).await.unwrap();
//! # // Wait for the server to start, the developer is responsible for this in production
//! # tokio::time::sleep(std::time::Duration::from_millis(10)).await;
//! assert_eq!(client.add(2, 5).await.unwrap(), 7);
//! # });
//! ```
//!
//! The client can be closed by calling the `close` method on the client struct.
//! This will abort the connection.
#![warn(missing_docs)]
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse2, spanned::Spanned, DeriveInput, Field, Ident};
#[cfg(all(feature = "tcp", feature = "unix"))]
compile_error!("You can only enable one of the 'tcp' or 'unix' features");
#[cfg(all(not(feature = "tcp"), not(feature = "unix")))]
compile_error!("You must enable either the 'tcp' or 'unix' feature");
#[cfg(all(feature = "unix", not(unix)))]
compile_error!("The 'unix' feature requires compiling for a unix target");
/// Generate all the necessary RPC code for a protocol from an enum describing it.
///
/// This macro will generate various enums and structs to enable communication
/// between a server and a client. The following items will be generated, where {}
/// is the name of the protocol enum:
/// - `{}ServerHandler` - A trait that the server must implement to handle queries
/// - `{}Server` - A struct that the server uses to communicate with clients
/// - `{}Client` - A struct that the client uses to communicate with a server
///
/// Each variant of the passed enum represents a query that the client can send to the
/// server. The first field of each variant is the question (serverbound), the second field
/// is the answer (clientbound). You may use tuples to represent sending multiple arguments and
/// you may use the unit type `()` to represent no arguments. Only data types which implement
/// [`Clone`], [`serde::Serialize`], and [`serde::Deserialize`] can be used.
///
/// For more information on how to use the generated code, see the [crate-level documentation](index.html).
///
/// # Example
/// ```rust
/// use eagle::Protocol;
///
/// #[derive(Protocol)]
/// pub enum Example {
/// Add((i32, i32), i32),
/// Length(String, usize),
/// }
/// ```
#[proc_macro_derive(Protocol)]
pub fn derive_protocol_derive(input: TokenStream) -> TokenStream {
let expanded = derive_protocol(input.into());
TokenStream::from(expanded)
}
fn derive_protocol(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
let input = parse2::(input).unwrap();
// TODO: These logs should be filterable in some way
#[cfg(feature = "log")]
#[allow(unused_variables)]
let debug = quote! { log::debug! };
#[cfg(feature = "log")]
#[allow(unused_variables)]
let info = quote! { log::info! };
#[cfg(feature = "log")]
#[allow(unused_variables)]
let warn = quote! { log::warn! };
#[cfg(feature = "log")]
#[allow(unused_variables)]
let error = quote! { log::error! };
#[cfg(not(feature = "log"))]
#[allow(unused_variables)]
let debug = quote! { eprintln! };
#[cfg(not(feature = "log"))]
#[allow(unused_variables)]
let info = quote! { eprintln! };
#[cfg(not(feature = "log"))]
#[allow(unused_variables)]
let warn = quote! { eprintln! };
#[cfg(not(feature = "log"))]
#[allow(unused_variables)]
let error = quote! { eprintln! };
// 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()
}
};
let name = &input.ident;
let error_enum_name = format_ident!("__{}Error", name);
let answer_enum_name = format_ident!("__{}Answer", name);
let question_enum_name = format_ident!("__{}Question", name);
let query_enum_name = format_ident!("__{}Query", name);
let queries_struct_name = format_ident!("__{}Queries", name);
let client_connection_struct_name = format_ident!("__{}Connection", name);
let server_trait_name = format_ident!("{}ServerHandler", name);
let server_connection_struct_name = format_ident!("{}Server", name);
let client_struct_name = format_ident!("{}Client", name);
let vis = &input.vis;
let mut server_trait = Vec::new();
let mut server_enum = Vec::new();
let mut client_impl = Vec::new();
let mut client_enum = Vec::new();
let mut server_handler = Vec::new();
let mut query_enum = Vec::new();
let mut query_from_question_enum = Vec::new();
let mut query_set_answer = Vec::new();
let mut query_get_answer = 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();
}
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 question_handler_args = field_to_handler_args(question_field);
let question_tuple_args = field_to_tuple_args(question_field);
let answer_type = variant_fields.next().unwrap().ty.clone();
// The variants that either the server or the client will use
// The "server" enum contains messages the server can send, the "client" enum contains messages the client can send
server_enum.push(quote! {
#var_name(#answer_type)
});
client_enum.push(quote! {
#var_name(#question_field)
});
// There is a From implementation for the client enum to the query enum
query_from_question_enum.push(quote! {
#question_enum_name::#var_name(question) => #query_enum_name::#var_name(question, None),
});
// There is a function that must be implemented to set the answer in the query enum
query_set_answer.push(quote! {
#query_enum_name::#var_name(question, answer_opt) => match answer {
#answer_enum_name::#var_name(answer) => {
#debug("Setting answer for query {}", stringify!(#var_name));
answer_opt.replace(answer);
},
_ => panic!("The answer for this query is not the correct type."),
},
});
// There is a function that must be implemented to get the answer from the query enum
query_get_answer.push(quote! {
#query_enum_name::#var_name(_, answer) => match answer {
Some(answer) => Some(#answer_enum_name::#var_name(answer.clone())),
None => None
},
});
// There is a function that the server uses to call the appropriate function when receiving a query
server_handler.push(quote! {
#question_enum_name::#var_name(#question_tuple_args) => {
#info("Received query {}", stringify!(#var_name));
let answer = self.handler.lock().await.#var_name(#question_handler_args).await;
return #answer_enum_name::#var_name(answer);
},
});
// The function that the server needs to implement
server_trait.push(quote! {
fn #var_name(&mut self, #question_args) -> impl std::future::Future