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::{HttpResponse, Responder, http::header, web};
227use std::sync::{Arc, Mutex};
228use std::time::Duration;
229
230use crate::SharedBrowserPool;
231use crate::pool::BrowserPool;
232use crate::service::{
233 self, DEFAULT_TIMEOUT_SECS, ErrorResponse, HealthResponse, PdfFromHtmlRequest,
234 PdfFromUrlRequest, PdfServiceError,
235};
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!("PDF from HTML request: {} bytes", request.html.len());
457
458 let result = tokio::time::timeout(
459 Duration::from_secs(DEFAULT_TIMEOUT_SECS),
460 web::block(move || service::generate_pdf_from_html(&pool, &request)),
461 )
462 .await;
463
464 match result {
465 Ok(Ok(Ok(response))) => build_pdf_response(response),
466 Ok(Ok(Err(e))) => build_error_response(e),
467 Ok(Err(blocking_err)) => {
468 log::error!("Blocking task error: {}", blocking_err);
469 build_error_response(PdfServiceError::Internal(blocking_err.to_string()))
470 }
471 Err(_timeout) => {
472 log::error!("PDF generation timed out");
473 build_error_response(PdfServiceError::Timeout(format!(
474 "Operation timed out after {} seconds",
475 DEFAULT_TIMEOUT_SECS
476 )))
477 }
478 }
479}
480
481/// Get browser pool statistics.
482///
483/// Returns real-time metrics about the browser pool including available
484/// browsers, active browsers, and total count.
485///
486/// # Endpoint
487///
488/// ```text
489/// GET /pool/stats
490/// ```
491///
492/// # Response (200 OK)
493///
494/// ```json
495/// {
496/// "available": 3,
497/// "active": 2,
498/// "total": 5
499/// }
500/// ```
501///
502/// | Field | Type | Description |
503/// |-------|------|-------------|
504/// | `available` | number | Browsers ready to handle requests |
505/// | `active` | number | Browsers currently in use |
506/// | `total` | number | Total browsers (available + active) |
507///
508/// # Errors
509///
510/// | Status | Code | Description |
511/// |--------|------|-------------|
512/// | 500 | `POOL_LOCK_FAILED` | Failed to acquire pool lock |
513///
514/// # Use Cases
515///
516/// - Monitoring dashboards
517/// - Prometheus/Grafana metrics
518/// - Capacity planning
519/// - Debugging pool exhaustion
520///
521/// # Usage in App
522///
523/// ```rust,ignore
524/// App::new()
525/// .app_data(web::Data::new(pool.clone()))
526/// .route("/pool/stats", web::get().to(pool_stats))
527/// ```
528pub async fn pool_stats(pool: web::Data<SharedPool>) -> impl Responder {
529 match service::get_pool_stats(&pool) {
530 Ok(stats) => HttpResponse::Ok().json(stats),
531 Err(e) => build_error_response(e),
532 }
533}
534
535/// Health check endpoint.
536///
537/// Simple endpoint that returns 200 OK if the service is running.
538/// Does not check pool health - use [`readiness_check`] for that.
539///
540/// # Endpoint
541///
542/// ```text
543/// GET /health
544/// ```
545///
546/// # Response (200 OK)
547///
548/// ```json
549/// {
550/// "status": "healthy",
551/// "service": "html2pdf-api"
552/// }
553/// ```
554///
555/// # Use Cases
556///
557/// - Kubernetes liveness probe
558/// - Load balancer health check
559/// - Uptime monitoring
560///
561/// # Kubernetes Example
562///
563/// ```yaml
564/// livenessProbe:
565/// httpGet:
566/// path: /health
567/// port: 8080
568/// initialDelaySeconds: 10
569/// periodSeconds: 30
570/// ```
571///
572/// # Usage in App
573///
574/// ```rust,ignore
575/// App::new()
576/// .route("/health", web::get().to(health_check))
577/// ```
578pub async fn health_check() -> impl Responder {
579 HttpResponse::Ok().json(HealthResponse::default())
580}
581
582/// Readiness check endpoint.
583///
584/// Returns 200 OK if the pool has capacity to handle requests,
585/// 503 Service Unavailable otherwise.
586///
587/// Unlike [`health_check`], this actually checks the pool state.
588///
589/// # Endpoint
590///
591/// ```text
592/// GET /ready
593/// ```
594///
595/// # Response
596///
597/// ## Ready (200 OK)
598///
599/// ```json
600/// {
601/// "status": "ready"
602/// }
603/// ```
604///
605/// ## Not Ready (503 Service Unavailable)
606///
607/// ```json
608/// {
609/// "status": "not_ready",
610/// "reason": "no_available_capacity"
611/// }
612/// ```
613///
614/// # Readiness Criteria
615///
616/// The service is "ready" if either:
617/// - There are idle browsers available (`available > 0`), OR
618/// - There is capacity to create new browsers (`active < max_pool_size`)
619///
620/// # Use Cases
621///
622/// - Kubernetes readiness probe
623/// - Load balancer health check (remove from rotation when busy)
624/// - Auto-scaling triggers
625///
626/// # Kubernetes Example
627///
628/// ```yaml
629/// readinessProbe:
630/// httpGet:
631/// path: /ready
632/// port: 8080
633/// initialDelaySeconds: 5
634/// periodSeconds: 10
635/// ```
636///
637/// # Usage in App
638///
639/// ```rust,ignore
640/// App::new()
641/// .app_data(web::Data::new(pool.clone()))
642/// .route("/ready", web::get().to(readiness_check))
643/// ```
644pub async fn readiness_check(pool: web::Data<SharedPool>) -> impl Responder {
645 match service::is_pool_ready(&pool) {
646 Ok(true) => HttpResponse::Ok().json(serde_json::json!({
647 "status": "ready"
648 })),
649 Ok(false) => HttpResponse::ServiceUnavailable().json(serde_json::json!({
650 "status": "not_ready",
651 "reason": "no_available_capacity"
652 })),
653 Err(e) => HttpResponse::ServiceUnavailable().json(ErrorResponse::from(e)),
654 }
655}
656
657// ============================================================================
658// Route Configuration
659// ============================================================================
660
661/// Configure all PDF routes.
662///
663/// Adds all pre-built handlers to the Actix-web app with default paths.
664/// This is the easiest way to set up the PDF service.
665///
666/// # Routes Added
667///
668/// | Method | Path | Handler | Description |
669/// |--------|------|---------|-------------|
670/// | GET | `/pdf` | [`pdf_from_url`] | Convert URL to PDF |
671/// | POST | `/pdf/html` | [`pdf_from_html`] | Convert HTML to PDF |
672/// | GET | `/pool/stats` | [`pool_stats`] | Pool statistics |
673/// | GET | `/health` | [`health_check`] | Health check |
674/// | GET | `/ready` | [`readiness_check`] | Readiness check |
675///
676/// # Example
677///
678/// ```rust,ignore
679/// use actix_web::{App, HttpServer, web};
680/// use html2pdf_api::prelude::*;
681/// use html2pdf_api::integrations::actix::configure_routes;
682///
683/// #[actix_web::main]
684/// async fn main() -> std::io::Result<()> {
685/// let pool = init_browser_pool().await?;
686///
687/// HttpServer::new(move || {
688/// App::new()
689/// .app_data(web::Data::new(pool.clone()))
690/// .configure(configure_routes)
691/// })
692/// .bind("127.0.0.1:8080")?
693/// .run()
694/// .await
695/// }
696/// ```
697///
698/// # Adding Custom Routes
699///
700/// You can combine `configure_routes` with additional routes:
701///
702/// ```rust,ignore
703/// App::new()
704/// .app_data(web::Data::new(pool.clone()))
705/// .configure(configure_routes) // Pre-built routes
706/// .route("/custom", web::get().to(my_custom_handler)) // Your routes
707/// ```
708///
709/// # Custom Path Prefix
710///
711/// To mount routes under a prefix, use `web::scope`:
712///
713/// ```rust,ignore
714/// App::new()
715/// .app_data(web::Data::new(pool.clone()))
716/// .service(
717/// web::scope("/api/v1")
718/// .configure(configure_routes)
719/// )
720/// // Routes will be: /api/v1/pdf, /api/v1/health, etc.
721/// ```
722pub fn configure_routes(cfg: &mut web::ServiceConfig) {
723 cfg.route("/pdf", web::get().to(pdf_from_url))
724 .route("/pdf/html", web::post().to(pdf_from_html))
725 .route("/pool/stats", web::get().to(pool_stats))
726 .route("/health", web::get().to(health_check))
727 .route("/ready", web::get().to(readiness_check));
728}
729
730// ============================================================================
731// Response Builders (Internal)
732// ============================================================================
733
734/// Build HTTP response for successful PDF generation.
735fn build_pdf_response(response: crate::service::PdfResponse) -> HttpResponse {
736 log::info!(
737 "PDF generated successfully: {} bytes, filename={}",
738 response.size(),
739 response.filename
740 );
741
742 HttpResponse::Ok()
743 .content_type("application/pdf")
744 .insert_header((header::CACHE_CONTROL, "no-cache"))
745 .insert_header((header::CONTENT_DISPOSITION, response.content_disposition()))
746 .body(response.data)
747}
748
749/// Build HTTP response for errors.
750fn build_error_response(error: PdfServiceError) -> HttpResponse {
751 let status_code = error.status_code();
752 let body = ErrorResponse::from(&error);
753
754 log::warn!("PDF generation error: {} (HTTP {})", error, status_code);
755
756 match status_code {
757 400 => HttpResponse::BadRequest().json(body),
758 502 => HttpResponse::BadGateway().json(body),
759 503 => HttpResponse::ServiceUnavailable().json(body),
760 504 => HttpResponse::GatewayTimeout().json(body),
761 _ => HttpResponse::InternalServerError().json(body),
762 }
763}
764
765// ============================================================================
766// Extension Trait (Backward Compatibility)
767// ============================================================================
768
769/// Extension trait for `BrowserPool` with Actix-web helpers.
770///
771/// Provides convenient methods for integrating with Actix-web.
772///
773/// # Example
774///
775/// ```rust,ignore
776/// use html2pdf_api::integrations::actix::BrowserPoolActixExt;
777///
778/// let pool = BrowserPool::builder()
779/// .factory(Box::new(ChromeBrowserFactory::with_defaults()))
780/// .build()?;
781///
782/// pool.warmup().await?;
783///
784/// // Convert directly to Actix-web Data
785/// let pool_data = pool.into_actix_data();
786///
787/// HttpServer::new(move || {
788/// App::new()
789/// .app_data(pool_data.clone())
790/// .configure(configure_routes)
791/// })
792/// ```
793pub trait BrowserPoolActixExt {
794 /// Convert the pool into Actix-web `Data` wrapper.
795 ///
796 /// This is equivalent to calling `into_shared()` and then wrapping
797 /// with `web::Data::new()`.
798 ///
799 /// # Example
800 ///
801 /// ```rust,ignore
802 /// use html2pdf_api::integrations::actix::BrowserPoolActixExt;
803 ///
804 /// let pool = BrowserPool::builder()
805 /// .factory(Box::new(ChromeBrowserFactory::with_defaults()))
806 /// .build()?;
807 ///
808 /// let pool_data = pool.into_actix_data();
809 ///
810 /// HttpServer::new(move || {
811 /// App::new()
812 /// .app_data(pool_data.clone())
813 /// })
814 /// ```
815 fn into_actix_data(self) -> BrowserPoolData;
816}
817
818impl BrowserPoolActixExt for BrowserPool {
819 fn into_actix_data(self) -> BrowserPoolData {
820 web::Data::new(self.into_shared())
821 }
822}
823
824// ============================================================================
825// Helper Functions (Backward Compatibility)
826// ============================================================================
827
828/// Create Actix-web `Data` from an existing shared pool.
829///
830/// Use this when you already have a `SharedBrowserPool` and want to
831/// wrap it for Actix-web.
832///
833/// # Parameters
834///
835/// * `pool` - The shared browser pool.
836///
837/// # Returns
838///
839/// `BrowserPoolData` ready for use with `App::app_data()`.
840///
841/// # Example
842///
843/// ```rust,ignore
844/// use html2pdf_api::integrations::actix::create_pool_data;
845///
846/// let shared_pool = pool.into_shared();
847/// let pool_data = create_pool_data(shared_pool);
848///
849/// App::new().app_data(pool_data)
850/// ```
851pub fn create_pool_data(pool: SharedBrowserPool) -> BrowserPoolData {
852 web::Data::new(pool)
853}
854
855/// Create Actix-web `Data` from an `Arc` reference.
856///
857/// Use this when you need to keep a reference to the pool for shutdown.
858///
859/// # Parameters
860///
861/// * `pool` - Arc reference to the shared browser pool.
862///
863/// # Returns
864///
865/// `BrowserPoolData` ready for use with `App::app_data()`.
866///
867/// # Example
868///
869/// ```rust,ignore
870/// use html2pdf_api::integrations::actix::create_pool_data_from_arc;
871///
872/// let shared_pool = pool.into_shared();
873/// let pool_for_shutdown = Arc::clone(&shared_pool);
874/// let pool_data = create_pool_data_from_arc(shared_pool);
875///
876/// // Use pool_data in App
877/// // Use pool_for_shutdown for cleanup
878/// ```
879pub fn create_pool_data_from_arc(pool: Arc<std::sync::Mutex<BrowserPool>>) -> BrowserPoolData {
880 web::Data::new(pool)
881}
882
883// ============================================================================
884// Tests
885// ============================================================================
886
887#[cfg(test)]
888mod tests {
889 use super::*;
890
891 #[test]
892 fn test_type_alias_compiles() {
893 // Verify the type alias is valid
894 fn _accepts_pool_data(_: BrowserPoolData) {}
895 fn _accepts_shared_pool(_: SharedPool) {}
896 }
897
898 #[tokio::test]
899 async fn test_shared_pool_type_matches() {
900 // SharedPool and SharedBrowserPool should be compatible
901 fn _takes_shared_pool(_: SharedPool) {}
902 fn _returns_shared_browser_pool() -> SharedBrowserPool {
903 Arc::new(std::sync::Mutex::new(
904 BrowserPool::builder()
905 .factory(Box::new(crate::factory::mock::MockBrowserFactory::new()))
906 .build()
907 .unwrap(),
908 ))
909 }
910
911 // This should compile, proving type compatibility
912 let pool: SharedBrowserPool = _returns_shared_browser_pool();
913 let _: SharedPool = pool;
914 }
915}