html2pdf_api/integrations/axum.rs
1//! Axum framework integration.
2//!
3//! This module provides helpers for using `BrowserPool` with Axum.
4//!
5//! # Setup
6//!
7//! Add to your `Cargo.toml`:
8//!
9//! ```toml
10//! [dependencies]
11//! html2pdf-api = { version = "0.1", features = ["axum-integration"] }
12//! axum = "0.8"
13//! tower = "0.5"
14//! ```
15//!
16//! # Basic Usage with State
17//!
18//! ```rust,ignore
19//! use axum::{
20//! Router,
21//! routing::get,
22//! extract::State,
23//! response::IntoResponse,
24//! http::StatusCode,
25//! };
26//! use html2pdf_api::prelude::*;
27//! use std::sync::Arc;
28//!
29//! async fn generate_pdf(
30//! State(pool): State<SharedBrowserPool>,
31//! ) -> Result<impl IntoResponse, StatusCode> {
32//! let pool_guard = pool.lock().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
33//! let browser = pool_guard.get().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
34//!
35//! let tab = browser.new_tab().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
36//! tab.navigate_to("https://example.com").map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
37//!
38//! // Generate PDF...
39//! let pdf_data = tab.print_to_pdf(None).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
40//!
41//! Ok((
42//! [(axum::http::header::CONTENT_TYPE, "application/pdf")],
43//! pdf_data,
44//! ))
45//! }
46//!
47//! #[tokio::main]
48//! async fn main() {
49//! // Create and warmup pool
50//! let pool = BrowserPool::builder()
51//! .factory(Box::new(ChromeBrowserFactory::with_defaults()))
52//! .build()
53//! .expect("Failed to create pool");
54//!
55//! pool.warmup().await.expect("Failed to warmup");
56//!
57//! // Convert to shared state
58//! let shared_pool = pool.into_shared();
59//!
60//! let app = Router::new()
61//! .route("/pdf", get(generate_pdf))
62//! .with_state(shared_pool);
63//!
64//! let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await.unwrap();
65//! axum::serve(listener, app).await.unwrap();
66//! }
67//! ```
68//!
69//! # Using Extension Layer
70//!
71//! Alternatively, use the Extension layer pattern:
72//!
73//! ```rust,ignore
74//! use axum::{
75//! Router,
76//! routing::get,
77//! Extension,
78//! response::IntoResponse,
79//! };
80//! use html2pdf_api::prelude::*;
81//! use std::sync::Arc;
82//!
83//! async fn generate_pdf(
84//! Extension(pool): Extension<SharedBrowserPool>,
85//! ) -> impl IntoResponse {
86//! let pool_guard = pool.lock().unwrap();
87//! let browser = pool_guard.get().unwrap();
88//! // ...
89//! }
90//!
91//! #[tokio::main]
92//! async fn main() {
93//! let pool = BrowserPool::builder()
94//! .factory(Box::new(ChromeBrowserFactory::with_defaults()))
95//! .build()
96//! .expect("Failed to create pool");
97//!
98//! pool.warmup().await.expect("Failed to warmup");
99//!
100//! let shared_pool = pool.into_shared();
101//!
102//! let app = Router::new()
103//! .route("/pdf", get(generate_pdf))
104//! .layer(Extension(shared_pool));
105//!
106//! let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await.unwrap();
107//! axum::serve(listener, app).await.unwrap();
108//! }
109//! ```
110//!
111//! # Using with `init_browser_pool`
112//!
113//! If you have the `env-config` feature enabled:
114//!
115//! ```rust,ignore
116//! use axum::{Router, routing::get};
117//! use html2pdf_api::init_browser_pool;
118//!
119//! #[tokio::main]
120//! async fn main() {
121//! let pool = init_browser_pool().await
122//! .expect("Failed to initialize browser pool");
123//!
124//! let app = Router::new()
125//! .route("/pdf", get(generate_pdf))
126//! .with_state(pool);
127//!
128//! let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await.unwrap();
129//! axum::serve(listener, app).await.unwrap();
130//! }
131//! ```
132//!
133//! # Graceful Shutdown
134//!
135//! For proper cleanup with graceful shutdown:
136//!
137//! ```rust,ignore
138//! use axum::Router;
139//! use html2pdf_api::prelude::*;
140//! use std::sync::Arc;
141//! use tokio::signal;
142//!
143//! #[tokio::main]
144//! async fn main() {
145//! let pool = BrowserPool::builder()
146//! .factory(Box::new(ChromeBrowserFactory::with_defaults()))
147//! .build()
148//! .expect("Failed to create pool");
149//!
150//! pool.warmup().await.expect("Failed to warmup");
151//!
152//! let shared_pool = Arc::new(std::sync::Mutex::new(pool));
153//! let shutdown_pool = Arc::clone(&shared_pool);
154//!
155//! let app = Router::new()
156//! .with_state(shared_pool);
157//!
158//! let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await.unwrap();
159//!
160//! axum::serve(listener, app)
161//! .with_graceful_shutdown(shutdown_signal(shutdown_pool))
162//! .await
163//! .unwrap();
164//! }
165//!
166//! async fn shutdown_signal(pool: SharedBrowserPool) {
167//! let ctrl_c = async {
168//! signal::ctrl_c().await.expect("Failed to listen for ctrl+c");
169//! };
170//!
171//! #[cfg(unix)]
172//! let terminate = async {
173//! signal::unix::signal(signal::unix::SignalKind::terminate())
174//! .expect("Failed to install signal handler")
175//! .recv()
176//! .await;
177//! };
178//!
179//! #[cfg(not(unix))]
180//! let terminate = std::future::pending::<()>();
181//!
182//! tokio::select! {
183//! _ = ctrl_c => {},
184//! _ = terminate => {},
185//! }
186//!
187//! println!("Shutting down...");
188//! if let Ok(mut pool) = pool.lock() {
189//! pool.shutdown_async().await;
190//! }
191//! }
192//! ```
193//!
194//! # Custom Extractor
195//!
196//! For cleaner handler signatures, create a custom extractor:
197//!
198//! ```rust,ignore
199//! use axum::{
200//! async_trait,
201//! extract::{FromRequestParts, State},
202//! http::{request::Parts, StatusCode},
203//! };
204//! use html2pdf_api::prelude::*;
205//!
206//! pub struct Browser(pub BrowserHandle);
207//!
208//! #[async_trait]
209//! impl FromRequestParts<SharedBrowserPool> for Browser {
210//! type Rejection = StatusCode;
211//!
212//! async fn from_request_parts(
213//! _parts: &mut Parts,
214//! state: &SharedBrowserPool,
215//! ) -> Result<Self, Self::Rejection> {
216//! let pool = state.lock().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
217//! let browser = pool.get().map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?;
218//! Ok(Browser(browser))
219//! }
220//! }
221//!
222//! // Then use in handlers:
223//! async fn generate_pdf(Browser(browser): Browser) -> impl IntoResponse {
224//! let tab = browser.new_tab().unwrap();
225//! // ...
226//! }
227//! ```
228
229use axum::extract::State;
230
231use crate::SharedBrowserPool;
232use crate::pool::BrowserPool;
233
234/// Type alias for Axum `State` extractor with the shared pool.
235///
236/// Use this type in your handler parameters:
237///
238/// ```rust,ignore
239/// async fn handler(
240/// BrowserPoolState(pool): BrowserPoolState,
241/// ) -> impl IntoResponse {
242/// let pool = pool.lock().unwrap();
243/// let browser = pool.get()?;
244/// // ...
245/// }
246/// ```
247pub type BrowserPoolState = State<SharedBrowserPool>;
248
249/// Extension trait for `BrowserPool` with Axum helpers.
250///
251/// Provides convenient methods for integrating with Axum.
252pub trait BrowserPoolAxumExt {
253 /// Convert the pool into a form suitable for Axum's `with_state()`.
254 ///
255 /// # Example
256 ///
257 /// ```rust,ignore
258 /// use html2pdf_api::integrations::axum::BrowserPoolAxumExt;
259 ///
260 /// let pool = BrowserPool::builder()
261 /// .factory(Box::new(ChromeBrowserFactory::with_defaults()))
262 /// .build()?;
263 ///
264 /// let state = pool.into_axum_state();
265 ///
266 /// Router::new()
267 /// .route("/pdf", get(generate_pdf))
268 /// .with_state(state)
269 /// ```
270 fn into_axum_state(self) -> SharedBrowserPool;
271
272 /// Convert the pool into an Extension layer.
273 ///
274 /// # Example
275 ///
276 /// ```rust,ignore
277 /// use axum::Extension;
278 /// use html2pdf_api::integrations::axum::BrowserPoolAxumExt;
279 ///
280 /// let pool = BrowserPool::builder()
281 /// .factory(Box::new(ChromeBrowserFactory::with_defaults()))
282 /// .build()?;
283 ///
284 /// let extension = pool.into_axum_extension();
285 ///
286 /// Router::new()
287 /// .route("/pdf", get(generate_pdf))
288 /// .layer(extension)
289 /// ```
290 fn into_axum_extension(self) -> axum::Extension<SharedBrowserPool>;
291}
292
293impl BrowserPoolAxumExt for BrowserPool {
294 fn into_axum_state(self) -> SharedBrowserPool {
295 self.into_shared()
296 }
297
298 fn into_axum_extension(self) -> axum::Extension<SharedBrowserPool> {
299 axum::Extension(self.into_shared())
300 }
301}
302
303/// Create an Axum Extension from an existing shared pool.
304///
305/// Use this when you already have a `SharedBrowserPool` and want to
306/// create an Extension layer.
307///
308/// # Parameters
309///
310/// * `pool` - The shared browser pool.
311///
312/// # Returns
313///
314/// `Extension<SharedBrowserPool>` ready for use with `Router::layer()`.
315///
316/// # Example
317///
318/// ```rust,ignore
319/// use html2pdf_api::integrations::axum::create_extension;
320///
321/// let shared_pool = pool.into_shared();
322/// let extension = create_extension(shared_pool);
323///
324/// Router::new().layer(extension)
325/// ```
326pub fn create_extension(pool: SharedBrowserPool) -> axum::Extension<SharedBrowserPool> {
327 axum::Extension(pool)
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[test]
335 fn test_type_alias_compiles() {
336 // This test just verifies the type alias is valid
337 fn _accepts_pool_state(_: BrowserPoolState) {}
338 }
339}