9 Commits

Author SHA1 Message Date
8313209e0e Bump for testing
All checks were successful
Build library & run tests / build (unix) (push) Successful in 1m7s
Build library & run tests / build (tcp) (push) Successful in 1m10s
Build library & run tests / docs (push) Successful in 22s
Publish library / publish (push) Successful in 32s
2024-06-24 21:20:40 +02:00
91deddc1d2 Add publishing to gitea releases for the release workflow
All checks were successful
Build library & run tests / build (unix) (push) Successful in 34s
Build library & run tests / build (tcp) (push) Successful in 35s
Build library & run tests / docs (push) Successful in 35s
2024-06-24 21:20:02 +02:00
0d018d0869 Attempt to add publish workflow
All checks were successful
Build library & run tests / build (unix) (push) Successful in 34s
Build library & run tests / build (tcp) (push) Successful in 34s
Build library & run tests / docs (push) Successful in 35s
2024-06-24 21:12:14 +02:00
71adf67727 Add description and repository to manifest
All checks were successful
Build library & run tests / build (unix) (push) Successful in 57s
Build library & run tests / docs (push) Successful in 58s
Build library & run tests / build (tcp) (push) Successful in 1m1s
2024-06-24 21:04:36 +02:00
b12b382d94 Try fixing doc upload action
All checks were successful
Build library & run tests / build (unix) (push) Successful in 17s
Build library & run tests / build (tcp) (push) Successful in 19s
Build library & run tests / docs (push) Successful in 22s
2024-06-24 18:35:02 +02:00
812861640d Significantly update the documentation
Some checks failed
Build library & run tests / docs (push) Failing after 1m0s
Build library & run tests / build (unix) (push) Successful in 1m6s
Build library & run tests / build (tcp) (push) Successful in 1m7s
2024-06-24 18:26:19 +02:00
bfd4c1346f Update README.md
All checks were successful
Build library & run tests / build (unix) (push) Successful in 28s
Build library & run tests / build (tcp) (push) Successful in 30s
2024-06-24 16:58:14 +02:00
bf183a0598 Fix crash in server due to overeager parsing
All checks were successful
Build library & run tests / build (unix) (push) Successful in 29s
Build library & run tests / build (tcp) (push) Successful in 29s
2024-06-24 15:58:54 +02:00
fc570fa0bd Fix compile error on unix
Some checks failed
Build library & run tests / build (unix) (push) Failing after 27s
Build library & run tests / build (tcp) (push) Successful in 29s
2024-06-24 15:38:55 +02:00
9 changed files with 354 additions and 55 deletions

View File

@@ -16,3 +16,15 @@ jobs:
run: nix build .#clippy_${{ matrix.feature }}
- name: Build & test
run: nix build .#${{ matrix.feature }}
docs:
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Build docs
run: nix build .#doc.doc
- name: (Temporary) Upload docs
uses: actions/upload-artifact@v3
with:
name: docs
path: ./result-doc

View File

@@ -0,0 +1,23 @@
name: Publish library
on:
push:
branches:
- master
tags:
- v*
workflow_dispatch:
jobs:
publish:
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Publish to Gitea Cargo registry
run: nix develop -c cargo publish --dry-run --token ${{ secrets.GITEA_TOKEN }} --index sparse+https://git.colon-three.com/api/packages/kodi/cargo/
- name: Publish to crates.io
run: nix develop -c cargo publish --dry-run --token ${{ secrets.CRATESIO_TOKEN }}
- name: Publish to Gitea Releases
uses: akkuman/gitea-release-action@v1
with:
draft: true

55
Cargo.lock generated
View File

