Skip to content

Commit 307f265

Browse files
authored
feat: ipc and axum serving example (#33)
* feat: ipc serving example * feat: axum w/ websockets * chore: bump version
1 parent fc74ea4 commit 307f265

File tree

7 files changed

+198
-27
lines changed

7 files changed

+198
-27
lines changed

Cargo.toml

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description = "Simple, modern, ergonomic JSON-RPC 2.0 router built with tower an
55
keywords = ["json-rpc", "jsonrpc", "json"]
66
categories = ["web-programming::http-server", "web-programming::websocket"]
77

8-
version = "0.4.0"
8+
version = "0.4.1"
99
edition = "2021"
1010
rust-version = "1.81"
1111
authors = ["init4", "James Prestwich"]
@@ -73,11 +73,20 @@ inherits = "release"
7373
debug = 2
7474
strip = false
7575

76-
[profile.bench]
77-
inherits = "profiling"
78-
7976
[profile.ci-rust]
8077
inherits = "dev"
8178
strip = true
8279
debug = false
8380
incremental = false
81+
82+
[[example]]
83+
name = "ipc"
84+
required-features = ["ipc"]
85+
86+
[[example]]
87+
name = "axum"
88+
required-features = ["axum", "pubsub"]
89+
90+
[[example]]
91+
name = "cors"
92+
required-features = ["axum"]

examples/axum.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
use ajj::HandlerCtx;
2+
use std::net::SocketAddr;
3+
4+
#[tokio::main]
5+
async fn main() -> eyre::Result<()> {
6+
let router = make_router();
7+
8+
// Serve via `POST` on `/` and Websockets on `/ws`
9+
let axum = router.clone().into_axum_with_ws("/", "/ws");
10+
11+
// Now we can serve the router on a TCP listener
12+
let addr = SocketAddr::from(([127, 0, 0, 1], 0));
13+
let listener = tokio::net::TcpListener::bind(addr).await?;
14+
15+
println!("Listening for POST on {}/", listener.local_addr()?);
16+
println!("Listening for WS on {}/ws", listener.local_addr()?);
17+
18+
println!("use Ctrl-C to stop");
19+
axum::serve(listener, axum).await.map_err(Into::into)
20+
}
21+
22+
fn make_router() -> ajj::Router<()> {
23+
ajj::Router::<()>::new()
24+
.route("helloWorld", || async {
25+
tracing::info!("serving hello world");
26+
Ok::<_, ()>("Hello, world!")
27+
})
28+
.route("addNumbers", |(a, b): (u32, u32)| async move {
29+
tracing::info!("serving addNumbers");
30+
Ok::<_, ()>(a + b)
31+
})
32+
.route("notify", |ctx: HandlerCtx| async move {
33+
// Check if notifications are enabled for the connection.
34+
if !ctx.notifications_enabled() {
35+
// This error will appear in the ResponsePayload's `data` field.
36+
return Err("notifications are disabled");
37+
}
38+
39+
let req_id = 15u8;
40+
41+
// Spawn a task to send the notification after a short delay.
42+
ctx.spawn_with_ctx(|ctx| async move {
43+
// something expensive goes here
44+
let result = 100_000_000;
45+
let _ = ctx
46+
.notify(&serde_json::json!({
47+
"req_id": req_id,
48+
"result": result,
49+
}))
50+
.await;
51+
});
52+
53+
// Return the request ID immediately.
54+
Ok(req_id)
55+
})
56+
}

examples/cors.rs

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! Basic example of using ajj with CORS.
1+
//! Basic example of using ajj with CORS via axum.
22
//!
33
//! This example demonstrates how to set up a simple HTTP server using `axum`
44
//! and `tower_http` for CORS support.
@@ -14,9 +14,32 @@
1414
1515
use axum::http::{HeaderValue, Method};
1616
use eyre::{ensure, Context};
17-
use std::{future::IntoFuture, net::SocketAddr};
17+
use std::net::SocketAddr;
1818
use tower_http::cors::{AllowOrigin, Any, CorsLayer};
1919

20+
#[tokio::main]
21+
async fn main() -> eyre::Result<()> {
22+
let cors = std::env::args().nth(1).unwrap_or("*".to_string());
23+
24+
let router = make_router()
25+
// Convert to an axum router
26+
.into_axum("/")
27+
// And then layer on your CORS settings
28+
.layer(make_cors(&cors)?);
29+
30+
// Now we can serve the router on a TCP listener
31+
let addr = SocketAddr::from(([127, 0, 0, 1], 0));
32+
let listener = tokio::net::TcpListener::bind(addr).await?;
33+
34+
println!("Listening on {}", listener.local_addr()?);
35+
println!("CORS allowed for: {}", cors);
36+
if cors == "*" {
37+
println!("(specify cors domains as a comma-separated list to restrict origins)");
38+
}
39+
println!("use Ctrl-C to stop");
40+
axum::serve(listener, router).await.map_err(Into::into)
41+
}
42+
2043
fn get_allowed(cors: &str) -> eyre::Result<AllowOrigin> {
2144
// Wildcard `*` means any origin is allowed.
2245
if cors == "*" {
@@ -53,12 +76,10 @@ fn make_cors(cors: &str) -> eyre::Result<CorsLayer> {
5376
.allow_headers(Any))
5477
}
5578

56-
#[tokio::main]
57-
async fn main() -> eyre::Result<()> {
58-
let cors = std::env::args().nth(1).unwrap_or("*".to_string());
59-
79+
// Setting up an AJJ router is easy and fun!
80+
fn make_router() -> ajj::Router<()> {
6081
// Setting up an AJJ router is easy and fun!
61-
let router = ajj::Router::<()>::new()
82+
ajj::Router::<()>::new()
6283
.route("helloWorld", || async {
6384
tracing::info!("serving hello world");
6485
Ok::<_, ()>("Hello, world!")
@@ -67,15 +88,4 @@ async fn main() -> eyre::Result<()> {
6788
tracing::info!("serving addNumbers");
6889
Ok::<_, ()>(a + b)
6990
})
70-
// Convert to an axum router
71-
.into_axum("/")
72-
// And then layer on your CORS settings
73-
.layer(make_cors(&cors)?);
74-
75-
// Now we can serve the router on a TCP listener
76-
let addr = SocketAddr::from(([127, 0, 0, 1], 0));
77-
let listener = tokio::net::TcpListener::bind(addr).await?;
78-
79-
axum::serve(listener, router).into_future().await?;
80-
Ok(())
8191
}

examples/ipc.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//! Basic example of running a JSON-RPC server over IPC using ajj
2+
//!
3+
//! This example demonstrates how to set up a simple IPC server using `ajj`, and
4+
//! defines a few basic RPC methods.
5+
//!
6+
//! The `make_router` function sets up a router with three routes:
7+
//! - `helloWorld`: Returns a greeting string.
8+
//! - `addNumbers`: Takes two numbers as parameters and returns their sum.
9+
//! - `notify`: Sends a notification after a short delay, demonstrating the use
10+
//! of notifications in the RPC context.
11+
12+
use ajj::{
13+
pubsub::{
14+
ipc::{to_name, ListenerOptions},
15+
Connect,
16+
},
17+
HandlerCtx, Router,
18+
};
19+
use tempfile::NamedTempFile;
20+
21+
#[tokio::main]
22+
async fn main() -> eyre::Result<()> {
23+
let router = make_router();
24+
25+
// Create a temporary file for the IPC socket.
26+
let tempfile = NamedTempFile::new()?;
27+
let name = to_name(tempfile.path().as_os_str()).expect("invalid name");
28+
29+
println!("Serving IPC on socket: {:?}", tempfile.path());
30+
println!("use Ctrl-C to stop");
31+
32+
// The guard keeps the server running until dropped.
33+
let guard = ListenerOptions::new().name(name).serve(router).await?;
34+
35+
// Shut down on Ctrl-C
36+
tokio::signal::ctrl_c().await?;
37+
drop(guard);
38+
39+
Ok(())
40+
}
41+
42+
// Setting up an AJJ router is easy and fun!
43+
fn make_router() -> Router<()> {
44+
Router::<()>::new()
45+
.route("helloWorld", || async {
46+
tracing::info!("serving hello world");
47+
Ok::<_, ()>("Hello, world!")
48+
})
49+
.route("addNumbers", |(a, b): (u32, u32)| async move {
50+
tracing::info!("serving addNumbers");
51+
Ok::<_, ()>(a + b)
52+
})
53+
.route("notify", |ctx: HandlerCtx| async move {
54+
// Check if notifications are enabled for the connection.
55+
if !ctx.notifications_enabled() {
56+
// This error will appear in the ResponsePayload's `data` field.
57+
return Err("notifications are disabled");
58+
}
59+
60+
let req_id = 15u8;
61+
62+
// Spawn a task to send the notification after a short delay.
63+
ctx.spawn_with_ctx(|ctx| async move {
64+
// something expensive goes here
65+
let result = 100_000_000;
66+
let _ = ctx
67+
.notify(&serde_json::json!({
68+
"req_id": req_id,
69+
"result": result,
70+
}))
71+
.await;
72+
});
73+
74+
// Return the request ID immediately.
75+
Ok(req_id)
76+
})
77+
}

src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@
3131
//!
3232
//! let req_id = 15u8;
3333
//!
34-
//! tokio::task::spawn_blocking(move || {
34+
//! ctx.spawn_with_ctx(|ctx| async move {
3535
//! // something expensive goes here
3636
//! let result = 100_000_000;
3737
//! let _ = ctx.notify(&serde_json::json!({
3838
//! "req_id": req_id,
3939
//! "result": result,
40-
//! }));
40+
//! })).await;
4141
//! });
4242
//! Ok(req_id)
4343
//! })
@@ -148,7 +148,7 @@
148148
)]
149149
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
150150
#![deny(unused_must_use, rust_2018_idioms)]
151-
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
151+
#![cfg_attr(docsrs, feature(doc_cfg))]
152152

153153
#[macro_use]
154154
pub(crate) mod macros;
File renamed without changes.

src/pubsub/mod.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,29 @@
8989
//! [`HandlerCtx`]: crate::HandlerCtx
9090
9191
#[cfg(feature = "ipc")]
92-
mod ipc;
92+
mod ipc_inner;
9393
#[cfg(feature = "ipc")]
9494
#[doc(hidden)]
95-
pub use ipc::ReadJsonStream;
95+
// Re-exported for use in tests
96+
pub use ipc_inner::ReadJsonStream;
97+
98+
/// IPC support via interprocess local sockets.
99+
#[cfg(feature = "ipc")]
100+
pub mod ipc {
101+
use std::ffi::OsStr;
102+
103+
pub use interprocess::local_socket::{self as local_socket, Listener, ListenerOptions, Name};
104+
105+
/// Convenience function to convert an [`OsStr`] to a local socket [`Name`]
106+
/// in a platform-safe way.
107+
pub fn to_name(path: &OsStr) -> std::io::Result<local_socket::Name<'_>> {
108+
if cfg!(windows) && !path.as_encoded_bytes().starts_with(br"\\.\pipe\") {
109+
local_socket::ToNsName::to_ns_name::<local_socket::GenericNamespaced>(path)
110+
} else {
111+
local_socket::ToFsName::to_fs_name::<local_socket::GenericFilePath>(path)
112+
}
113+
}
114+
}
96115

97116
mod shared;
98117
pub(crate) use shared::WriteItem;

0 commit comments

Comments
 (0)