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}