html2pdf_api/integrations/actix.rs
1//! Actix-web framework integration.
2//!
3//! This module provides helpers and pre-built handlers for using `BrowserPool`
4//! with Actix-web. You can choose between using the pre-built handlers for
5//! quick setup, or writing custom handlers for full control.
6//!
7//! # Quick Start
8//!
9//! ## Option 1: Pre-built Routes (Fastest Setup)
10//!
11//! Use [`configure_routes`] to add all PDF endpoints with a single line:
12//!
13//! ```rust,ignore
14//! use actix_web::{App, HttpServer, web};
15//! use html2pdf_api::prelude::*;
16//!
17//! #[actix_web::main]
18//! async fn main() -> std::io::Result<()> {
19//! let pool = init_browser_pool().await
20//! .expect("Failed to initialize browser pool");
21//!
22//! HttpServer::new(move || {
23//! App::new()
24//! .app_data(web::Data::new(pool.clone()))
25//! .configure(html2pdf_api::integrations::actix::configure_routes)
26//! })
27//! .bind("127.0.0.1:8080")?
28//! .run()
29//! .await
30//! }
31//! ```
32//!
33//! This gives you the following endpoints:
34//!
35//! | Method | Path | Description |
36//! |--------|------|-------------|
37//! | GET | `/pdf?url=...` | Convert URL to PDF |
38//! | POST | `/pdf/html` | Convert HTML to PDF |
39//! | GET | `/pool/stats` | Pool statistics |
40//! | GET | `/health` | Health check |
41//! | GET | `/ready` | Readiness check |
42//!
43//! ## Option 2: Mix Pre-built and Custom Handlers
44//!
45//! Use individual pre-built handlers alongside your own:
46//!
47//! ```rust,ignore
48//! use actix_web::{App, HttpServer, web};
49//! use html2pdf_api::prelude::*;
50//! use html2pdf_api::integrations::actix::{pdf_from_url, health_check};
51//!
52//! async fn my_custom_handler() -> impl Responder {
53//! // Your custom logic
54//! }
55//!
56//! #[actix_web::main]
57//! async fn main() -> std::io::Result<()> {
58//! let pool = init_browser_pool().await?;
59//!
60//! HttpServer::new(move || {
61//! App::new()
62//! .app_data(web::Data::new(pool.clone()))
63//! // Pre-built handlers
64//! .route("/pdf", web::get().to(pdf_from_url))
65//! .route("/health", web::get().to(health_check))
66//! // Custom handler
67//! .route("/custom", web::get().to(my_custom_handler))
68//! })
69//! .bind("127.0.0.1:8080")?
70//! .run()
71//! .await
72//! }
73//! ```
74//!
75//! ## Option 3: Custom Handlers with Service Functions
76//!
77//! For full control, use the service functions directly:
78//!
79//! ```rust,ignore
80//! use actix_web::{web, HttpResponse, Responder};
81//! use html2pdf_api::prelude::*;
82//! use html2pdf_api::service::{generate_pdf_from_url, PdfFromUrlRequest};
83//!
84//! async fn my_pdf_handler(
85//! pool: web::Data<SharedBrowserPool>,
86//! query: web::Query<PdfFromUrlRequest>,
87//! ) -> impl Responder {
88//! // Custom pre-processing: auth, rate limiting, logging, etc.
89//! log::info!("Custom handler: {}", query.url);
90//!
91//! let pool = pool.into_inner();
92//! let request = query.into_inner();
93//!
94//! // Call service in blocking context
95//! let result = web::block(move || {
96//! generate_pdf_from_url(&pool, &request)
97//! }).await;
98//!
99//! match result {
100//! Ok(Ok(pdf)) => {
101//! // Custom post-processing
102//! HttpResponse::Ok()
103//! .content_type("application/pdf")
104//! .insert_header(("X-Custom-Header", "value"))
105//! .body(pdf.data)
106//! }
107//! Ok(Err(e)) => HttpResponse::BadRequest().body(e.to_string()),
108//! Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
109//! }
110//! }
111//! ```
112//!
113//! ## Option 4: Full Manual Control (Original Approach)
114//!
115//! For complete control over browser operations:
116//!
117//! ```rust,ignore
118//! use actix_web::{web, HttpResponse, Responder};
119//! use html2pdf_api::prelude::*;
120//!
121//! async fn manual_pdf_handler(
122//! pool: web::Data<SharedBrowserPool>,
123//! ) -> impl Responder {
124//! let pool_guard = match pool.lock() {
125//! Ok(guard) => guard,
126//! Err(e) => return HttpResponse::InternalServerError().body(e.to_string()),
127//! };
128//!
129//! let browser = match pool_guard.get() {
130//! Ok(b) => b,
131//! Err(e) => return HttpResponse::ServiceUnavailable().body(e.to_string()),
132//! };
133//!
134//! let tab = browser.new_tab().unwrap();
135//! tab.navigate_to("https://example.com").unwrap();
136//! tab.wait_until_navigated().unwrap();
137//!
138//! let pdf_data = tab.print_to_pdf(None).unwrap();
139//!
140//! HttpResponse::Ok()
141//! .content_type("application/pdf")
142//! .body(pdf_data)
143//! }
144//! ```
145//!
146//! # Setup
147//!
148//! Add to your `Cargo.toml`:
149//!
150//! ```toml
151//! [dependencies]
152//! html2pdf-api = { version = "0.2", features = ["actix-integration"] }
153//! actix-web = "4"
154//! ```
155//!
156//! # Graceful Shutdown
157//!
158//! For proper cleanup, shutdown the pool when the server stops:
159//!
160//! ```rust,ignore
161//! use actix_web::{App, HttpServer, web};
162//! use html2pdf_api::prelude::*;
163//! use std::sync::Arc;
164//!
165//! #[actix_web::main]
166//! async fn main() -> std::io::Result<()> {
167//! let pool = init_browser_pool().await
168//! .expect("Failed to initialize browser pool");
169//!
170//! // Keep a reference for shutdown
171//! let shutdown_pool = pool.clone();
172//!
173//! let server = HttpServer::new(move || {
174//! App::new()
175//! .app_data(web::Data::new(pool.clone()))
176//! .configure(html2pdf_api::integrations::actix::configure_routes)
177//! })
178//! .bind("127.0.0.1:8080")?
179//! .run();
180//!
181//! // Run server
182//! let result = server.await;
183//!
184//! // Cleanup pool after server stops
185//! if let Ok(mut pool) = shutdown_pool.lock() {
186//! pool.shutdown();
187//! }
188//!
189//! result
190//! }
191//! ```
192//!
193//! # API Reference
194//!
195//! ## Pre-built Handlers
196//!
197//! | Handler | Method | Default Path | Description |
198//! |---------|--------|--------------|-------------|
199//! | [`pdf_from_url`] | GET | `/pdf` | Convert URL to PDF |
200//! | [`pdf_from_html`] | POST | `/pdf/html` | Convert HTML to PDF |
201//! | [`pool_stats`] | GET | `/pool/stats` | Pool statistics |
202//! | [`health_check`] | GET | `/health` | Health check (always 200) |
203//! | [`readiness_check`] | GET | `/ready` | Readiness check (checks pool) |
204//!
205//! ## Type Aliases
206//!
207//! | Type | Description |
208//! |------|-------------|
209//! | [`SharedPool`] | `Arc<Mutex<BrowserPool>>` - for service functions |
210//! | [`BrowserPoolData`] | `web::Data<SharedBrowserPool>` - for handler parameters |
211//!
212//! ## Helper Functions
213//!
214//! | Function | Description |
215//! |----------|-------------|
216//! | [`configure_routes`] | Configure all pre-built routes |
217//! | [`create_pool_data`] | Wrap `SharedBrowserPool` in `web::Data` |
218//! | [`create_pool_data_from_arc`] | Wrap `Arc<Mutex<BrowserPool>>` in `web::Data` |
219//!
220//! ## Extension Traits
221//!
222//! | Trait | Description |
223//! |-------|-------------|
224//! | [`BrowserPoolActixExt`] | Adds `into_actix_data()` to `BrowserPool` |
225
226use actix_web::{http::header, web, HttpResponse, Responder};
227use std::sync::{Arc, Mutex};
228use std::time::Duration;
229
230use crate::pool::BrowserPool;
231use crate::service::{
232 self, ErrorResponse, HealthResponse, PdfFromHtmlRequest, PdfFromUrlRequest,
233 PdfServiceError, DEFAULT_TIMEOUT_SECS,
234};
235use crate::SharedBrowserPool;
236
237// ============================================================================
238// Type Aliases
239// ============================================================================
240
241/// Type alias for shared browser pool.
242///
243/// This is the standard pool type used by the service functions.
244/// It's an `Arc<Mutex<BrowserPool>>` which allows safe sharing across
245/// threads and handlers.
246///
247/// # Usage
248///
249/// ```rust,ignore
250/// use html2pdf_api::integrations::actix::SharedPool;
251///
252/// fn my_function(pool: &SharedPool) {
253/// let guard = pool.lock().unwrap();
254/// let browser = guard.get().unwrap();
255/// // ...
256/// }
257/// ```
258pub type SharedPool = Arc<Mutex<BrowserPool>>;
259
260/// Type alias for Actix-web `Data` wrapper around the shared pool.
261///
262/// Use this type in your handler parameters for automatic extraction:
263///
264/// ```rust,ignore
265/// use html2pdf_api::integrations::actix::BrowserPoolData;
266///
267/// async fn handler(pool: BrowserPoolData) -> impl Responder {
268/// let pool_guard = pool.lock().unwrap();
269/// let browser = pool_guard.get()?;
270/// // ...
271/// }
272/// ```
273///
274/// # Note
275///
276/// `BrowserPoolData` and `web::Data<SharedPool>` are interchangeable.
277/// Use whichever is more convenient for your code.
278pub type BrowserPoolData = web::Data<SharedBrowserPool>;
279
280// ============================================================================
281// Pre-built Handlers
282// ============================================================================
283
284/// Generate PDF from a URL.
285///
286/// This handler converts a web page to PDF using the browser pool.
287///
288/// # Endpoint
289///
290/// ```text
291/// GET /pdf?url=https://example.com&filename=output.pdf
292/// ```
293///
294/// # Query Parameters
295///
296/// | Parameter | Type | Required | Default | Description |
297/// |-----------|------|----------|---------|-------------|
298/// | `url` | string | **Yes** | - | URL to convert (must be valid HTTP/HTTPS) |
299/// | `filename` | string | No | `"document.pdf"` | Output filename |
300/// | `waitsecs` | u64 | No | `5` | Seconds to wait for JavaScript |
301/// | `landscape` | bool | No | `false` | Use landscape orientation |
302/// | `download` | bool | No | `false` | Force download vs inline display |
303/// | `print_background` | bool | No | `true` | Include background graphics |
304///
305/// # Response
306///
307/// ## Success (200 OK)
308///
309/// Returns PDF binary data with headers:
310/// - `Content-Type: application/pdf`
311/// - `Content-Disposition: inline; filename="document.pdf"` (or `attachment` if `download=true`)
312/// - `Cache-Control: no-cache`
313///
314/// ## Errors
315///
316/// | Status | Code | Description |
317/// |--------|------|-------------|
318/// | 400 | `INVALID_URL` | URL is empty or malformed |
319/// | 502 | `NAVIGATION_FAILED` | Failed to load the URL |
320/// | 503 | `BROWSER_UNAVAILABLE` | No browsers available in pool |
321/// | 504 | `TIMEOUT` | Operation timed out |
322///
323/// # Examples
324///
325/// ## Basic Request
326///
327/// ```text
328/// GET /pdf?url=https://example.com
329/// ```
330///
331/// ## With Options
332///
333/// ```text
334/// GET /pdf?url=https://example.com/report&filename=report.pdf&landscape=true&waitsecs=10
335/// ```
336///
337/// ## Force Download
338///
339/// ```text
340/// GET /pdf?url=https://example.com&download=true&filename=download.pdf
341/// ```
342///
343/// # Usage in App
344///
345/// ```rust,ignore
346/// App::new()
347/// .app_data(web::Data::new(pool.clone()))
348/// .route("/pdf", web::get().to(pdf_from_url))
349/// ```
350pub async fn pdf_from_url(
351 pool: web::Data<SharedPool>,
352 query: web::Query<PdfFromUrlRequest>,
353) -> impl Responder {
354 let request = query.into_inner();
355 let pool = pool.into_inner();
356
357 log::debug!("PDF from URL request: {}", request.url);
358
359 // Run blocking PDF generation with timeout
360 let result = tokio::time::timeout(
361 Duration::from_secs(DEFAULT_TIMEOUT_SECS),
362 web::block(move || service::generate_pdf_from_url(&pool, &request)),
363 )
364 .await;
365
366 match result {
367 Ok(Ok(Ok(response))) => build_pdf_response(response),
368 Ok(Ok(Err(e))) => build_error_response(e),
369 Ok(Err(blocking_err)) => {
370 log::error!("Blocking task error: {}", blocking_err);
371 build_error_response(PdfServiceError::Internal(blocking_err.to_string()))
372 }
373 Err(_timeout) => {
374 log::error!(
375 "PDF generation timed out after {} seconds",
376 DEFAULT_TIMEOUT_SECS
377 );
378 build_error_response(PdfServiceError::Timeout(format!(
379 "Operation timed out after {} seconds",
380 DEFAULT_TIMEOUT_SECS
381 )))
382 }
383 }
384}
385
386/// Generate PDF from HTML content.
387///
388/// This handler converts HTML content directly to PDF without requiring
389/// a web server to host the HTML.
390///
391/// # Endpoint
392///
393/// ```text
394/// POST /pdf/html
395/// Content-Type: application/json
396/// ```
397///
398/// # Request Body
399///
400/// ```json
401/// {
402/// "html": "<html><body><h1>Hello World</h1></body></html>",
403/// "filename": "document.pdf",
404/// "waitsecs": 2,
405/// "landscape": false,
406/// "download": false,
407/// "print_background": true
408/// }
409/// ```
410///
411/// | Field | Type | Required | Default | Description |
412/// |-------|------|----------|---------|-------------|
413/// | `html` | string | **Yes** | - | HTML content to convert |
414/// | `filename` | string | No | `"document.pdf"` | Output filename |
415/// | `waitsecs` | u64 | No | `2` | Seconds to wait for JavaScript |
416/// | `landscape` | bool | No | `false` | Use landscape orientation |
417/// | `download` | bool | No | `false` | Force download vs inline display |
418/// | `print_background` | bool | No | `true` | Include background graphics |
419///
420/// # Response
421///
422/// Same as [`pdf_from_url`].
423///
424/// # Errors
425///
426/// | Status | Code | Description |
427/// |--------|------|-------------|
428/// | 400 | `EMPTY_HTML` | HTML content is empty or whitespace |
429/// | 502 | `PDF_GENERATION_FAILED` | Failed to generate PDF |
430/// | 503 | `BROWSER_UNAVAILABLE` | No browsers available |
431/// | 504 | `TIMEOUT` | Operation timed out |
432///
433/// # Example Request
434///
435/// ```bash
436/// curl -X POST http://localhost:8080/pdf/html \
437/// -H "Content-Type: application/json" \
438/// -d '{"html": "<h1>Hello</h1>", "filename": "hello.pdf"}' \
439/// --output hello.pdf
440/// ```
441///
442/// # Usage in App
443///
444/// ```rust,ignore
445/// App::new()
446/// .app_data(web::Data::new(pool.clone()))
447/// .route("/pdf/html", web::post().to(pdf_from_html))
448/// ```
449pub async fn pdf_from_html(
450 pool: web::Data<SharedPool>,
451 body: web::Json<PdfFromHtmlRequest>,
452) -> impl Responder {
453 let request = body.into_inner();
454 let pool = pool.into_inner();
455
456 log::debug!(
457 "PDF from HTML request: {} bytes",
458 request.html.len()
459 );
460
461 let result = tokio::time::timeout(
462 Duration::from_secs(DEFAULT_TIMEOUT_SECS),
463 web::block(move || service::generate_pdf_from_html(&pool, &request)),
464 )
465 .await;
466
467 match result {
468 Ok(Ok(Ok(response))) => build_pdf_response(response),
469 Ok(Ok(Err(e))) => build_error_response(e),
470 Ok(Err(blocking_err)) => {
471 log::error!("Blocking task error: {}", blocking_err);
472 build_error_response(PdfServiceError::Internal(blocking_err.to_string()))
473 }
474 Err(_timeout) => {
475 log::error!("PDF generation timed out");
476 build_error_response(PdfServiceError::Timeout(format!(
477 "Operation timed out after {} seconds",
478 DEFAULT_TIMEOUT_SECS
479 )))
480 }
481 }
482}
483
484/// Get browser pool statistics.
485///
486/// Returns real-time metrics about the browser pool including available
487/// browsers, active browsers, and total count.
488///
489/// # Endpoint
490///
491/// ```text
492/// GET /pool/stats
493/// ```
494///
495/// # Response (200 OK)
496///
497/// ```json
498/// {
499/// "available": 3,
500/// "active": 2,
501/// "total": 5
502/// }
503/// ```
504///
505/// | Field | Type | Description |
506/// |-------|------|-------------|
507/// | `available` | number | Browsers ready to handle requests |
508/// | `active` | number | Browsers currently in use |
509/// | `total` | number | Total browsers (available + active) |
510///
511/// # Errors
512///
513/// | Status | Code | Description |
514/// |--------|------|-------------|
515/// | 500 | `POOL_LOCK_FAILED` | Failed to acquire pool lock |
516///
517/// # Use Cases
518///
519/// - Monitoring dashboards
520/// - Prometheus/Grafana metrics
521/// - Capacity planning
522/// - Debugging pool exhaustion
523///
524/// # Usage in App
525///
526/// ```rust,ignore
527/// App::new()
528/// .app_data(web::Data::new(pool.clone()))
529/// .route("/pool/stats", web::get().to(pool_stats))
530/// ```
531pub async fn pool_stats(pool: web::Data<SharedPool>) -> impl Responder {
532 match service::get_pool_stats(&pool) {
533 Ok(stats) => HttpResponse::Ok().json(stats),
534 Err(e) => build_error_response(e),
535 }
536}
537
538/// Health check endpoint.
539///
540/// Simple endpoint that returns 200 OK if the service is running.
541/// Does not check pool health - use [`readiness_check`] for that.
542///
543/// # Endpoint
544///
545/// ```text
546/// GET /health
547/// ```
548///
549/// # Response (200 OK)
550///
551/// ```json
552/// {
553/// "status": "healthy",
554/// "service": "html2pdf-api"
555/// }
556/// ```
557///
558/// # Use Cases
559///
560/// - Kubernetes liveness probe
561/// - Load balancer health check
562/// - Uptime monitoring
563///
564/// # Kubernetes Example
565///
566/// ```yaml
567/// livenessProbe:
568/// httpGet:
569/// path: /health
570/// port: 8080
571/// initialDelaySeconds: 10
572/// periodSeconds: 30
573/// ```
574///
575/// # Usage in App
576///
577/// ```rust,ignore
578/// App::new()
579/// .route("/health", web::get().to(health_check))
580/// ```
581pub async fn health_check() -> impl Responder {
582 HttpResponse::Ok().json(HealthResponse::default())
583}
584
585/// Readiness check endpoint.
586///
587/// Returns 200 OK if the pool has capacity to handle requests,
588/// 503 Service Unavailable otherwise.
589///
590/// Unlike [`health_check`], this actually checks the pool state.
591///
592/// # Endpoint
593///
594/// ```text
595/// GET /ready
596/// ```
597///
598/// # Response
599///
600/// ## Ready (200 OK)
601///
602/// ```json
603/// {
604/// "status": "ready"
605/// }
606/// ```
607///
608/// ## Not Ready (503 Service Unavailable)
609///
610/// ```json
611/// {
612/// "status": "not_ready",
613/// "reason": "no_available_capacity"
614/// }
615/// ```
616///
617/// # Readiness Criteria
618///
619/// The service is "ready" if either:
620/// - There are idle browsers available (`available > 0`), OR
621/// - There is capacity to create new browsers (`active < max_pool_size`)
622///
623/// # Use Cases
624///
625/// - Kubernetes readiness probe
626/// - Load balancer health check (remove from rotation when busy)
627/// - Auto-scaling triggers
628///
629/// # Kubernetes Example
630///
631/// ```yaml
632/// readinessProbe:
633/// httpGet:
634/// path: /ready
635/// port: 8080
636/// initialDelaySeconds: 5
637/// periodSeconds: 10
638/// ```
639///
640/// # Usage in App
641///
642/// ```rust,ignore
643/// App::new()
644/// .app_data(web::Data::new(pool.clone()))
645/// .route("/ready", web::get().to(readiness_check))
646/// ```
647pub async fn readiness_check(pool: web::Data<SharedPool>) -> impl Responder {
648 match service::is_pool_ready(&pool) {
649 Ok(true) => HttpResponse::Ok().json(serde_json::json!({
650 "status": "ready"
651 })),
652 Ok(false) => HttpResponse::ServiceUnavailable().json(serde_json::json!({
653 "status": "not_ready",
654 "reason": "no_available_capacity"
655 })),
656 Err(e) => HttpResponse::ServiceUnavailable().json(ErrorResponse::from(e)),
657 }
658}
659
660// ============================================================================
661// Route Configuration
662// ============================================================================
663
664/// Configure all PDF routes.
665///
666/// Adds all pre-built handlers to the Actix-web app with default paths.
667/// This is the easiest way to set up the PDF service.
668///
669/// # Routes Added
670///
671/// | Method | Path | Handler | Description |
672/// |--------|------|---------|-------------|
673/// | GET | `/pdf` | [`pdf_from_url`] | Convert URL to PDF |
674/// | POST | `/pdf/html` | [`pdf_from_html`] | Convert HTML to PDF |
675/// | GET | `/pool/stats` | [`pool_stats`] | Pool statistics |
676/// | GET | `/health` | [`health_check`] | Health check |
677/// | GET | `/ready` | [`readiness_check`] | Readiness check |
678///
679/// # Example
680///
681/// ```rust,ignore
682/// use actix_web::{App, HttpServer, web};
683/// use html2pdf_api::prelude::*;
684/// use html2pdf_api::integrations::actix::configure_routes;
685///
686/// #[actix_web::main]
687/// async fn main() -> std::io::Result<()> {
688/// let pool = init_browser_pool().await?;
689///
690/// HttpServer::new(move || {
691/// App::new()
692/// .app_data(web::Data::new(pool.clone()))
693/// .configure(configure_routes)
694/// })
695/// .bind("127.0.0.1:8080")?
696/// .run()
697/// .await
698/// }
699/// ```
700///
701/// # Adding Custom Routes
702///
703/// You can combine `configure_routes` with additional routes:
704///
705/// ```rust,ignore
706/// App::new()
707/// .app_data(web::Data::new(pool.clone()))
708/// .configure(configure_routes) // Pre-built routes
709/// .route("/custom", web::get().to(my_custom_handler)) // Your routes
710/// ```
711///
712/// # Custom Path Prefix
713///
714/// To mount routes under a prefix, use `web::scope`:
715///
716/// ```rust,ignore
717/// App::new()
718/// .app_data(web::Data::new(pool.clone()))
719/// .service(
720/// web::scope("/api/v1")
721/// .configure(configure_routes)
722/// )
723/// // Routes will be: /api/v1/pdf, /api/v1/health, etc.
724/// ```
725pub fn configure_routes(cfg: &mut web::ServiceConfig) {
726 cfg.route("/pdf", web::get().to(pdf_from_url))
727 .route("/pdf/html", web::post().to(pdf_from_html))
728 .route("/pool/stats", web::get().to(pool_stats))
729 .route("/health", web::get().to(health_check))
730 .route("/ready", web::get().to(readiness_check));
731}
732
733// ============================================================================
734// Response Builders (Internal)
735// ============================================================================
736
737/// Build HTTP response for successful PDF generation.
738fn build_pdf_response(response: crate::service::PdfResponse) -> HttpResponse {
739 log::info!(
740 "PDF generated successfully: {} bytes, filename={}",
741 response.size(),
742 response.filename
743 );
744
745 HttpResponse::Ok()
746 .content_type("application/pdf")
747 .insert_header((header::CACHE_CONTROL, "no-cache"))
748 .insert_header((header::CONTENT_DISPOSITION, response.content_disposition()))
749 .body(response.data)
750}
751
752/// Build HTTP response for errors.
753fn build_error_response(error: PdfServiceError) -> HttpResponse {
754 let status_code = error.status_code();
755 let body = ErrorResponse::from(&error);
756
757 log::warn!(
758 "PDF generation error: {} (HTTP {})",
759 error,
760 status_code
761 );
762
763 match status_code {
764 400 => HttpResponse::BadRequest().json(body),
765 502 => HttpResponse::BadGateway().json(body),
766 503 => HttpResponse::ServiceUnavailable().json(body),
767 504 => HttpResponse::GatewayTimeout().json(body),
768 _ => HttpResponse::InternalServerError().json(body),
769 }
770}
771
772// ============================================================================
773// Extension Trait (Backward Compatibility)
774// ============================================================================
775
776/// Extension trait for `BrowserPool` with Actix-web helpers.
777///
778/// Provides convenient methods for integrating with Actix-web.
779///
780/// # Example
781///
782/// ```rust,ignore
783/// use html2pdf_api::integrations::actix::BrowserPoolActixExt;
784///
785/// let pool = BrowserPool::builder()
786/// .factory(Box::new(ChromeBrowserFactory::with_defaults()))
787/// .build()?;
788///
789/// pool.warmup().await?;
790///
791/// // Convert directly to Actix-web Data
792/// let pool_data = pool.into_actix_data();
793///
794/// HttpServer::new(move || {
795/// App::new()
796/// .app_data(pool_data.clone())
797/// .configure(configure_routes)
798/// })
799/// ```
800pub trait BrowserPoolActixExt {
801 /// Convert the pool into Actix-web `Data` wrapper.
802 ///
803 /// This is equivalent to calling `into_shared()` and then wrapping
804 /// with `web::Data::new()`.
805 ///
806 /// # Example
807 ///
808 /// ```rust,ignore
809 /// use html2pdf_api::integrations::actix::BrowserPoolActixExt;
810 ///
811 /// let pool = BrowserPool::builder()
812 /// .factory(Box::new(ChromeBrowserFactory::with_defaults()))
813 /// .build()?;
814 ///
815 /// let pool_data = pool.into_actix_data();
816 ///
817 /// HttpServer::new(move || {
818 /// App::new()
819 /// .app_data(pool_data.clone())
820 /// })
821 /// ```
822 fn into_actix_data(self) -> BrowserPoolData;
823}
824
825impl BrowserPoolActixExt for BrowserPool {
826 fn into_actix_data(self) -> BrowserPoolData {
827 web::Data::new(self.into_shared())
828 }
829}
830
831// ============================================================================
832// Helper Functions (Backward Compatibility)
833// ============================================================================
834
835/// Create Actix-web `Data` from an existing shared pool.
836///
837/// Use this when you already have a `SharedBrowserPool` and want to
838/// wrap it for Actix-web.
839///
840/// # Parameters
841///
842/// * `pool` - The shared browser pool.
843///
844/// # Returns
845///
846/// `BrowserPoolData` ready for use with `App::app_data()`.
847///
848/// # Example
849///
850/// ```rust,ignore
851/// use html2pdf_api::integrations::actix::create_pool_data;
852///
853/// let shared_pool = pool.into_shared();
854/// let pool_data = create_pool_data(shared_pool);
855///
856/// App::new().app_data(pool_data)
857/// ```
858pub fn create_pool_data(pool: SharedBrowserPool) -> BrowserPoolData {
859 web::Data::new(pool)
860}
861
862/// Create Actix-web `Data` from an `Arc` reference.
863///
864/// Use this when you need to keep a reference to the pool for shutdown.
865///
866/// # Parameters
867///
868/// * `pool` - Arc reference to the shared browser pool.
869///
870/// # Returns
871///
872/// `BrowserPoolData` ready for use with `App::app_data()`.
873///
874/// # Example
875///
876/// ```rust,ignore
877/// use html2pdf_api::integrations::actix::create_pool_data_from_arc;
878///
879/// let shared_pool = pool.into_shared();
880/// let pool_for_shutdown = Arc::clone(&shared_pool);
881/// let pool_data = create_pool_data_from_arc(shared_pool);
882///
883/// // Use pool_data in App
884/// // Use pool_for_shutdown for cleanup
885/// ```
886pub fn create_pool_data_from_arc(pool: Arc<std::sync::Mutex<BrowserPool>>) -> BrowserPoolData {
887 web::Data::new(pool)
888}
889
890// ============================================================================
891// Tests
892// ============================================================================
893
894#[cfg(test)]
895mod tests {
896 use super::*;
897
898 #[test]
899 fn test_type_alias_compiles() {
900 // Verify the type alias is valid
901 fn _accepts_pool_data(_: BrowserPoolData) {}
902 fn _accepts_shared_pool(_: SharedPool) {}
903 }
904
905 #[test]
906 fn test_shared_pool_type_matches() {
907 // SharedPool and SharedBrowserPool should be compatible
908 fn _takes_shared_pool(_: SharedPool) {}
909 fn _returns_shared_browser_pool() -> SharedBrowserPool {
910 Arc::new(std::sync::Mutex::new(
911 BrowserPool::builder()
912 .factory(Box::new(crate::factory::mock::MockBrowserFactory::new()))
913 .build()
914 .unwrap(),
915 ))
916 }
917
918 // This should compile, proving type compatibility
919 let pool: SharedBrowserPool = _returns_shared_browser_pool();
920 let _: SharedPool = pool;
921 }
922}