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}