@@ -75,6 +75,28 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "async-stream"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
dependencies = [
"async-stream-impl",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-stream-impl"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "backtrace"
version = "0.3.73"
@@ -131,7 +153,7 @@ checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
[[package]]
name = "eagle"
version = "0.2.0"
version = "0.2.1"
dependencies = [
"env_logger",
"log",
@@ -142,6 +164,7 @@ dependencies = [
"serde",
"syn",
"tokio",
"tokio-test",
]
[[package]]
@@ -167,6 +190,12 @@ dependencies = [
"log",
]
[[package]]
name = "futures-core"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]]
name = "getrandom"
version = "0.2.15"
@@ -435,6 +464,30 @@ dependencies = [
"syn",
]
[[package]]
name = "tokio-stream"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
]
[[package]]
name = "tokio-test"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7"
dependencies = [
"async-stream",
"bytes",
"futures-core",
"tokio",
"tokio-stream",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"

View File

@@ -1,9 +1,11 @@
[package]
name = "eagle"
version = "0.2.0"
edition = "2021"
version = "0.2.1"
description = "A simple library for creating RPC protocols."
repository = "https://git.colon-three.com/kodi/eagle"
authors = ["KodiCraft <kodi@kdcf.me>"]
license = "AGPL-3.0"
publish = ["gitea"]
edition = "2021"
resolver = "2"
[features]
@@ -27,6 +29,7 @@ log = { version = "0.4.21", optional = true }
tokio = { version = "1.38.0", features = ["sync", "rt-multi-thread", "macros", "time", "io-util", "net"] }
env_logger = "0.11.3"
log = "0.4.21"
tokio-test = "0.4.4"
[lib]
proc-macro = true

View File

@@ -1,58 +1,87 @@
# 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, the interface is likely to change and the documentation is not final. Basic functionality is fully implemented and works as expected.
## 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`](https://crates.io/crates/tokio) and uses [`serde`](https://crates.io/crates/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.
```rs
Inside this crate, you can define your protocol as an enum:
```rust
use eagle::Protocol;
use serde::{Serialize, Deserialize};
#[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`].
```rs
use shared::TestProtocolServer;
The [`Protocol`] macro will create a number of exports in your shared crate. You will be able to import them by name in your client and server.
pub struct Server;
impl TestProtocolServer for Server {
fn addition(&mut self, a: i32, b: i32) -> i32 {
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:
```rust
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;
```rust
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.
```rust
use shared::ExampleClient;
let client = ExampleClient::connect("127.0.0.1:1234").await.unwrap();
assert_eq!(client.addition(5, 2), 7);
```
## License

View File

@@ -20,6 +20,12 @@
doCheck = true;
mode = "test";
};
doc = naersk-lib.buildPackage {
src = ./.;
doDoc = true;
mode = "test";
cargoDocOptions = x: x ++ ["--no-deps"];
};
unix = naersk-lib.buildPackage {
src = ./.;
doCheck = true;

View File

@@ -1,5 +1,5 @@
/*
Eagle - A library for easy communication in full-stack Rust applications
Eagle - A simple library for RPC in Rust
Copyright (c) 2024 KodiCraft
This program is free software: you can redistribute it and/or modify
@@ -15,6 +15,139 @@ 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/>.
*/
//! # Eagle - A simple library for RPC in Rust
//!
//! <div class="warning">Eagle is still in early development. This documentation is subject to change and may not be entirely accurate.</div>
//!
//! 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};
@@ -26,6 +159,33 @@ 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());
@@ -77,7 +237,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);
@@ -282,7 +442,7 @@ fn derive_protocol(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream
#listener_statement
loop {
let (stream, _) = listener.accept().await?;
#info("Accepted connection from {}", stream.peer_addr()?);
#info("Accepted connection from {:?}", stream.peer_addr()?);
let self_clone = self.clone();
let run_task = tokio::spawn(async move {
self_clone.run(stream).await;
@@ -320,7 +480,7 @@ fn derive_protocol(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream
Ok(n) => {
#debug("Received {} bytes (server)", n);
buf.extend_from_slice(&read_buf[..n]);
loop {
while buf.len() >= 4 {
let len = u32::from_le_bytes(buf[..4].try_into().expect("Failed to convert bytes to u32"));
if buf.len() >= (4 + len as usize) {
let serialized = std::str::from_utf8(&buf[4..(4 + len as usize)]).expect("Failed to convert bytes to string");

View File

@@ -1,5 +1,5 @@
/*
Eagle - A library for easy communication in full-stack Rust applications
Eagle - A simple library for RPC in Rust
Copyright (c) 2024 KodiCraft
This program is free software: you can redistribute it and/or modify

View File

@@ -1,5 +1,5 @@
/*
Eagle - A library for easy communication in full-stack Rust applications
Eagle - A simple library for RPC in Rust
Copyright (c) 2024 KodiCraft
This program is free software: you can redistribute it and/or modify
@@ -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
}
@@ -53,29 +53,42 @@ impl TestProtocolServerTrait for TrivialServer {
async fn void(&mut self) {}
}
struct Cleanup {
address: String,
}
impl Drop for Cleanup {
fn drop(&mut self) {
std::fs::remove_file(&self.address).unwrap();
}
}
#[tokio::test]
async fn e2e() {
init_logger();
#[cfg(feature = "unix")]
let address = "/tmp/eagle-test.sock";
let address = format!("/tmp/eagle-test-{}.sock", rand::random::<u64>());
#[cfg(feature = "unix")]
let _cleanup = Cleanup {
address: address.clone(),
};
#[cfg(feature = "tcp")]
let address = format!("127.0.0.1:{}", 10000 + rand::random::<u64>() % 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();
let res = client.addition(2, 5).await.unwrap();
// assert_eq!(client.addition(2, 5).await.unwrap(), 7);
// assert_eq!(
// client.some_kind_of_question("Hello, world!".to_string())
// .await
// .unwrap(),
// "Hello, world!".len() as i32
// );
// assert_eq!(
// client.this_responds_with_a_string(42).await.unwrap(),
// "The number is 42"
// );
// client.void().await.unwrap();
// server_task.abort();
assert_eq!(client.addition(2, 5).await.unwrap(), 7);
assert_eq!(
client
.some_kind_of_question("Hello, world!".to_string())
.await
.unwrap(),
"Hello, world!".len() as i32
);
assert_eq!(
client.this_responds_with_a_string(42).await.unwrap(),
"The number is 42"
);
client.void().await.unwrap();
server_task.abort();
}