html2pdf_api/service/
mod.rs

1//! PDF generation service module.
2//!
3//! This module provides the **framework-agnostic core** of the PDF generation
4//! service. It contains shared types, error definitions, and the core PDF
5//! generation logic that is reused across all web framework integrations.
6//!
7//! # Module Overview
8//!
9//! ```text
10//! ┌─────────────────────────────────────────────────────────────────────────┐
11//! │                        html2pdf-api crate                               │
12//! │                                                                         │
13//! │  ┌───────────────────────────────────────────────────────────────────┐  │
14//! │  │                    service module (this module)                   │  │
15//! │  │                                                                   │  │
16//! │  │  ┌─────────────────────────┐  ┌─────────────────────────────────┐ │  │
17//! │  │  │      types.rs           │  │          pdf.rs                 │ │  │
18//! │  │  │  ┌───────────────────┐  │  │  ┌───────────────────────────┐  │ │  │
19//! │  │  │  │ PdfFromUrlRequest │  │  │  │ generate_pdf_from_url()   │  │ │  │
20//! │  │  │  │ PdfFromHtmlRequest│  │  │  │ generate_pdf_from_html()  │  │ │  │
21//! │  │  │  │ PdfResponse       │  │  │  │ get_pool_stats()          │  │ │  │
22//! │  │  │  │ PdfServiceError   │  │  │  │ is_pool_ready()           │  │ │  │
23//! │  │  │  │ ErrorResponse     │  │  │  └───────────────────────────┘  │ │  │
24//! │  │  │  │ PoolStatsResponse │  │  │                                 │ │  │
25//! │  │  │  │ HealthResponse    │  │  │                                 │ │  │
26//! │  │  │  └───────────────────┘  │  │                                 │ │  │
27//! │  │  └─────────────────────────┘  └─────────────────────────────────┘ │  │
28//! │  └───────────────────────────────────────────────────────────────────┘  │
29//! │                                    │                                    │
30//! │                                    │ used by                            │
31//! │                                    ▼                                    │
32//! │  ┌───────────────────────────────────────────────────────────────────┐  │
33//! │  │                    integrations module                            │  │
34//! │  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐                │  │
35//! │  │  │  actix.rs   │  │  rocket.rs  │  │   axum.rs   │                │  │
36//! │  │  │  (handlers) │  │  (handlers) │  │  (handlers) │                │  │
37//! │  │  └─────────────┘  └─────────────┘  └─────────────┘                │  │
38//! │  └───────────────────────────────────────────────────────────────────┘  │
39//! └─────────────────────────────────────────────────────────────────────────┘
40//! ```
41//!
42//! # Design Philosophy
43//!
44//! This module follows the **"thin handler, thick service"** pattern:
45//!
46//! | Layer | Responsibility | This Module? |
47//! |-------|----------------|--------------|
48//! | **Service** | Core business logic, validation, PDF generation | ✅ Yes |
49//! | **Handler** | HTTP request/response mapping, framework glue | ❌ No (integrations) |
50//!
51//! Benefits of this design:
52//! - **Single source of truth** for PDF generation logic
53//! - **Easy testing** without HTTP overhead
54//! - **Framework flexibility** - add new frameworks without duplicating logic
55//! - **Type safety** - shared types ensure consistency across integrations
56//!
57//! # Public API Summary
58//!
59//! ## Request Types
60//!
61//! | Type | Purpose | Used By |
62//! |------|---------|---------|
63//! | [`PdfFromUrlRequest`] | Parameters for URL → PDF conversion | `GET /pdf` |
64//! | [`PdfFromHtmlRequest`] | Parameters for HTML → PDF conversion | `POST /pdf/html` |
65//!
66//! ## Response Types
67//!
68//! | Type | Purpose | Used By |
69//! |------|---------|---------|
70//! | [`PdfResponse`] | Successful PDF generation result | PDF endpoints |
71//! | [`PoolStatsResponse`] | Browser pool statistics | `GET /pool/stats` |
72//! | [`HealthResponse`] | Health check response | `GET /health` |
73//! | [`ErrorResponse`] | JSON error response | All endpoints (on error) |
74//!
75//! ## Error Types
76//!
77//! | Type | Purpose |
78//! |------|---------|
79//! | [`PdfServiceError`] | All possible service errors with HTTP status mapping |
80//!
81//! ## Core Functions
82//!
83//! | Function | Purpose | Blocking? |
84//! |----------|---------|-----------|
85//! | [`generate_pdf_from_url`] | Convert URL to PDF | ⚠️ Yes |
86//! | [`generate_pdf_from_html`] | Convert HTML to PDF | ⚠️ Yes |
87//! | [`get_pool_stats`] | Get pool statistics | ✅ Fast |
88//! | [`is_pool_ready`] | Check pool readiness | ✅ Fast |
89//!
90//! ## Constants
91//!
92//! | Constant | Value | Purpose |
93//! |----------|-------|---------|
94//! | [`DEFAULT_TIMEOUT_SECS`] | 60 | Overall operation timeout |
95//! | [`DEFAULT_WAIT_SECS`] | 5 | JavaScript wait time |
96//!
97//! # Usage Patterns
98//!
99//! ## Pattern 1: Use Pre-built Framework Integration (Recommended)
100//!
101//! The easiest way to use this library is via the pre-built integrations:
102//!
103//! ```rust,ignore
104//! use actix_web::{App, HttpServer, web};
105//! use html2pdf_api::prelude::*;
106//!
107//! #[actix_web::main]
108//! async fn main() -> std::io::Result<()> {
109//!     let pool = init_browser_pool().await?;
110//!
111//!     HttpServer::new(move || {
112//!         App::new()
113//!             .app_data(web::Data::new(pool.clone()))
114//!             .configure(html2pdf_api::integrations::actix::configure_routes)
115//!     })
116//!     .bind("127.0.0.1:8080")?
117//!     .run()
118//!     .await
119//! }
120//! ```
121//!
122//! ## Pattern 2: Custom Handlers with Service Functions
123//!
124//! For custom behavior, use the service functions directly:
125//!
126//! ```rust,ignore
127//! use actix_web::{web, HttpResponse};
128//! use html2pdf_api::service::{
129//!     generate_pdf_from_url, PdfFromUrlRequest, PdfServiceError
130//! };
131//! use std::sync::{Arc, Mutex};
132//!
133//! async fn custom_pdf_handler(
134//!     pool: web::Data<Arc<Mutex<BrowserPool>>>,
135//!     query: web::Query<PdfFromUrlRequest>,
136//! ) -> HttpResponse {
137//!     // Add custom logic: authentication, rate limiting, logging, etc.
138//!     log::info!("Custom handler called for: {}", query.url);
139//!
140//!     let pool = pool.into_inner();
141//!     let request = query.into_inner();
142//!
143//!     // Call service in blocking context
144//!     let result = web::block(move || {
145//!         generate_pdf_from_url(&pool, &request)
146//!     }).await;
147//!
148//!     match result {
149//!         Ok(Ok(pdf)) => {
150//!             // Add custom headers, transform response, etc.
151//!             HttpResponse::Ok()
152//!                 .content_type("application/pdf")
153//!                 .insert_header(("X-Custom-Header", "value"))
154//!                 .body(pdf.data)
155//!         }
156//!         Ok(Err(e)) => {
157//!             // Custom error handling
158//!             HttpResponse::build(http::StatusCode::from_u16(e.status_code()).unwrap())
159//!                 .json(serde_json::json!({
160//!                     "error": e.to_string(),
161//!                     "code": e.error_code(),
162//!                     "request_id": "custom-id-123"
163//!                 }))
164//!         }
165//!         Err(e) => {
166//!             HttpResponse::InternalServerError().body(e.to_string())
167//!         }
168//!     }
169//! }
170//! ```
171//!
172//! ## Pattern 3: Direct Service Usage (Non-HTTP)
173//!
174//! For CLI tools, batch processing, or testing:
175//!
176//! ```rust,ignore
177//! use html2pdf_api::service::{
178//!     generate_pdf_from_url, generate_pdf_from_html,
179//!     PdfFromUrlRequest, PdfFromHtmlRequest,
180//! };
181//! use std::sync::Mutex;
182//!
183//! fn batch_convert(pool: &Mutex<BrowserPool>, urls: Vec<String>) -> Vec<Result<Vec<u8>, PdfServiceError>> {
184//!     urls.into_iter()
185//!         .map(|url| {
186//!             let request = PdfFromUrlRequest {
187//!                 url,
188//!                 landscape: Some(true),
189//!                 ..Default::default()
190//!             };
191//!             generate_pdf_from_url(pool, &request).map(|r| r.data)
192//!         })
193//!         .collect()
194//! }
195//!
196//! fn generate_report(pool: &Mutex<BrowserPool>, html: String) -> Result<(), Box<dyn std::error::Error>> {
197//!     let request = PdfFromHtmlRequest {
198//!         html,
199//!         filename: Some("report.pdf".to_string()),
200//!         ..Default::default()
201//!     };
202//!
203//!     let response = generate_pdf_from_html(pool, &request)?;
204//!     std::fs::write("report.pdf", &response.data)?;
205//!     println!("Generated report: {} bytes", response.size());
206//!     Ok(())
207//! }
208//! ```
209//!
210//! # Blocking Behavior
211//!
212//! ⚠️ **Important:** The PDF generation functions ([`generate_pdf_from_url`] and
213//! [`generate_pdf_from_html`]) are **blocking** and should never be called directly
214//! from an async context.
215//!
216//! ## Correct Usage
217//!
218//! ```rust,ignore
219//! // ✅ Actix-web: Use web::block
220//! let result = web::block(move || {
221//!     generate_pdf_from_url(&pool, &request)
222//! }).await;
223//!
224//! // ✅ Tokio: Use spawn_blocking
225//! let result = tokio::task::spawn_blocking(move || {
226//!     generate_pdf_from_url(&pool, &request)
227//! }).await;
228//!
229//! // ✅ Synchronous context: Call directly
230//! let result = generate_pdf_from_url(&pool, &request);
231//! ```
232//!
233//! ## Incorrect Usage
234//!
235//! ```rust,ignore
236//! // ❌ WRONG: Blocking the async runtime
237//! async fn bad_handler(pool: web::Data<SharedPool>) -> HttpResponse {
238//!     // This blocks the entire async runtime thread!
239//!     let result = generate_pdf_from_url(&pool, &request);
240//!     // ...
241//! }
242//! ```
243//!
244//! # Error Handling
245//!
246//! All service functions return `Result<T, PdfServiceError>`. The error type
247//! provides HTTP status codes and error codes for easy API response building:
248//!
249//! ```rust,ignore
250//! use html2pdf_api::service::{PdfServiceError, ErrorResponse};
251//!
252//! fn handle_error(error: PdfServiceError) -> (u16, ErrorResponse) {
253//!     let status = error.status_code();  // e.g., 400, 503, 504
254//!     let response = ErrorResponse::from(&error);
255//!     (status, response)
256//! }
257//!
258//! // Check if error is worth retrying
259//! if error.is_retryable() {
260//!     // Wait and retry
261//!     std::thread::sleep(Duration::from_secs(1));
262//! }
263//! ```
264//!
265//! # Testing
266//!
267//! The service functions can be tested without HTTP:
268//!
269//! ```rust,ignore
270//! use html2pdf_api::service::{generate_pdf_from_url, PdfFromUrlRequest, PdfServiceError};
271//! use html2pdf_api::factory::mock::MockBrowserFactory;
272//!
273//! #[test]
274//! fn test_invalid_url_returns_error() {
275//!     let pool = create_test_pool();
276//!     
277//!     let request = PdfFromUrlRequest {
278//!         url: "not-a-valid-url".to_string(),
279//!         ..Default::default()
280//!     };
281//!     
282//!     let result = generate_pdf_from_url(&pool, &request);
283//!     
284//!     assert!(matches!(result, Err(PdfServiceError::InvalidUrl(_))));
285//! }
286//!
287//! #[test]
288//! fn test_empty_html_returns_error() {
289//!     let pool = create_test_pool();
290//!     
291//!     let request = PdfFromHtmlRequest {
292//!         html: "   ".to_string(),  // whitespace only
293//!         ..Default::default()
294//!     };
295//!     
296//!     let result = generate_pdf_from_html(&pool, &request);
297//!     
298//!     assert!(matches!(result, Err(PdfServiceError::EmptyHtml)));
299//! }
300//! ```
301//!
302//! # Feature Flags
303//!
304//! This module is always available. However, the types include serde support
305//! which is enabled by any integration feature:
306//!
307//! | Feature | Effect on this module |
308//! |---------|----------------------|
309//! | `actix-integration` | Enables `serde` for request/response types |
310//! | `rocket-integration` | Enables `serde` for request/response types |
311//! | `axum-integration` | Enables `serde` for request/response types |
312//!
313//! # See Also
314//!
315//! - [`crate::pool`] - Browser pool management
316//! - [`crate::integrations`] - Framework-specific handlers
317//! - [`crate::prelude`] - Convenient re-exports
318
319mod pdf;
320mod types;
321
322// ============================================================================
323// Re-exports: Types
324// ============================================================================
325
326pub use types::ErrorResponse;
327pub use types::HealthResponse;
328pub use types::PdfFromHtmlRequest;
329pub use types::PdfFromUrlRequest;
330pub use types::PdfResponse;
331pub use types::PdfServiceError;
332pub use types::PoolStatsResponse;
333
334// ============================================================================
335// Re-exports: Functions
336// ============================================================================
337
338pub use pdf::generate_pdf_from_html;
339pub use pdf::generate_pdf_from_url;
340pub use pdf::get_pool_stats;
341pub use pdf::is_pool_ready;
342
343// ============================================================================
344// Re-exports: Constants
345// ============================================================================
346
347pub use pdf::DEFAULT_TIMEOUT_SECS;
348pub use pdf::DEFAULT_WAIT_SECS;
349
350// ============================================================================
351// Module-level tests
352// ============================================================================
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357
358    /// Verify all expected types are exported.
359    #[test]
360    fn test_type_exports() {
361        // Request types
362        let _: PdfFromUrlRequest = PdfFromUrlRequest::default();
363        let _: PdfFromHtmlRequest = PdfFromHtmlRequest::default();
364
365        // Response types
366        let _: PdfResponse = PdfResponse::new(vec![], "test.pdf".to_string(), false);
367        let _: PoolStatsResponse = PoolStatsResponse {
368            available: 0,
369            active: 0,
370            total: 0,
371        };
372        let _: HealthResponse = HealthResponse::default();
373        let _: ErrorResponse = ErrorResponse {
374            error: "test".to_string(),
375            code: "TEST".to_string(),
376        };
377
378        // Error types
379        let _: PdfServiceError = PdfServiceError::EmptyHtml;
380    }
381
382    /// Verify all expected constants are exported.
383    #[test]
384    fn test_constant_exports() {
385        assert!(DEFAULT_TIMEOUT_SECS > 0);
386        assert!(DEFAULT_WAIT_SECS > 0);
387        assert!(DEFAULT_TIMEOUT_SECS >= DEFAULT_WAIT_SECS);
388    }
389
390    /// Verify error type conversions work.
391    #[test]
392    fn test_error_to_response_conversion() {
393        let error = PdfServiceError::InvalidUrl("test".to_string());
394        let response: ErrorResponse = error.into();
395
396        assert_eq!(response.code, "INVALID_URL");
397        assert!(response.error.contains("Invalid URL"));
398    }
399}