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}