Skip to content

Commit 5a3bf33

Browse files
committed
Add lifecycle hooks, pagination, and cache
Introduce an in-process events system and lifecycle hooks (events::EventBus, LifecycleHooks) and integrate on_start/on_shutdown hooks into RustApi (on_start/on_shutdown and invocation in run/run_with_shutdown). Add pagination helpers: Paginate and CursorPaginate extractors, and Paginated / CursorPaginated HATEOAS response wrappers with IntoResponse implementations and OpenAPI-friendly metadata/headers. Extend Route with error_response to annotate typed error responses in generated OpenAPI. Significantly enhance the cache middleware: CacheConfig options (ttl, max_entries, vary_headers, etag), CacheStore with LRU eviction, CacheHandle for invalidation, ETag/If-None-Match handling, vary-by-header keys, builder API (with_handle/build), and tests. Update public exports to expose new types from rustapi-core and adjust rustapi-extras exports accordingly. Added tests and small refactors across core crates to wire these features together.
1 parent 189ae08 commit 5a3bf33

24 files changed

Lines changed: 2118 additions & 403 deletions

crates/rustapi-core/src/app.rs

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
//! RustApi application builder
22
33
use crate::error::Result;
4+
use crate::events::LifecycleHooks;
45
use crate::interceptor::{InterceptorChain, RequestInterceptor, ResponseInterceptor};
56
use crate::middleware::{BodyLimitLayer, LayerStack, MiddlewareLayer, DEFAULT_BODY_LIMIT};
67
use crate::response::IntoResponse;
78
use crate::router::{MethodRouter, Router};
89
use crate::server::Server;
910
use std::collections::BTreeMap;
11+
use std::future::Future;
1012
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
1113

1214
/// Main application builder for RustAPI
@@ -32,6 +34,7 @@ pub struct RustApi {
3234
layers: LayerStack,
3335
body_limit: Option<usize>,
3436
interceptors: InterceptorChain,
37+
lifecycle_hooks: LifecycleHooks,
3538
#[cfg(feature = "http3")]
3639
http3_config: Option<crate::http3::Http3Config>,
3740
status_config: Option<crate::status::StatusConfig>,
@@ -60,6 +63,7 @@ impl RustApi {
6063
layers: LayerStack::new(),
6164
body_limit: Some(DEFAULT_BODY_LIMIT), // Default 1MB limit
6265
interceptors: InterceptorChain::new(),
66+
lifecycle_hooks: LifecycleHooks::new(),
6367
#[cfg(feature = "http3")]
6468
http3_config: None,
6569
status_config: None,
@@ -298,6 +302,60 @@ impl RustApi {
298302
app
299303
}
300304

305+
/// Register an `on_start` lifecycle hook
306+
///
307+
/// The callback runs **after** route registration and **before** the server
308+
/// begins accepting connections. Multiple hooks execute in registration order.
309+
///
310+
/// # Example
311+
///
312+
/// ```rust,ignore
313+
/// RustApi::new()
314+
/// .on_start(|| async {
315+
/// println!("🚀 Server starting...");
316+
/// // e.g. run DB migrations, warm caches
317+
/// })
318+
/// .run("127.0.0.1:8080")
319+
/// .await
320+
/// ```
321+
pub fn on_start<F, Fut>(mut self, hook: F) -> Self
322+
where
323+
F: FnOnce() -> Fut + Send + 'static,
324+
Fut: Future<Output = ()> + Send + 'static,
325+
{
326+
self.lifecycle_hooks
327+
.on_start
328+
.push(Box::new(move || Box::pin(hook())));
329+
self
330+
}
331+
332+
/// Register an `on_shutdown` lifecycle hook
333+
///
334+
/// The callback runs **after** the shutdown signal is received and the server
335+
/// stops accepting new connections. Multiple hooks execute in registration order.
336+
///
337+
/// # Example
338+
///
339+
/// ```rust,ignore
340+
/// RustApi::new()
341+
/// .on_shutdown(|| async {
342+
/// println!("👋 Server shutting down...");
343+
/// // e.g. flush logs, close DB connections
344+
/// })
345+
/// .run_with_shutdown("127.0.0.1:8080", ctrl_c())
346+
/// .await
347+
/// ```
348+
pub fn on_shutdown<F, Fut>(mut self, hook: F) -> Self
349+
where
350+
F: FnOnce() -> Fut + Send + 'static,
351+
Fut: Future<Output = ()> + Send + 'static,
352+
{
353+
self.lifecycle_hooks
354+
.on_shutdown
355+
.push(Box::new(move || Box::pin(hook())));
356+
self
357+
}
358+
301359
/// Register an OpenAPI schema
302360
///
303361
/// # Example
@@ -955,6 +1013,11 @@ impl RustApi {
9551013
self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
9561014
}
9571015

1016+
// Run on_start lifecycle hooks before accepting connections
1017+
for hook in self.lifecycle_hooks.on_start {
1018+
hook().await;
1019+
}
1020+
9581021
let server = Server::new(self.router, self.layers, self.interceptors);
9591022
server.run(addr).await
9601023
}
@@ -975,8 +1038,23 @@ impl RustApi {
9751038
self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
9761039
}
9771040

1041+
// Run on_start lifecycle hooks before accepting connections
1042+
for hook in self.lifecycle_hooks.on_start {
1043+
hook().await;
1044+
}
1045+
1046+
// Wrap the shutdown signal to run on_shutdown hooks after signal fires
1047+
let shutdown_hooks = self.lifecycle_hooks.on_shutdown;
1048+
let wrapped_signal = async move {
1049+
signal.await;
1050+
// Run on_shutdown hooks after the shutdown signal fires
1051+
for hook in shutdown_hooks {
1052+
hook().await;
1053+
}
1054+
};
1055+
9781056
let server = Server::new(self.router, self.layers, self.interceptors);
979-
server.run_with_shutdown(addr.as_ref(), signal).await
1057+
server.run_with_shutdown(addr.as_ref(), wrapped_signal).await
9801058
}
9811059

9821060
/// Get the inner router (for testing or advanced usage)

0 commit comments

Comments
 (0)