From 3074e464316591e47dd8881876ece71f3b46ddbb Mon Sep 17 00:00:00 2001 From: Tunay Engin Date: Wed, 11 Feb 2026 15:47:12 +0300 Subject: [PATCH] Add optional rustapi-grpc crate (tonic/prost) Introduce a new optional crate `rustapi-grpc` providing gRPC integration helpers built on Tonic/Prost. Exposes helpers (run_concurrently, run_rustapi_and_grpc, run_rustapi_and_grpc_with_shutdown), re-exports `tonic` and `prost`, and includes unit tests and documentation. Wire the crate into the workspace and the `rustapi-rs` facade as a feature-gated module (feature `grpc`), add the feature to the `full` meta-feature, and surface the option in the `cargo rustapi new` interactive template. Also update workspace Cargo.toml to declare tonic/prost, expand tokio features for the CLI crate, add docs/README entries, and update Cargo.lock accordingly. --- Cargo.lock | 102 ++++++++- Cargo.toml | 6 + README.md | 9 +- crates/cargo-rustapi/Cargo.toml | 2 +- crates/cargo-rustapi/src/commands/new.rs | 15 +- crates/rustapi-grpc/Cargo.toml | 23 +++ crates/rustapi-grpc/README.md | 52 +++++ crates/rustapi-grpc/src/lib.rs | 252 +++++++++++++++++++++++ crates/rustapi-rs/Cargo.toml | 6 +- crates/rustapi-rs/src/lib.rs | 16 ++ docs/cookbook/src/SUMMARY.md | 1 + docs/cookbook/src/crates/README.md | 1 + docs/cookbook/src/crates/rustapi_grpc.md | 60 ++++++ 13 files changed, 536 insertions(+), 9 deletions(-) create mode 100644 crates/rustapi-grpc/Cargo.toml create mode 100644 crates/rustapi-grpc/README.md create mode 100644 crates/rustapi-grpc/src/lib.rs create mode 100644 docs/cookbook/src/crates/rustapi_grpc.md diff --git a/Cargo.lock b/Cargo.lock index adaee480..1a705531 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -181,6 +181,49 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit 0.8.4", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + [[package]] name = "base62" version = "2.2.3" @@ -1396,6 +1439,19 @@ dependencies = [ "webpki-roots 1.0.6", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -1823,6 +1879,12 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "md-5" version = "0.10.6" @@ -2932,7 +2994,7 @@ dependencies = [ "hyper", "hyper-util", "linkme", - "matchit", + "matchit 0.7.3", "pin-project-lite", "prometheus", "proptest", @@ -3000,6 +3062,17 @@ dependencies = [ "uuid", ] +[[package]] +name = "rustapi-grpc" +version = "0.1.335" +dependencies = [ + "prost", + "rustapi-core", + "tokio", + "tonic", + "tonic-health", +] + [[package]] name = "rustapi-jobs" version = "0.1.335" @@ -3049,6 +3122,7 @@ dependencies = [ "doc-comment", "rustapi-core", "rustapi-extras", + "rustapi-grpc", "rustapi-macros", "rustapi-openapi", "rustapi-toon", @@ -4163,6 +4237,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] @@ -4268,20 +4343,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a286e33f82f8a1ee2df63f4fa35c0becf4a85a0cb03091a15fd7bf0b402dc94a" dependencies = [ "async-trait", + "axum", "base64", "bytes", + "h2", "http", "http-body", "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", "percent-encoding", "pin-project", + "socket2 0.6.2", "sync_wrapper", + "tokio", "tokio-stream", + "tower", "tower-layer", "tower-service", "tracing", ] +[[package]] +name = "tonic-health" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dbde2c702c4be12b9b2f6f7e6c824a84a7b7be177070cada8ee575a581af359" +dependencies = [ + "prost", + "tokio", + "tokio-stream", + "tonic", + "tonic-prost", +] + [[package]] name = "tonic-prost" version = "0.14.3" @@ -4313,11 +4409,15 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", + "indexmap", "pin-project-lite", + "slab", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", + "tracing", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c8c10507..b01fba27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "crates/rustapi-view", "crates/rustapi-testing", "crates/rustapi-jobs", + "crates/rustapi-grpc", "crates/cargo-rustapi", ] @@ -79,6 +80,10 @@ criterion = { version = "0.5", features = ["html_reports"] } tokio-tungstenite = "0.24" tungstenite = "0.24" +# gRPC +tonic = "0.14" +prost = "0.14" + # Template engine tera = "1.19" @@ -100,6 +105,7 @@ rustapi-ws = { path = "crates/rustapi-ws", version = "0.1.335" } rustapi-view = { path = "crates/rustapi-view", version = "0.1.335" } rustapi-testing = { path = "crates/rustapi-testing", version = "0.1.335" } rustapi-jobs = { path = "crates/rustapi-jobs", version = "0.1.335" } +rustapi-grpc = { path = "crates/rustapi-grpc", version = "0.1.335" } # HTTP/3 (QUIC) quinn = "0.11" diff --git a/README.md b/README.md index c408aa3a..bf7366e8 100644 --- a/README.md +++ b/README.md @@ -106,14 +106,16 @@ async fn main() { * ✅ **WebSocket**: Full permessage-deflate negotiation and compression * ✅ **OpenAPI**: Improved reference integrity and native validation docs * ✅ **Async Validation**: Deep integration with application state for complex rules +* ✅ **gRPC Foundation**: New optional `rustapi-grpc` crate with Tonic/Prost integration and side-by-side HTTP + gRPC runners (`run_rustapi_and_grpc`, `run_rustapi_and_grpc_with_shutdown`) +* ✅ **CLI DX Update**: `cargo rustapi new` interactive feature selection now includes `grpc` ## 🗺️ Public Roadmap: Next 30 Days We build in public. Here is our immediate focus for **February 2026**: -* [ ] **Visual Status Page**: Automatic health dashboard for all endpoints. -* [ ] **gRPC Integration**: First-class support via Tonic. -* [ ] **Distributed Tracing**: One-line OpenTelemetry setup. +* [x] **Visual Status Page**: Automatic health dashboard for all endpoints. +* [x] **gRPC Integration (Foundation)**: First-class optional crate via Tonic (`rustapi-grpc`) with RustAPI facade-level feature flag support. +* [x] **Distributed Tracing**: One-line OpenTelemetry setup. * [ ] **RustAPI Cloud**: One-click deploy to major cloud providers. ## 📚 Documentation @@ -122,6 +124,7 @@ We moved our detailed architecture, recipes, and deep-dives to the **[Cookbook]( * [System Architecture & Diagrams](docs/cookbook/src/architecture/system_overview.md) * [Performance Benchmarks](docs/cookbook/src/concepts/performance.md) +* [gRPC Integration Guide](docs/cookbook/src/crates/rustapi_grpc.md) * [Full Examples](crates/rustapi-rs/examples/) --- diff --git a/crates/cargo-rustapi/Cargo.toml b/crates/cargo-rustapi/Cargo.toml index 0c8f8933..7cb171af 100644 --- a/crates/cargo-rustapi/Cargo.toml +++ b/crates/cargo-rustapi/Cargo.toml @@ -28,7 +28,7 @@ walkdir = "2.5" toml_edit = "0.22" # Async -tokio = { workspace = true, features = ["process", "fs"] } +tokio = { workspace = true, features = ["process", "fs", "macros", "rt-multi-thread", "time", "signal"] } # Serialization serde = { workspace = true } diff --git a/crates/cargo-rustapi/src/commands/new.rs b/crates/cargo-rustapi/src/commands/new.rs index af5bbcf6..ae6953d6 100644 --- a/crates/cargo-rustapi/src/commands/new.rs +++ b/crates/cargo-rustapi/src/commands/new.rs @@ -90,10 +90,19 @@ pub async fn new_project(mut args: NewArgs) -> Result<()> { } else if args.yes { vec![] } else { - let available = ["jwt", "cors", "rate-limit", "config", "toon", "ws", "view"]; + let available = [ + "jwt", + "cors", + "rate-limit", + "config", + "toon", + "ws", + "view", + "grpc", + ]; let defaults = match template { - ProjectTemplate::Full => vec![true, true, true, true, false, false, false], - ProjectTemplate::Web => vec![false, false, false, false, false, false, true], + ProjectTemplate::Full => vec![true, true, true, true, false, false, false, false], + ProjectTemplate::Web => vec![false, false, false, false, false, false, true, false], _ => vec![false; available.len()], }; diff --git a/crates/rustapi-grpc/Cargo.toml b/crates/rustapi-grpc/Cargo.toml new file mode 100644 index 00000000..4ca0c986 --- /dev/null +++ b/crates/rustapi-grpc/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "rustapi-grpc" +description = "gRPC integration helpers for RustAPI powered by Tonic" +documentation = "https://docs.rs/rustapi-grpc" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +keywords = ["web", "framework", "api", "grpc", "tonic"] +categories = ["web-programming::http-server", "network-programming"] +rust-version.workspace = true +readme = "README.md" + +[dependencies] +rustapi-core = { workspace = true } +tokio = { workspace = true, features = ["macros"] } +tonic = { workspace = true, features = ["transport"] } +prost = { workspace = true } + +[dev-dependencies] +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] } +tonic-health = "0.14" diff --git a/crates/rustapi-grpc/README.md b/crates/rustapi-grpc/README.md new file mode 100644 index 00000000..6dc90540 --- /dev/null +++ b/crates/rustapi-grpc/README.md @@ -0,0 +1,52 @@ +# rustapi-grpc + +`rustapi-grpc` provides gRPC integration helpers for RustAPI with [Tonic](https://github.com/hyperium/tonic). + +## What it gives you + +- `run_concurrently(http, grpc)`: run two server futures together. +- `run_rustapi_and_grpc(app, http_addr, grpc)`: convenience helper for RustAPI + gRPC side-by-side. +- `run_rustapi_and_grpc_with_shutdown(app, http_addr, signal, grpc_with_shutdown)`: shared shutdown signal for both servers. +- Re-exports: `tonic`, `prost`. + +## Example + +```rust,ignore +use rustapi_rs::grpc::{run_rustapi_and_grpc, tonic}; +use rustapi_rs::prelude::*; + +#[rustapi_rs::get("/health")] +async fn health() -> &'static str { "ok" } + +#[tokio::main] +async fn main() -> Result<(), Box> { + let http_app = RustApi::new().route("/health", get(health)); + + let grpc_addr = "127.0.0.1:50051".parse()?; + let grpc_server = tonic::transport::Server::builder() + .add_service(MyGreeterServer::new(MyGreeter::default())) + .serve(grpc_addr); + + run_rustapi_and_grpc(http_app, "127.0.0.1:8080", grpc_server).await?; + Ok(()) +} +``` + +## Shared shutdown (Ctrl+C) + +```rust,ignore +use rustapi_rs::grpc::{run_rustapi_and_grpc_with_shutdown, tonic}; + +let grpc_addr = "127.0.0.1:50051".parse()?; + +run_rustapi_and_grpc_with_shutdown( + http_app, + "127.0.0.1:8080", + tokio::signal::ctrl_c(), + move |shutdown| { + tonic::transport::Server::builder() + .add_service(MyGreeterServer::new(MyGreeter::default())) + .serve_with_shutdown(grpc_addr, shutdown) + }, +).await?; +``` diff --git a/crates/rustapi-grpc/src/lib.rs b/crates/rustapi-grpc/src/lib.rs new file mode 100644 index 00000000..59585e13 --- /dev/null +++ b/crates/rustapi-grpc/src/lib.rs @@ -0,0 +1,252 @@ +//! # rustapi-grpc +//! +//! gRPC integration helpers for RustAPI using [`tonic`]. +//! +//! This crate keeps RustAPI's facade approach: your app code stays simple while you can +//! run a RustAPI HTTP server and a Tonic gRPC server side-by-side in the same process. +//! +//! ## Quick start +//! +//! ```rust,ignore +//! use rustapi_rs::grpc::{run_rustapi_and_grpc, tonic}; +//! use rustapi_rs::prelude::*; +//! +//! #[rustapi_rs::get("/health")] +//! async fn health() -> &'static str { "ok" } +//! +//! #[tokio::main] +//! async fn main() -> Result<(), Box> { +//! let http_app = RustApi::new().route("/health", get(health)); +//! +//! let grpc_addr = "127.0.0.1:50051".parse()?; +//! let grpc_server = tonic::transport::Server::builder() +//! .add_service(MyGreeterServer::new(MyGreeter::default())) +//! .serve(grpc_addr); +//! +//! run_rustapi_and_grpc(http_app, "127.0.0.1:8080", grpc_server).await?; +//! Ok(()) +//! } +//! ``` + +#![warn(missing_docs)] +#![warn(rustdoc::missing_crate_level_docs)] + +use rustapi_core::RustApi; +use std::error::Error; +use std::future::Future; +use std::pin::Pin; +use tokio::sync::watch; + +/// Boxed error used by this crate. +pub type BoxError = Box; + +/// Result type used by this crate. +pub type Result = std::result::Result; + +/// Shutdown future type used by gRPC server builders. +pub type ShutdownFuture = Pin + Send + 'static>>; + +/// Re-export `tonic` so users can use a single dependency surface from `rustapi-rs`. +pub use tonic; + +/// Re-export `prost` for protobuf message derives and runtime types. +pub use prost; + +fn to_boxed_error(err: E) -> BoxError +where + E: Error + Send + Sync + 'static, +{ + Box::new(err) +} + +/// Run two independent servers/tasks concurrently. +/// +/// This is useful for running a RustAPI HTTP server together with a Tonic gRPC server. +/// +/// The function returns when one of the futures returns an error, or when both complete successfully. +pub async fn run_concurrently(http_future: HF, grpc_future: GF) -> Result<()> +where + HF: Future> + Send, + GF: Future> + Send, + HE: Error + Send + Sync + 'static, + GE: Error + Send + Sync + 'static, +{ + let http_task = async move { http_future.await.map_err(to_boxed_error) }; + let grpc_task = async move { grpc_future.await.map_err(to_boxed_error) }; + + let (_http_ok, _grpc_ok) = tokio::try_join!(http_task, grpc_task)?; + Ok(()) +} + +/// Run a `RustApi` HTTP server and any gRPC future side-by-side. +/// +/// `grpc_future` is typically a Tonic server future: +/// `tonic::transport::Server::builder().add_service(...).serve(addr)`. +pub async fn run_rustapi_and_grpc( + app: RustApi, + http_addr: impl AsRef, + grpc_future: GF, +) -> Result<()> +where + GF: Future> + Send, + GE: Error + Send + Sync + 'static, +{ + let http_addr = http_addr.as_ref().to_string(); + let http_task = async move { app.run(&http_addr).await }; + let grpc_task = async move { grpc_future.await.map_err(to_boxed_error) }; + + let (_http_ok, _grpc_ok) = tokio::try_join!(http_task, grpc_task)?; + Ok(()) +} + +/// Run RustAPI HTTP and gRPC servers together with a shared shutdown signal. +/// +/// This helper lets you provide a single shutdown signal (for example `tokio::signal::ctrl_c()`) +/// and uses it for both servers. +/// +/// # Example +/// +/// ```rust,ignore +/// use rustapi_rs::grpc::{run_rustapi_and_grpc_with_shutdown, tonic}; +/// use rustapi_rs::prelude::*; +/// +/// #[rustapi_rs::get("/health")] +/// async fn health() -> &'static str { "ok" } +/// +/// #[tokio::main] +/// async fn main() -> Result<(), Box> { +/// let app = RustApi::new().route("/health", get(health)); +/// let grpc_addr = "127.0.0.1:50051".parse()?; +/// +/// run_rustapi_and_grpc_with_shutdown( +/// app, +/// "127.0.0.1:8080", +/// tokio::signal::ctrl_c(), +/// move |shutdown| { +/// tonic::transport::Server::builder() +/// .add_service(MyGreeterServer::new(MyGreeter::default())) +/// .serve_with_shutdown(grpc_addr, shutdown) +/// }, +/// ).await?; +/// +/// Ok(()) +/// } +/// ``` +pub async fn run_rustapi_and_grpc_with_shutdown( + app: RustApi, + http_addr: impl AsRef, + shutdown_signal: SF, + grpc_with_shutdown: F, +) -> Result<()> +where + GF: Future> + Send, + GE: Error + Send + Sync + 'static, + SF: Future + Send + 'static, + F: FnOnce(ShutdownFuture) -> GF, +{ + let http_addr = http_addr.as_ref().to_string(); + let (shutdown_tx, shutdown_rx) = watch::channel(false); + + // Fan out a single shutdown signal to both servers. + let shutdown_dispatch = tokio::spawn(async move { + shutdown_signal.await; + let _ = shutdown_tx.send(true); + }); + + let http_shutdown = shutdown_notifier(shutdown_rx.clone()); + let grpc_shutdown = shutdown_notifier(shutdown_rx); + + let http_task = async move { app.run_with_shutdown(&http_addr, http_shutdown).await }; + let grpc_task = async move { + grpc_with_shutdown(Box::pin(grpc_shutdown)) + .await + .map_err(to_boxed_error) + }; + + let joined = tokio::try_join!(http_task, grpc_task).map(|_| ()); + + shutdown_dispatch.abort(); + let _ = shutdown_dispatch.await; + + joined +} + +async fn shutdown_notifier(mut rx: watch::Receiver) { + if *rx.borrow() { + return; + } + + while rx.changed().await.is_ok() { + if *rx.borrow() { + break; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rustapi_core::get; + use std::io; + use tokio::sync::oneshot; + use tokio::time::{sleep, timeout, Duration}; + + #[tokio::test] + async fn run_concurrently_returns_ok_when_both_succeed() { + let http = async { Ok::<(), io::Error>(()) }; + let grpc = async { Ok::<(), io::Error>(()) }; + + let result = run_concurrently(http, grpc).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn run_concurrently_returns_err_when_any_fails() { + let http = async { Err::<(), _>(io::Error::other("http failed")) }; + + let grpc = async { + sleep(Duration::from_millis(20)).await; + Ok::<(), io::Error>(()) + }; + + let result = run_concurrently(http, grpc).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn run_rustapi_and_grpc_with_shutdown_stops_both_servers() { + async fn health() -> &'static str { + "ok" + } + + let app = RustApi::new().route("/health", get(health)); + let grpc_addr = "127.0.0.1:0".parse().expect("valid socket addr"); + let (tx, rx) = oneshot::channel::<()>(); + + let run_future = run_rustapi_and_grpc_with_shutdown( + app, + "127.0.0.1:0", + async move { + let _ = rx.await; + }, + move |shutdown| { + let (_reporter, health_service) = tonic_health::server::health_reporter(); + tonic::transport::Server::builder() + .add_service(health_service) + .serve_with_shutdown(grpc_addr, shutdown) + }, + ); + + tokio::spawn(async move { + sleep(Duration::from_millis(75)).await; + let _ = tx.send(()); + }); + + let result = timeout(Duration::from_secs(3), run_future).await; + assert!(result.is_ok(), "servers should stop before timeout"); + assert!( + result.expect("timeout checked").is_ok(), + "graceful shutdown should succeed" + ); + } +} diff --git a/crates/rustapi-rs/Cargo.toml b/crates/rustapi-rs/Cargo.toml index 0ac00f35..6ecfaf0d 100644 --- a/crates/rustapi-rs/Cargo.toml +++ b/crates/rustapi-rs/Cargo.toml @@ -19,6 +19,7 @@ rustapi-extras = { workspace = true, optional = true } rustapi-toon = { workspace = true, optional = true } rustapi-ws = { workspace = true, optional = true } rustapi-view = { workspace = true, optional = true } +rustapi-grpc = { workspace = true, optional = true } rustapi-validate = { workspace = true } async-trait = { workspace = true } @@ -68,6 +69,9 @@ ws = ["dep:rustapi-ws"] # Template engine support view = ["dep:rustapi-view"] +# gRPC support +grpc = ["dep:rustapi-grpc"] + # New Phase 11 & Observability features timeout = ["dep:rustapi-extras", "rustapi-extras/timeout"] guard = ["dep:rustapi-extras", "rustapi-extras/guard"] @@ -87,4 +91,4 @@ replay = ["dep:rustapi-extras", "rustapi-extras/replay"] # Meta features extras = ["jwt", "cors", "rate-limit"] -full = ["extras", "config", "cookies", "sqlx", "toon", "insight", "compression", "ws", "view", "timeout", "guard", "logging", "circuit-breaker", "security-headers", "api-key", "cache", "dedup", "sanitization", "otel", "structured-logging", "replay", "legacy-validator"] +full = ["extras", "config", "cookies", "sqlx", "toon", "insight", "compression", "ws", "view", "grpc", "timeout", "guard", "logging", "circuit-breaker", "security-headers", "api-key", "cache", "dedup", "sanitization", "otel", "structured-logging", "replay", "legacy-validator"] diff --git a/crates/rustapi-rs/src/lib.rs b/crates/rustapi-rs/src/lib.rs index 1afed499..66982a36 100644 --- a/crates/rustapi-rs/src/lib.rs +++ b/crates/rustapi-rs/src/lib.rs @@ -49,6 +49,7 @@ //! - `config` - Configuration management with `.env` file support //! - `cookies` - Cookie parsing extractor //! - `sqlx` - SQLx database error conversion to ApiError +//! - `grpc` - Tonic-based gRPC integration helpers //! - `legacy-validator` - Compatibility mode for `validator::Validate` //! - `extras` - Meta feature enabling jwt, cors, and rate-limit //! - `full` - All optional features enabled @@ -222,6 +223,15 @@ pub mod view { pub use rustapi_view::*; } +// Re-export gRPC support (feature-gated) +#[cfg(feature = "grpc")] +pub mod grpc { + //! gRPC integration helpers powered by Tonic. + //! + //! Use this module to run RustAPI HTTP and gRPC servers side-by-side. + pub use rustapi_grpc::*; +} + /// Prelude module - import everything you need with `use rustapi_rs::prelude::*` pub mod prelude { // Core types @@ -364,6 +374,12 @@ pub mod prelude { // View/Template types (feature-gated) #[cfg(feature = "view")] pub use rustapi_view::{ContextBuilder, Templates, TemplatesConfig, View}; + + // gRPC integration (feature-gated) + #[cfg(feature = "grpc")] + pub use rustapi_grpc::{ + run_concurrently, run_rustapi_and_grpc, run_rustapi_and_grpc_with_shutdown, + }; } #[cfg(test)] diff --git a/docs/cookbook/src/SUMMARY.md b/docs/cookbook/src/SUMMARY.md index e63bd506..b7abae2b 100644 --- a/docs/cookbook/src/SUMMARY.md +++ b/docs/cookbook/src/SUMMARY.md @@ -21,6 +21,7 @@ - [rustapi-extras: The Toolbox](crates/rustapi_extras.md) - [rustapi-toon: The Diplomat](crates/rustapi_toon.md) - [rustapi-ws: The Live Wire](crates/rustapi_ws.md) + - [rustapi-grpc: The Bridge](crates/rustapi_grpc.md) - [rustapi-view: The Artist](crates/rustapi_view.md) - [rustapi-jobs: The Workhorse](crates/rustapi_jobs.md) - [rustapi-testing: The Auditor](crates/rustapi_testing.md) diff --git a/docs/cookbook/src/crates/README.md b/docs/cookbook/src/crates/README.md index f62e81d8..8abdc4a7 100644 --- a/docs/cookbook/src/crates/README.md +++ b/docs/cookbook/src/crates/README.md @@ -8,3 +8,4 @@ RustAPI is a collection of focused, interoperable crates. Each crate has a speci - **[rustapi-core](rustapi_core.md)**: The Engine - **[rustapi-macros](rustapi_macros.md)**: The Magic - **[rustapi-validate](rustapi_validation.md)**: The Gatekeeper +- **[rustapi-grpc](rustapi_grpc.md)**: The Bridge diff --git a/docs/cookbook/src/crates/rustapi_grpc.md b/docs/cookbook/src/crates/rustapi_grpc.md new file mode 100644 index 00000000..01572762 --- /dev/null +++ b/docs/cookbook/src/crates/rustapi_grpc.md @@ -0,0 +1,60 @@ +# rustapi-grpc: The Bridge + +**Lens**: "The Bridge" +**Philosophy**: "HTTP and gRPC, one runtime." + +`rustapi-grpc` is an optional crate that helps you run a RustAPI HTTP server and a Tonic gRPC server in the same process. + +## What You Get + +- `run_concurrently(http, grpc)` for running two server futures side-by-side. +- `run_rustapi_and_grpc(app, http_addr, grpc)` convenience helper. +- `run_rustapi_and_grpc_with_shutdown(app, http_addr, signal, grpc_with_shutdown)` for graceful shared shutdown. +- Re-exports of `tonic` and `prost`. + +## Enable It + +```toml +[dependencies] +rustapi-rs = { version = "0.1.335", features = ["grpc"] } +``` + +## Basic Usage + +```rust,ignore +use rustapi_rs::grpc::{run_rustapi_and_grpc, tonic}; +use rustapi_rs::prelude::*; + +#[rustapi_rs::get("/health")] +async fn health() -> &'static str { "ok" } + +#[tokio::main] +async fn main() -> Result<(), Box> { + let http_app = RustApi::new().route("/health", get(health)); + + let grpc_addr = "127.0.0.1:50051".parse()?; + let grpc_server = tonic::transport::Server::builder() + .add_service(MyGreeterServer::new(MyGreeter::default())) + .serve(grpc_addr); + + run_rustapi_and_grpc(http_app, "127.0.0.1:8080", grpc_server).await?; + Ok(()) +} +``` + +## Graceful Shutdown + +```rust,ignore +use rustapi_rs::grpc::{run_rustapi_and_grpc_with_shutdown, tonic}; + +run_rustapi_and_grpc_with_shutdown( + http_app, + "127.0.0.1:8080", + tokio::signal::ctrl_c(), + move |shutdown| { + tonic::transport::Server::builder() + .add_service(MyGreeterServer::new(MyGreeter::default())) + .serve_with_shutdown("127.0.0.1:50051".parse().unwrap(), shutdown) + }, +).await?; +```