diff --git a/README.md b/README.md index f7733d5..f81d261 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,90 @@ # Eagle -## Disclaimer +## Stability -Eagle is still in development and not currently usable. The current state is barely a proof of concept. +Eagle is still in early development. Performance is not ideal and the interface is likely to change over time. However, +it is in a usable state currently. ## What is Eagle? -Eagle is a library designed to make "full-stack" applications with Rust. It allows you to define a communication protocol -based on simple "questions" and "answers" which can be implemented as simple functions. From the perspective of the client -(which sends "questions") the protocol is simply a set of async functions on a struct. From the perspective of the server -(which sends "answers") the protocol is a trait which it implements on any struct of its choice. +Eagle is a library which allows you to easily build an [RPC](https://en.wikipedia.org/wiki/Remote_procedure_call) protocol. +It uses a macro to generate the required communication code and makes adding new functions easy and quick. Eagle is designed to work specifically with `tokio` and uses `serde` for formatting data. ## Using Eagle -The way that `eagle` is designed to be used is inside a shared dependency between your "server" and your "client". Both of these should be in a workspace. Create a `shared` crate which both components should depend on. Inside this crate, you can -define your protocol as an enum: +The way that `eagle` is designed to be used is inside a shared dependency between your "server" and your "client". Both of these should be in a workspace. Create a `shared` crate which both components should depend on, this crate should have `eagle` as a dependency. By default `eagle` uses TCP for communication, but you may disable default features and enable the `unix` feature on `eagle` to use unix sockets instead. + +Inside this crate, you can define your protocol as an enum: ```rs use eagle::Protocol; +use serde::{Serialize, Deserliaze}; + +#[derive(Clone, Serialize, Deserialize)] +pub struct ExampleStruct { + a: i32, + b: i32 +} #[derive(Protocol)] -pub enum TestProtocol { +pub enum Example { Addition((i32, i32), i32), - SomeKindOfQuestion(String, i32) + StructuredDataAlsoWorks(ExampleStruct, ()), + SetState(i32, i32), + GetState((), i32) } ``` -In your server, you will be able to implement this protocol for any struct (and in the future register it for communication): +Each variant describes one of the functions that the client can call, the first field on a variant represents the arguments that the client can send and the second field represents the return value. In the example above, the `addition` function would take in two `i32`s and return another `i32`. Any data passed this way must implement `Clone` as well as `serde::Serialize` and `serde::Deserialize`. + +Once your protocol is defined, you can implement it on your server. To do so, you must first implement a handler for your +protocol. A handler must implement `Clone` as well as the `ServerHandler` trait for your protocol. For the above example: ```rs -use shared::TestProtocolServer; +use shared::ExampleServerHandler; -pub struct Server; -impl TestProtocolServer for Server { - fn addition(&mut self, a: i32, b: i32) -> i32 { +struct ExampleHandler { + state: i32 +} +impl ExampleServerHandler for ExampleHandler { + async fn addition(&mut self, a: i32, b: i32) -> i32 { a + b } - fn some_kind_of_question(&mut self, question: String) -> i32 { - 42 + async fn get_state(&mut self) -> i32 { + self.state } + + async fn set_state(&mut self, state: i32) -> i32 { + self.state = state; + self.state + } + + /* ... */ } ``` -In your client, you can use an instance of the client struct to query the server: +Your handler can now be used by the server. You can easily bind your server to a socket with: ```rs -use shared::TestProtocolClient; +use shared::ExampleServer; -#[tokio::main] -async fn main() { - let client = TestProtocolClient::new(); - assert_eq!(client.addition(2, 2).await, 4); -} +let handler = ExampleHandler { state: 0 }; +let server_task = tokio::spawn(ExampleServer::bind(handler, "127.0.0.1:1234")); +// Or, if you're using the 'unix' feature... +let server_task = tokio::spawn(ExampleServer::bind(handler, "/tmp/sock")); + +``` + +Note that bind is an asynchronous function which should never return, you must put it in a separate task. Once bound, the server will await for connections and start responding to queries. + +On the client, all you need to do is to use your protocol's `Client` to connect and you can start making requests. + +```rs +use shared::ExampleClient; + +let client = ExampleClient::connect("127.0.0.1:1234").await.unwrap(); +assert_eq!(client.addition(5, 2), 7); ``` ## License diff --git a/src/lib.rs b/src/lib.rs index 22dbe55..f0a8fb8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,7 +77,7 @@ fn derive_protocol(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream 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!("{}ServerTrait", 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); diff --git a/tests/full.rs b/tests/full.rs index 7c33a4f..c5624ce 100644 --- a/tests/full.rs +++ b/tests/full.rs @@ -40,7 +40,7 @@ enum TestProtocol { #[derive(Clone)] struct TrivialServer; -impl TestProtocolServerTrait for TrivialServer { +impl TestProtocolServerHandler for TrivialServer { async fn addition(&mut self, a: i32, b: i32) -> i32 { a + b } @@ -74,7 +74,7 @@ async fn e2e() { #[cfg(feature = "tcp")] let address = format!("127.0.0.1:{}", 10000 + rand::random::() % 1000); let server_task = tokio::spawn(TestProtocolServer::bind(TrivialServer, address.clone())); - // Wait for the server to start + // Wait for the server to start, the developer is responsible for this in production tokio::time::sleep(std::time::Duration::from_millis(10)).await; let client = TestProtocolClient::connect(address).await.unwrap(); assert_eq!(client.addition(2, 5).await.unwrap(), 7);