From 250d07abebf7314131b633c7f2926bc1ebd9836a Mon Sep 17 00:00:00 2001 From: James Date: Fri, 10 Oct 2025 10:35:09 -0400 Subject: [PATCH 1/3] feat: ipc serving example --- Cargo.toml | 5 ++ examples/cors.rs | 47 ++++++++++------- examples/ipc.rs | 78 +++++++++++++++++++++++++++++ src/lib.rs | 4 +- src/pubsub/{ipc.rs => ipc_inner.rs} | 0 src/pubsub/mod.rs | 11 +++- 6 files changed, 124 insertions(+), 21 deletions(-) create mode 100644 examples/ipc.rs rename src/pubsub/{ipc.rs => ipc_inner.rs} (100%) diff --git a/Cargo.toml b/Cargo.toml index 36f6fc8..1514e85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,3 +81,8 @@ inherits = "dev" strip = true debug = false incremental = false + + +[[example]] +name = "ipc" +required-features = ["ipc"] \ No newline at end of file diff --git a/examples/cors.rs b/examples/cors.rs index a55d6a5..9309222 100644 --- a/examples/cors.rs +++ b/examples/cors.rs @@ -1,4 +1,4 @@ -//! Basic example of using ajj with CORS. +//! Basic example of using ajj with CORS via axum. //! //! This example demonstrates how to set up a simple HTTP server using `axum` //! and `tower_http` for CORS support. @@ -17,6 +17,32 @@ use eyre::{ensure, Context}; use std::{future::IntoFuture, net::SocketAddr}; use tower_http::cors::{AllowOrigin, Any, CorsLayer}; +#[tokio::main] +async fn main() -> eyre::Result<()> { + let cors = std::env::args().nth(1).unwrap_or("*".to_string()); + + let router = make_router() + // Convert to an axum router + .into_axum("/") + // And then layer on your CORS settings + .layer(make_cors(&cors)?); + + // Now we can serve the router on a TCP listener + let addr = SocketAddr::from(([127, 0, 0, 1], 0)); + let listener = tokio::net::TcpListener::bind(addr).await?; + + println!("Listening on {}", listener.local_addr()?); + println!("CORS allowed for: {}", cors); + if cors == "*" { + println!("(specify cors domains as a comma-separated list to restrict origins)"); + } + println!("use Ctrl-C to stop"); + axum::serve(listener, router) + .into_future() + .await + .map_err(Into::into) +} + fn get_allowed(cors: &str) -> eyre::Result { // Wildcard `*` means any origin is allowed. if cors == "*" { @@ -53,12 +79,10 @@ fn make_cors(cors: &str) -> eyre::Result { .allow_headers(Any)) } -#[tokio::main] -async fn main() -> eyre::Result<()> { - let cors = std::env::args().nth(1).unwrap_or("*".to_string()); - +// Setting up an AJJ router is easy and fun! +fn make_router() -> ajj::Router<()> { // Setting up an AJJ router is easy and fun! - let router = ajj::Router::<()>::new() + ajj::Router::<()>::new() .route("helloWorld", || async { tracing::info!("serving hello world"); Ok::<_, ()>("Hello, world!") @@ -67,15 +91,4 @@ async fn main() -> eyre::Result<()> { tracing::info!("serving addNumbers"); Ok::<_, ()>(a + b) }) - // Convert to an axum router - .into_axum("/") - // And then layer on your CORS settings - .layer(make_cors(&cors)?); - - // Now we can serve the router on a TCP listener - let addr = SocketAddr::from(([127, 0, 0, 1], 0)); - let listener = tokio::net::TcpListener::bind(addr).await?; - - axum::serve(listener, router).into_future().await?; - Ok(()) } diff --git a/examples/ipc.rs b/examples/ipc.rs new file mode 100644 index 0000000..e2e86d0 --- /dev/null +++ b/examples/ipc.rs @@ -0,0 +1,78 @@ +//! Basic example of running a JSON-RPC server over IPC using ajj +//! +//! This example demonstrates how to set up a simple IPC server using `ajj`. + +use ajj::{ + pubsub::{ipc::local_socket, Connect}, + HandlerCtx, Router, +}; +use tempfile::NamedTempFile; + +#[tokio::main] +async fn main() -> eyre::Result<()> { + let router = make_router(); + + // Create a temporary file for the IPC socket. + let tempfile = NamedTempFile::new()?; + let name = to_name(tempfile.path().as_os_str()).expect("invalid name"); + + println!("Serving IPC on socket: {:?}", tempfile.path()); + println!("use Ctrl-C to stop"); + + // The guard keeps the server running until dropped. + let guard = ajj::pubsub::ipc::ListenerOptions::new() + .name(name) + .serve(router) + .await?; + + // Shut down on Ctrl-C + tokio::signal::ctrl_c().await?; + drop(guard); + + Ok(()) +} + +fn to_name(path: &std::ffi::OsStr) -> std::io::Result> { + if cfg!(windows) && !path.as_encoded_bytes().starts_with(br"\\.\pipe\") { + local_socket::ToNsName::to_ns_name::(path) + } else { + local_socket::ToFsName::to_fs_name::(path) + } +} + +// Setting up an AJJ router is easy and fun! +fn make_router() -> Router<()> { + Router::<()>::new() + .route("helloWorld", || async { + tracing::info!("serving hello world"); + Ok::<_, ()>("Hello, world!") + }) + .route("addNumbers", |(a, b): (u32, u32)| async move { + tracing::info!("serving addNumbers"); + Ok::<_, ()>(a + b) + }) + .route("notify", |ctx: HandlerCtx| async move { + // Check if notifications are enabled for the connection. + if !ctx.notifications_enabled() { + // This error will appear in the ResponsePayload's `data` field. + return Err("notifications are disabled"); + } + + let req_id = 15u8; + + // Spawn a task to send the notification after a short delay. + ctx.spawn_with_ctx(|ctx| async move { + // something expensive goes here + let result = 100_000_000; + let _ = ctx + .notify(&serde_json::json!({ + "req_id": req_id, + "result": result, + })) + .await; + }); + + // Return the request ID immediately. + Ok(req_id) + }) +} diff --git a/src/lib.rs b/src/lib.rs index 37c297e..1a81c85 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,13 +31,13 @@ //! //! let req_id = 15u8; //! -//! tokio::task::spawn_blocking(move || { +//! ctx.spawn_with_ctx(|ctx| async move { //! // something expensive goes here //! let result = 100_000_000; //! let _ = ctx.notify(&serde_json::json!({ //! "req_id": req_id, //! "result": result, -//! })); +//! })).await; //! }); //! Ok(req_id) //! }) diff --git a/src/pubsub/ipc.rs b/src/pubsub/ipc_inner.rs similarity index 100% rename from src/pubsub/ipc.rs rename to src/pubsub/ipc_inner.rs diff --git a/src/pubsub/mod.rs b/src/pubsub/mod.rs index 51aa896..ab8a164 100644 --- a/src/pubsub/mod.rs +++ b/src/pubsub/mod.rs @@ -89,10 +89,17 @@ //! [`HandlerCtx`]: crate::HandlerCtx #[cfg(feature = "ipc")] -mod ipc; +mod ipc_inner; #[cfg(feature = "ipc")] #[doc(hidden)] -pub use ipc::ReadJsonStream; +// Re-exported for use in tests +pub use ipc_inner::ReadJsonStream; + +/// IPC support via interprocess local sockets. +#[cfg(feature = "ipc")] +pub mod ipc { + pub use interprocess::local_socket::{self as local_socket, Listener, ListenerOptions, Name}; +} mod shared; pub(crate) use shared::WriteItem; From 0ec4241081bfef733d7344a4410475099aca4ac6 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 10 Oct 2025 10:45:55 -0400 Subject: [PATCH 2/3] feat: axum w/ websockets --- Cargo.toml | 14 +++++++----- examples/axum.rs | 56 +++++++++++++++++++++++++++++++++++++++++++++++ examples/cors.rs | 7 ++---- examples/ipc.rs | 27 +++++++++++------------ src/pubsub/mod.rs | 12 ++++++++++ 5 files changed, 92 insertions(+), 24 deletions(-) create mode 100644 examples/axum.rs diff --git a/Cargo.toml b/Cargo.toml index 1514e85..25ee873 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,16 +73,20 @@ inherits = "release" debug = 2 strip = false -[profile.bench] -inherits = "profiling" - [profile.ci-rust] inherits = "dev" strip = true debug = false incremental = false - [[example]] name = "ipc" -required-features = ["ipc"] \ No newline at end of file +required-features = ["ipc"] + +[[example]] +name = "axum" +required-features = ["axum", "pubsub"] + +[[example]] +name = "cors" +required-features = ["axum"] \ No newline at end of file diff --git a/examples/axum.rs b/examples/axum.rs new file mode 100644 index 0000000..3a17afd --- /dev/null +++ b/examples/axum.rs @@ -0,0 +1,56 @@ +use ajj::HandlerCtx; +use std::net::SocketAddr; + +#[tokio::main] +async fn main() -> eyre::Result<()> { + let router = make_router(); + + // Serve via `POST` on `/` and Websockets on `/ws` + let axum = router.clone().into_axum_with_ws("/", "/ws"); + + // Now we can serve the router on a TCP listener + let addr = SocketAddr::from(([127, 0, 0, 1], 0)); + let listener = tokio::net::TcpListener::bind(addr).await?; + + println!("Listening for POST on {}/", listener.local_addr()?); + println!("Listening for WS on {}/ws", listener.local_addr()?); + + println!("use Ctrl-C to stop"); + axum::serve(listener, axum).await.map_err(Into::into) +} + +fn make_router() -> ajj::Router<()> { + ajj::Router::<()>::new() + .route("helloWorld", || async { + tracing::info!("serving hello world"); + Ok::<_, ()>("Hello, world!") + }) + .route("addNumbers", |(a, b): (u32, u32)| async move { + tracing::info!("serving addNumbers"); + Ok::<_, ()>(a + b) + }) + .route("notify", |ctx: HandlerCtx| async move { + // Check if notifications are enabled for the connection. + if !ctx.notifications_enabled() { + // This error will appear in the ResponsePayload's `data` field. + return Err("notifications are disabled"); + } + + let req_id = 15u8; + + // Spawn a task to send the notification after a short delay. + ctx.spawn_with_ctx(|ctx| async move { + // something expensive goes here + let result = 100_000_000; + let _ = ctx + .notify(&serde_json::json!({ + "req_id": req_id, + "result": result, + })) + .await; + }); + + // Return the request ID immediately. + Ok(req_id) + }) +} diff --git a/examples/cors.rs b/examples/cors.rs index 9309222..860a0a9 100644 --- a/examples/cors.rs +++ b/examples/cors.rs @@ -14,7 +14,7 @@ use axum::http::{HeaderValue, Method}; use eyre::{ensure, Context}; -use std::{future::IntoFuture, net::SocketAddr}; +use std::net::SocketAddr; use tower_http::cors::{AllowOrigin, Any, CorsLayer}; #[tokio::main] @@ -37,10 +37,7 @@ async fn main() -> eyre::Result<()> { println!("(specify cors domains as a comma-separated list to restrict origins)"); } println!("use Ctrl-C to stop"); - axum::serve(listener, router) - .into_future() - .await - .map_err(Into::into) + axum::serve(listener, router).await.map_err(Into::into) } fn get_allowed(cors: &str) -> eyre::Result { diff --git a/examples/ipc.rs b/examples/ipc.rs index e2e86d0..e1c048c 100644 --- a/examples/ipc.rs +++ b/examples/ipc.rs @@ -1,9 +1,19 @@ //! Basic example of running a JSON-RPC server over IPC using ajj //! -//! This example demonstrates how to set up a simple IPC server using `ajj`. +//! This example demonstrates how to set up a simple IPC server using `ajj`, and +//! defines a few basic RPC methods. +//! +//! The `make_router` function sets up a router with three routes: +//! - `helloWorld`: Returns a greeting string. +//! - `addNumbers`: Takes two numbers as parameters and returns their sum. +//! - `notify`: Sends a notification after a short delay, demonstrating the use +//! of notifications in the RPC context. use ajj::{ - pubsub::{ipc::local_socket, Connect}, + pubsub::{ + ipc::{to_name, ListenerOptions}, + Connect, + }, HandlerCtx, Router, }; use tempfile::NamedTempFile; @@ -20,10 +30,7 @@ async fn main() -> eyre::Result<()> { println!("use Ctrl-C to stop"); // The guard keeps the server running until dropped. - let guard = ajj::pubsub::ipc::ListenerOptions::new() - .name(name) - .serve(router) - .await?; + let guard = ListenerOptions::new().name(name).serve(router).await?; // Shut down on Ctrl-C tokio::signal::ctrl_c().await?; @@ -32,14 +39,6 @@ async fn main() -> eyre::Result<()> { Ok(()) } -fn to_name(path: &std::ffi::OsStr) -> std::io::Result> { - if cfg!(windows) && !path.as_encoded_bytes().starts_with(br"\\.\pipe\") { - local_socket::ToNsName::to_ns_name::(path) - } else { - local_socket::ToFsName::to_fs_name::(path) - } -} - // Setting up an AJJ router is easy and fun! fn make_router() -> Router<()> { Router::<()>::new() diff --git a/src/pubsub/mod.rs b/src/pubsub/mod.rs index ab8a164..54f64a2 100644 --- a/src/pubsub/mod.rs +++ b/src/pubsub/mod.rs @@ -98,7 +98,19 @@ pub use ipc_inner::ReadJsonStream; /// IPC support via interprocess local sockets. #[cfg(feature = "ipc")] pub mod ipc { + use std::ffi::OsStr; + pub use interprocess::local_socket::{self as local_socket, Listener, ListenerOptions, Name}; + + /// Convenience function to convert an [`OsStr`] to a local socket [`Name`] + /// in a platform-safe way. + pub fn to_name(path: &OsStr) -> std::io::Result> { + if cfg!(windows) && !path.as_encoded_bytes().starts_with(br"\\.\pipe\") { + local_socket::ToNsName::to_ns_name::(path) + } else { + local_socket::ToFsName::to_fs_name::(path) + } + } } mod shared; From ea131a2323f5f90d5bb9f0ba695edf46d793f3ab Mon Sep 17 00:00:00 2001 From: James Date: Mon, 13 Oct 2025 11:05:17 -0400 Subject: [PATCH 3/3] chore: bump version --- Cargo.toml | 2 +- src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 25ee873..317735f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ description = "Simple, modern, ergonomic JSON-RPC 2.0 router built with tower an keywords = ["json-rpc", "jsonrpc", "json"] categories = ["web-programming::http-server", "web-programming::websocket"] -version = "0.4.0" +version = "0.4.1" edition = "2021" rust-version = "1.81" authors = ["init4", "James Prestwich"] diff --git a/src/lib.rs b/src/lib.rs index 1a81c85..7f96723 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -148,7 +148,7 @@ )] #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![deny(unused_must_use, rust_2018_idioms)] -#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] #[macro_use] pub(crate) mod macros;