1 unstable release
0.0.1 | Mar 10, 2024 |
---|
#55 in #json-rpc-server
27KB
544 lines
rstrpc - Rust/TypeScript RPC
Generate type-safe TypeScript clients for your Rust APIs, with Serde compatibility, subscriptions, and more!
Features:
-
Context based middleware
-
Subscriptions
-
Nested routers
-
Serde compatibility for serialising and deserialising parameters and return values
-
Standardised JSONRPC 2.0 implementation, including
ws
andhttp
transport layers
Example
Check out the example
directory for a full example.
Rust Server
#[derive(Clone, Default)]
pub struct Ctx {
count: Arc<AtomicUsize>,
}
// Handlers are defined as functions, where the function name will be the name of the handler
#[handler]
async fn hello_world(_ctx: Ctx) -> String {
"Hello, world!".to_string()
}
// Handlers have access to the app state
#[handler]
async fn count(ctx: Ctx) -> usize {
ctx.count.fetch_add(1, Ordering::Relaxed)
}
// Handlers can accept parameters
#[handler]
async fn count_by(ctx: Ctx, amount: usize) -> usize {
ctx.count.fetch_add(amount, Ordering::Relaxed)
}
// Handlers can return a stream, in order to act as a subscription
#[handler(subscription)]
async fn countdown(ctx: AppCtx, min: usize, max: usize) -> impl Stream<Item = usize> {
stream::iter(min..=max).then(|n| async move {
n
})
}
#[tokio::main]
async fn main() {
// Build the app, attaching handlers as required
let app = Router::new()
.handler(hello_world)
.handler(count);
// Create a stop channel so that the server can be programatically terminated
let (stop_handle, server_handle) = stop_channel();
// Global app context
let ctx = Ctx::default();
// Create or nest app into an existing axum server
let router = axum::Router::<()>::new()
.route("/", get(|| async { "another endpoint!" }))
.nest_service("/rpc", app.to_service(move |_| {
// For each request that comes in, clone the context so that it can be shared around
ctx.clone()
}, stop_handle));
// Start the axum rounter as normal
hyper::Server::bind(&SocketAddr::from([127, 0, 0, 1], 9944))
.serve(router.into_make_service())
.await
.unwrap();
// Upon termination of the hyper server, properly shutdown the RPC server
server_handle.stop().unwrap();
}
TypeScript
import { ws } from "@rstrpc/client";
// This type is automatically generated based on the Rust API
import type { Server } from "./bindings.ts";
// Start a new client, passing the type as a generic parameter
const client = ws<Server>("ws://localhost:9944/rpc");
// Handlers can be accessed from the client just by calling the method!
const message = await client.hello_world();
console.log(message); // "Hello, world!"
for (let i = 0; i < 5; i++) {
const count = await client.count();
console.log(`The count is: ${count}`);
}
// Parameters are typed, and are passed as if it were a regular function
await client.count_by(10);
// Subscriptions are just like regular handlers, except they also accept life-cycle handlers for
// data, errors, and subcription end
await client.countdown(1, 4).subscribe({
on_data: (data) => {
console.log(`Countdown: ${data}`);
},
on_end: () => {
console.log("Countdown done!");
}
});
Acknowledgements
-
rspc
: Similar concept, however uses a bespoke solution for generating TypeScript types from Rust structs, which isn't completely compatible with all of Serde's features for serialising and deserialising structs. -
trpc
: Needs no introduction, however it being restricted to TypeScript backends makes it relatively useless for Rust developers.
Dependencies
~12–23MB
~330K SLoC