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;
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<BrowserPool>` which allows safe sharing across
245/// threads and handlers. No outer `Mutex` needed — the pool uses
246/// fine-grained internal locks.
247///
248/// # Usage
249///
250/// ```rust,ignore
251/// use html2pdf_api::integrations::actix::SharedPool;
252///
253/// fn my_function(pool: &SharedPool) {
254/// let browser = pool.get().unwrap();
255/// // ...
256/// }
257/// ```
258pub type SharedPool = Arc<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 browser = pool.get().unwrap();
269/// // ...
270/// }
271/// ```
272///
273/// # Note
274///
275/// `BrowserPoolData` and `web::Data<SharedPool>` are interchangeable.
276/// Use whichever is more convenient for your code.
277pub type BrowserPoolData = web::Data<SharedBrowserPool>;
278
279// ============================================================================
280// Pre-built Handlers
281// ============================================================================
282
283/// Generate PDF from a URL.
284///
285/// This handler converts a web page to PDF using the browser pool.
286///
287/// # Endpoint
288///
289/// ```text
290/// GET /pdf?url=https://example.com&filename=output.pdf
291/// ```
292///
293/// # Query Parameters
294///
295/// | Parameter | Type | Required | Default | Description |
296/// |-----------|------|----------|---------|-------------|
297/// | `url` | string | **Yes** | - | URL to convert (must be valid HTTP/HTTPS) |
298/// | `filename` | string | No | `"document.pdf"` | Output filename |
299/// | `waitsecs` | u64 | No | `5` | Seconds to wait for JavaScript |
300/// | `landscape` | bool | No | `false` | Use landscape orientation |
301/// | `download` | bool | No | `false` | Force download vs inline display |
302/// | `print_background` | bool | No | `true` | Include background graphics |
303///
304/// # Response
305///
306/// ## Success (200 OK)
307///
308/// Returns PDF binary data with headers:
309/// - `Content-Type: application/pdf`
310/// - `Content-Disposition: inline; filename="document.pdf"` (or `attachment` if `download=true`)
311/// - `Cache-Control: no-cache`
312///
313/// ## Errors
314///
315/// | Status | Code | Description |
316/// |--------|------|-------------|
317/// | 400 | `INVALID_URL` | URL is empty or malformed |
318/// | 502 | `NAVIGATION_FAILED` | Failed to load the URL |
319/// | 503 | `BROWSER_UNAVAILABLE` | No browsers available in pool |
320/// | 504 | `TIMEOUT` | Operation timed out |
321///
322/// # Examples
323///
324/// ## Basic Request
325///
326/// ```text
327/// GET /pdf?url=https://example.com
328/// ```
329///
330/// ## With Options
331///
332/// ```text
333/// GET /pdf?url=https://example.com/report&filename=report.pdf&landscape=true&waitsecs=10
334/// ```
335///
336/// ## Force Download
337///
338/// ```text
339/// GET /pdf?url=https://example.com&download=true&filename=download.pdf
340/// ```
341///
342/// # Usage in App
343///
344/// ```rust,ignore
345/// App::new()
346/// .app_data(web::Data::new(pool.clone()))
347/// .route("/pdf", web::get().to(pdf_from_url))
348/// ```
349pub async fn pdf_from_url(
350 pool: web::Data<SharedPool>,
351 query: web::Query<PdfFromUrlRequest>,
352) -> impl Responder {
353 let request = query.into_inner();
354 let pool = pool.into_inner();
355
356 log::debug!("PDF from URL request: {}", request.url);
357
358 // Run blocking PDF generation with timeout
359 let result = tokio::time::timeout(
360 Duration::from_secs(DEFAULT_TIMEOUT_SECS),
361 web::block(move || service::generate_pdf_from_url(&pool, &request)),
362 )
363 .await;
364
365 match result {
366 Ok(Ok(Ok(response))) => build_pdf_response(response),
367 Ok(Ok(Err(e))) => build_error_response(e),
368 Ok(Err(blocking_err)) => {
369 log::error!("Blocking task error: {}", blocking_err);
370 build_error_response(PdfServiceError::Internal(blocking_err.to_string()))
371 }
372 Err(_timeout) => {
373 log::error!(
374 "PDF generation timed out after {} seconds",
375 DEFAULT_TIMEOUT_SECS
376 );
377 build_error_response(PdfServiceError::Timeout(format!(
378 "Operation timed out after {} seconds",
379 DEFAULT_TIMEOUT_SECS
380 )))
381 }
382 }
383}
384
385/// Generate PDF from HTML content.
386///
387/// This handler converts HTML content directly to PDF without requiring
388/// a web server to host the HTML.
389///
390/// # Endpoint
391///
392/// ```text
393/// POST /pdf/html
394/// Content-Type: application/json
395/// ```
396///
397/// # Request Body
398///
399/// ```json
400/// {
401/// "html": "<html><body><h1>Hello World</h1></body></html>",
402/// "filename": "document.pdf",
403/// "waitsecs": 2,
404/// "landscape": false,
405/// "download": false,
406/// "print_background": true
407/// }
408/// ```
409///
410/// | Field | Type | Required | Default | Description |
411/// |-------|------|----------|---------|-------------|
412/// | `html` | string | **Yes** | - | HTML content to convert |
413/// | `filename` | string | No | `"document.pdf"` | Output filename |
414/// | `waitsecs` | u64 | No | `2` | Seconds to wait for JavaScript |
415/// | `landscape` | bool | No | `false` | Use landscape orientation |
416/// | `download` | bool | No | `false` | Force download vs inline display |
417/// | `print_background` | bool | No | `true` | Include background graphics |
418///
419/// # Response
420///
421/// Same as [`pdf_from_url`].
422///
423/// # Errors
424///
425/// | Status | Code | Description |
426/// |--------|------|-------------|
427/// | 400 | `EMPTY_HTML` | HTML content is empty or whitespace |
428/// | 502 | `PDF_GENERATION_FAILED` | Failed to generate PDF |
429/// | 503 | `BROWSER_UNAVAILABLE` | No browsers available |
430/// | 504 | `TIMEOUT` | Operation timed out |
431///
432/// # Example Request
433///
434/// ```bash
435/// curl -X POST http://localhost:8080/pdf/html \
436/// -H "Content-Type: application/json" \
437/// -d '{"html": "<h1>Hello</h1>", "filename": "hello.pdf"}' \
438/// --output hello.pdf
439/// ```
440///
441/// # Usage in App
442///
443/// ```rust,ignore
444/// App::new()
445/// .app_data(web::Data::new(pool.clone()))
446/// .route("/pdf/html", web::post().to(pdf_from_html))
447/// ```
448pub async fn pdf_from_html(
449 pool: web::Data<SharedPool>,
450 body: web::Json<PdfFromHtmlRequest>,
451) -> impl Responder {
452 let request = body.into_inner();
453 let pool = pool.into_inner();
454
455 log::debug!("PDF from HTML request: {} bytes", request.html.len());
456
457 let result = tokio::time::timeout(
458 Duration::from_secs(DEFAULT_TIMEOUT_SECS),
459 web::block(move || service::generate_pdf_from_html(&pool, &request)),
460 )
461 .await;
462
463 match result {
464 Ok(Ok(Ok(response))) => build_pdf_response(response),
465 Ok(Ok(Err(e))) => build_error_response(e),
466 Ok(Err(blocking_err)) => {
467 log::error!("Blocking task error: {}", blocking_err);
468 build_error_response(PdfServiceError::Internal(blocking_err.to_string()))
469 }
470 Err(_timeout) => {
471 log::error!("PDF generation timed out");
472 build_error_response(PdfServiceError::Timeout(format!(
473 "Operation timed out after {} seconds",
474 DEFAULT_TIMEOUT_SECS
475 )))
476 }
477 }
478}
479
480/// Get browser pool statistics.
481///
482/// Returns real-time metrics about the browser pool including available
483/// browsers, active browsers, and total count.
484///
485/// # Endpoint
486///
487/// ```text
488/// GET /pool/stats
489/// ```
490///
491/// # Response (200 OK)
492///
493/// ```json
494/// {
495/// "available": 3,
496/// "active": 2,
497/// "total": 5
498/// }
499/// ```
500///
501/// | Field | Type | Description |
502/// |-------|------|-------------|
503/// | `available` | number | Browsers ready to handle requests |
504/// | `active` | number | Browsers currently in use |
505/// | `total` | number | Total browsers (available + active) |
506///
507/// # Errors
508///
509/// | Status | Code | Description |
510/// |--------|------|-------------|
511/// | 500 | `POOL_LOCK_FAILED` | Failed to acquire pool lock |
512///
513/// # Use Cases
514///
515/// - Monitoring dashboards
516/// - Prometheus/Grafana metrics
517/// - Capacity planning
518/// - Debugging pool exhaustion
519///
520/// # Usage in App
521///
522/// ```rust,ignore
523/// App::new()
524/// .app_data(web::Data::new(pool.clone()))
525/// .route("/pool/stats", web::get().to(pool_stats))
526/// ```
527pub async fn pool_stats(pool: web::Data<SharedPool>) -> impl Responder {
528 match service::get_pool_stats(&pool) {
529 Ok(stats) => HttpResponse::Ok().json(stats),
530 Err(e) => build_error_response(e),
531 }
532}
533
534/// Health check endpoint.
535///
536/// Simple endpoint that returns 200 OK if the service is running.
537/// Does not check pool health - use [`readiness_check`] for that.
538///
539/// # Endpoint
540///
541/// ```text
542/// GET /health
543/// ```
544///
545/// # Response (200 OK)
546///
547/// ```json
548/// {
549/// "status": "healthy",
550/// "service": "html2pdf-api"
551/// }
552/// ```
553///
554/// # Use Cases
555///
556/// - Kubernetes liveness probe
557/// - Load balancer health check
558/// - Uptime monitoring
559///
560/// # Kubernetes Example
561///
562/// ```yaml
563/// livenessProbe:
564/// httpGet:
565/// path: /health
566/// port: 8080
567/// initialDelaySeconds: 10
568/// periodSeconds: 30
569/// ```
570///
571/// # Usage in App
572///
573/// ```rust,ignore
574/// App::new()
575/// .route("/health", web::get().to(health_check))
576/// ```
577pub async fn health_check() -> impl Responder {
578 HttpResponse::Ok().json(HealthResponse::default())
579}
580
581/// Readiness check endpoint.
582///
583/// Returns 200 OK if the pool has capacity to handle requests,
584/// 503 Service Unavailable otherwise.
585///
586/// Unlike [`health_check`], this actually checks the pool state.
587///
588/// # Endpoint
589///
590/// ```text
591/// GET /ready
592/// ```
593///
594/// # Response
595///
596/// ## Ready (200 OK)
597///
598/// ```json
599/// {
600/// "status": "ready"
601/// }
602/// ```
603///
604/// ## Not Ready (503 Service Unavailable)
605///
606/// ```json
607/// {
608/// "status": "not_ready",
609/// "reason": "no_available_capacity"
610/// }
611/// ```
612///
613/// # Readiness Criteria
614///
615/// The service is "ready" if either:
616/// - There are idle browsers available (`available > 0`), OR
617/// - There is capacity to create new browsers (`active < max_pool_size`)
618///
619/// # Use Cases
620///
621/// - Kubernetes readiness probe
622/// - Load balancer health check (remove from rotation when busy)
623/// - Auto-scaling triggers
624///
625/// # Kubernetes Example
626///
627/// ```yaml
628/// readinessProbe:
629/// httpGet:
630/// path: /ready
631/// port: 8080
632/// initialDelaySeconds: 5
633/// periodSeconds: 10
634/// ```
635///
636/// # Usage in App
637///
638/// ```rust,ignore
639/// App::new()
640/// .app_data(web::Data::new(pool.clone()))
641/// .route("/ready", web::get().to(readiness_check))
642/// ```
643pub async fn readiness_check(pool: web::Data<SharedPool>) -> impl Responder {
644 match service::is_pool_ready(&pool) {
645 Ok(true) => HttpResponse::Ok().json(serde_json::json!({
646 "status": "ready"
647 })),
648 Ok(false) => HttpResponse::ServiceUnavailable().json(serde_json::json!({
649 "status": "not_ready",
650 "reason": "no_available_capacity"
651 })),
652 Err(e) => HttpResponse::ServiceUnavailable().json(ErrorResponse::from(e)),
653 }
654}
655
656// ============================================================================
657// Route Configuration
658// ============================================================================
659
660/// Configure all PDF routes.
661///
662/// Adds all pre-built handlers to the Actix-web app with default paths.
663/// This is the easiest way to set up the PDF service.
664///
665/// # Routes Added
666///
667/// | Method | Path | Handler | Description |
668/// |--------|------|---------|-------------|
669/// | GET | `/pdf` | [`pdf_from_url`] | Convert URL to PDF |
670/// | POST | `/pdf/html` | [`pdf_from_html`] | Convert HTML to PDF |
671/// | GET | `/pool/stats` | [`pool_stats`] | Pool statistics |
672/// | GET | `/health` | [`health_check`] | Health check |
673/// | GET | `/ready` | [`readiness_check`] | Readiness check |
674///
675/// # Example
676///
677/// ```rust,ignore
678/// use actix_web::{App, HttpServer, web};
679/// use html2pdf_api::prelude::*;
680/// use html2pdf_api::integrations::actix::configure_routes;
681///
682/// #[actix_web::main]
683/// async fn main() -> std::io::Result<()> {
684/// let pool = init_browser_pool().await?;
685///
686/// HttpServer::new(move || {
687/// App::new()
688/// .app_data(web::Data::new(pool.clone()))
689/// .configure(configure_routes)
690/// })
691/// .bind("127.0.0.1:8080")?
692/// .run()
693/// .await
694/// }
695/// ```
696///
697/// # Adding Custom Routes
698///
699/// You can combine `configure_routes` with additional routes:
700///
701/// ```rust,ignore
702/// App::new()
703/// .app_data(web::Data::new(pool.clone()))
704/// .configure(configure_routes) // Pre-built routes
705/// .route("/custom", web::get().to(my_custom_handler)) // Your routes
706/// ```
707///
708/// # Custom Path Prefix
709///
710/// To mount routes under a prefix, use `web::scope`:
711///
712/// ```rust,ignore
713/// App::new()
714/// .app_data(web::Data::new(pool.clone()))
715/// .service(
716/// web::scope("/api/v1")
717/// .configure(configure_routes)
718/// )
719/// // Routes will be: /api/v1/pdf, /api/v1/health, etc.
720/// ```
721pub fn configure_routes(cfg: &mut web::ServiceConfig) {
722 cfg.route("/pdf", web::get().to(pdf_from_url))
723 .route("/pdf/html", web::post().to(pdf_from_html))
724 .route("/pool/stats", web::get().to(pool_stats))
725 .route("/health", web::get().to(health_check))
726 .route("/ready", web::get().to(readiness_check));
727}
728
729// ============================================================================
730// Response Builders (Internal)
731// ============================================================================
732
733/// Build HTTP response for successful PDF generation.
734fn build_pdf_response(response: crate::service::PdfResponse) -> HttpResponse {
735 log::info!(
736 "PDF generated successfully: {} bytes, filename={}",
737 response.size(),
738 response.filename
739 );
740
741 HttpResponse::Ok()
742 .content_type("application/pdf")
743 .insert_header((header::CACHE_CONTROL, "no-cache"))
744 .insert_header((header::CONTENT_DISPOSITION, response.content_disposition()))
745 .body(response.data)
746}
747
748/// Build HTTP response for errors.
749fn build_error_response(error: PdfServiceError) -> HttpResponse {
750 let status_code = error.status_code();
751 let body = ErrorResponse::from(&error);
752
753 log::warn!("PDF generation error: {} (HTTP {})", error, status_code);
754
755 match status_code {
756 400 => HttpResponse::BadRequest().json(body),
757 502 => HttpResponse::BadGateway().json(body),
758 503 => HttpResponse::ServiceUnavailable().json(body),
759 504 => HttpResponse::GatewayTimeout().json(body),
760 _ => HttpResponse::InternalServerError().json(body),
761 }
762}
763
764// ============================================================================
765// Extension Trait (Backward Compatibility)
766// ============================================================================
767
768/// Extension trait for `BrowserPool` with Actix-web helpers.
769///
770/// Provides convenient methods for integrating with Actix-web.
771///
772/// # Example
773///
774/// ```rust,ignore
775/// use html2pdf_api::integrations::actix::BrowserPoolActixExt;
776///
777/// let pool = BrowserPool::builder()
778/// .factory(Box::new(ChromeBrowserFactory::with_defaults()))
779/// .build()?;
780///
781/// pool.warmup().await?;
782///
783/// // Convert directly to Actix-web Data
784/// let pool_data = pool.into_actix_data();
785///
786/// HttpServer::new(move || {
787/// App::new()
788/// .app_data(pool_data.clone())
789/// .configure(configure_routes)
790/// })
791/// ```
792pub trait BrowserPoolActixExt {
793 /// Convert the pool into Actix-web `Data` wrapper.
794 ///
795 /// This is equivalent to calling `into_shared()` and then wrapping
796 /// with `web::Data::new()`.
797 ///
798 /// # Example
799 ///
800 /// ```rust,ignore
801 /// use html2pdf_api::integrations::actix::BrowserPoolActixExt;
802 ///
803 /// let pool = BrowserPool::builder()
804 /// .factory(Box::new(ChromeBrowserFactory::with_defaults()))
805 /// .build()?;
806 ///
807 /// let pool_data = pool.into_actix_data();
808 ///
809 /// HttpServer::new(move || {
810 /// App::new()
811 /// .app_data(pool_data.clone())
812 /// })
813 /// ```
814 fn into_actix_data(self) -> BrowserPoolData;
815}
816
817impl BrowserPoolActixExt for BrowserPool {
818 fn into_actix_data(self) -> BrowserPoolData {
819 web::Data::new(self.into_shared())
820 }
821}
822
823// ============================================================================
824// Helper Functions (Backward Compatibility)
825// ============================================================================
826
827/// Create Actix-web `Data` from an existing shared pool.
828///
829/// Use this when you already have a `SharedBrowserPool` and want to
830/// wrap it for Actix-web.
831///
832/// # Parameters
833///
834/// * `pool` - The shared browser pool.
835///
836/// # Returns
837///
838/// `BrowserPoolData` ready for use with `App::app_data()`.
839///
840/// # Example
841///
842/// ```rust,ignore
843/// use html2pdf_api::integrations::actix::create_pool_data;
844///
845/// let shared_pool = pool.into_shared();
846/// let pool_data = create_pool_data(shared_pool);
847///
848/// App::new().app_data(pool_data)
849/// ```
850pub fn create_pool_data(pool: SharedBrowserPool) -> BrowserPoolData {
851 web::Data::new(pool)
852}
853
854/// Create Actix-web `Data` from an `Arc` reference.
855///
856/// Use this when you need to keep a reference to the pool for shutdown.
857///
858/// # Parameters
859///
860/// * `pool` - Arc reference to the shared browser pool.
861///
862/// # Returns
863///
864/// `BrowserPoolData` ready for use with `App::app_data()`.
865///
866/// # Example
867///
868/// ```rust,ignore
869/// use html2pdf_api::integrations::actix::create_pool_data_from_arc;
870///
871/// let shared_pool = pool.into_shared();
872/// let pool_for_shutdown = Arc::clone(&shared_pool);
873/// let pool_data = create_pool_data_from_arc(shared_pool);
874///
875/// // Use pool_data in App
876/// // Use pool_for_shutdown for cleanup
877/// ```
878pub fn create_pool_data_from_arc(pool: Arc<BrowserPool>) -> BrowserPoolData {
879 web::Data::new(pool)
880}
881
882// ============================================================================
883// Tests
884// ============================================================================
885
886#[cfg(test)]
887mod tests {
888 use super::*;
889
890 #[test]
891 fn test_type_alias_compiles() {
892 // Verify the type alias is valid
893 fn _accepts_pool_data(_: BrowserPoolData) {}
894 fn _accepts_shared_pool(_: SharedPool) {}
895 }
896
897 #[tokio::test]
898 async fn test_shared_pool_type_matches() {
899 // SharedPool and SharedBrowserPool should be compatible
900 fn _takes_shared_pool(_: SharedPool) {}
901 fn _returns_shared_browser_pool() -> SharedBrowserPool {
902 Arc::new(
903 BrowserPool::builder()
904 .factory(Box::new(crate::factory::mock::MockBrowserFactory::new()))
905 .build()
906 .unwrap(),
907 )
908 }
909
910 // This should compile, proving type compatibility
911 let pool: SharedBrowserPool = _returns_shared_browser_pool();
912 let _: SharedPool = pool;
913 }
914}