ripht_php_sapi/adapters/mod.rs
1pub mod cli;
2pub mod web;
3
4use std::path::Path;
5
6pub use cli::{CliRequest, CliRequestError};
7pub use web::{Method, WebRequest, WebRequestError};
8
9#[cfg(feature = "http")]
10pub use web::{from_http_parts, from_http_request};
11
12use crate::ExecutionContext;
13
14/// Common error type for all SAPI adapters.
15#[derive(Debug, Clone)]
16#[non_exhaustive]
17pub enum AdapterError {
18 /// Script file was not found.
19 ScriptNotFound(std::path::PathBuf),
20 /// Required configuration is missing.
21 MissingConfiguration(String),
22 /// Invalid configuration value.
23 InvalidConfiguration {
24 field: String,
25 value: String,
26 reason: String,
27 },
28 /// Web-specific errors.
29 Web(WebRequestError),
30 /// CLI-specific errors.
31 Cli(CliRequestError),
32}
33
34impl std::fmt::Display for AdapterError {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 match self {
37 Self::ScriptNotFound(path) => {
38 write!(f, "Script not found: {}", path.display())
39 }
40 Self::MissingConfiguration(field) => {
41 write!(f, "Missing required configuration: {}", field)
42 }
43 Self::InvalidConfiguration {
44 field,
45 value,
46 reason,
47 } => {
48 write!(
49 f,
50 "Invalid configuration for '{}' = '{}': {}",
51 field, value, reason
52 )
53 }
54 Self::Web(err) => write!(f, "Web adapter error: {}", err),
55 Self::Cli(err) => write!(f, "CLI adapter error: {}", err),
56 }
57 }
58}
59
60impl std::error::Error for AdapterError {
61 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
62 match self {
63 Self::Web(err) => Some(err),
64 Self::Cli(err) => Some(err),
65 _ => None,
66 }
67 }
68}
69
70impl From<WebRequestError> for AdapterError {
71 fn from(err: WebRequestError) -> Self {
72 Self::Web(err)
73 }
74}
75
76impl From<CliRequestError> for AdapterError {
77 fn from(err: CliRequestError) -> Self {
78 Self::Cli(err)
79 }
80}
81
82/// Trait for types that can be converted into PHP execution contexts.
83///
84/// This trait provides a unified interface for building `ExecutionContext` instances
85/// from different request types (Web, CLI, custom adapters). Implementors configure
86/// their internal state through builder methods, then call `build()` to create
87/// the final execution context.
88///
89/// # Design Goals
90///
91/// - **Unified Interface**: All adapters share the same `build()` signature
92/// - **Type Safety**: Strong typing prevents configuration errors at compile time
93/// - **Flexibility**: Adapters can have adapter-specific configuration while sharing common patterns
94/// - **Error Handling**: Consistent error types across all adapters
95///
96/// # Examples
97///
98/// ```no_run
99/// use ripht_php_sapi::{WebRequest, CliRequest, adapters::PhpSapiAdapter};
100/// use std::path::Path;
101///
102/// let script = Path::new("/var/www/script.php");
103///
104/// // Web adapter
105/// let web_ctx = WebRequest::get()
106/// .with_uri("/api/test")
107/// .build(script)?;
108///
109/// // CLI adapter
110/// let cli_ctx = CliRequest::new()
111/// .with_arg("--verbose")
112/// .build(script)?;
113///
114/// // Both return ExecutionContext through the same interface
115/// # Ok::<(), Box<dyn std::error::Error>>(())
116/// ```
117///
118/// # Implementation Requirements
119///
120/// Implementors must:
121/// 1. Validate configuration before building
122/// 2. Handle script path existence checking
123/// 3. Return appropriate error types
124/// 4. Ensure the resulting `ExecutionContext` is valid for execution
125///
126/// # Validation Patterns
127///
128/// The trait provides default validation methods that implementors can use:
129///
130/// ```ignore
131/// fn build(self, script_path: impl AsRef<Path>) -> Result<ExecutionContext, AdapterError> {
132/// let path = Self::validate_script_path(script_path)?;
133/// // ... adapter-specific building logic
134/// }
135/// ```
136pub trait PhpSapiAdapter {
137 /// Build an execution context from the configured adapter.
138 ///
139 /// This method consumes the adapter and produces an `ExecutionContext` ready
140 /// for execution. The script path is validated for existence, and all adapter
141 /// configuration is converted into the appropriate server variables, environment
142 /// variables, and execution parameters.
143 ///
144 /// # Arguments
145 ///
146 /// * `script_path` - Path to the PHP script to execute. Must exist and be readable.
147 ///
148 /// # Returns
149 ///
150 /// - `Ok(ExecutionContext)` - Ready to execute
151 /// - `Err(AdapterError)` - Configuration or validation error
152 ///
153 /// # Errors
154 ///
155 /// This method will return an error if:
156 /// - The script file does not exist
157 /// - Required configuration is missing
158 /// - Configuration values are invalid
159 /// - Adapter-specific validation fails
160 fn build(
161 self,
162 script_path: impl AsRef<Path>,
163 ) -> Result<ExecutionContext, AdapterError>;
164
165 /// Validate that a script path exists and is accessible.
166 ///
167 /// This is a utility method that adapters can use for consistent path validation.
168 /// It converts the path to an absolute path when possible and checks for existence.
169 ///
170 /// # Arguments
171 ///
172 /// * `script_path` - Path to validate
173 ///
174 /// # Returns
175 ///
176 /// - `Ok(PathBuf)` - Validated, absolute path
177 /// - `Err(AdapterError::ScriptNotFound)` - Path does not exist
178 fn validate_script_path(
179 script_path: impl AsRef<Path>,
180 ) -> Result<std::path::PathBuf, AdapterError>
181 where
182 Self: Sized,
183 {
184 let path = script_path
185 .as_ref()
186 .to_path_buf();
187
188 if !path.exists() {
189 return Err(AdapterError::ScriptNotFound(path));
190 }
191
192 // Try to canonicalize, but fall back to original path if it fails
193 Ok(std::fs::canonicalize(&path).unwrap_or(path))
194 }
195
196 /// Validate a configuration field is not empty.
197 ///
198 /// Utility method for validating string configuration fields.
199 ///
200 /// # Arguments
201 ///
202 /// * `field_name` - Name of the field for error reporting
203 /// * `value` - Value to validate
204 ///
205 /// # Returns
206 ///
207 /// - `Ok(())` - Value is non-empty
208 /// - `Err(AdapterError::MissingConfiguration)` - Value is empty
209 fn validate_non_empty(
210 field_name: &str,
211 value: &str,
212 ) -> Result<(), AdapterError>
213 where
214 Self: Sized,
215 {
216 if value.is_empty() {
217 Err(AdapterError::MissingConfiguration(field_name.to_string()))
218 } else {
219 Ok(())
220 }
221 }
222
223 /// Validate a configuration field against a predicate.
224 ///
225 /// Utility method for custom field validation.
226 ///
227 /// # Arguments
228 ///
229 /// * `field_name` - Name of the field for error reporting
230 /// * `value` - Value to validate
231 /// * `predicate` - Validation function
232 /// * `error_reason` - Reason for validation failure
233 ///
234 /// # Returns
235 ///
236 /// - `Ok(())` - Validation passed
237 /// - `Err(AdapterError::InvalidConfiguration)` - Validation failed
238 fn validate_field<T, F>(
239 field_name: &str,
240 value: &T,
241 predicate: F,
242 error_reason: &str,
243 ) -> Result<(), AdapterError>
244 where
245 Self: Sized,
246 T: std::fmt::Display,
247 F: FnOnce(&T) -> bool,
248 {
249 if predicate(value) {
250 Ok(())
251 } else {
252 Err(AdapterError::InvalidConfiguration {
253 field: field_name.to_string(),
254 value: value.to_string(),
255 reason: error_reason.to_string(),
256 })
257 }
258 }
259}
260
261// Implementations for existing adapters
262impl PhpSapiAdapter for WebRequest {
263 fn build(
264 self,
265 script_path: impl AsRef<Path>,
266 ) -> Result<ExecutionContext, AdapterError> {
267 self.build(script_path)
268 .map_err(AdapterError::from)
269 }
270}
271
272impl PhpSapiAdapter for CliRequest {
273 fn build(
274 self,
275 script_path: impl AsRef<Path>,
276 ) -> Result<ExecutionContext, AdapterError> {
277 self.build(script_path)
278 .map_err(AdapterError::from)
279 }
280}
281
282// AdapterError and PhpSapiAdapter are already defined in this module and public
283
284#[cfg(test)]
285mod tests;
286
287/// Example implementations demonstrating the `PhpSapiAdapter` trait pattern.
288///
289/// This module provides examples of how to create custom adapters that implement
290/// the `PhpSapiAdapter` trait. These examples are for documentation purposes
291/// and demonstrate best practices for adapter implementation.
292#[cfg(doc)]
293pub mod examples {
294 use super::*;
295 use std::path::{Path, PathBuf};
296
297 /// A minimal custom adapter demonstrating basic trait implementation.
298 ///
299 /// This example shows how to implement `PhpSapiAdapter` for a custom adapter
300 /// with simple configuration validation.
301 ///
302 /// ```
303 /// # use ripht_php_sapi::adapters::{PhpSapiAdapter, examples::MinimalAdapter};
304 /// # use std::path::Path;
305 ///
306 /// // Create and configure adapter
307 /// let adapter = MinimalAdapter::new()
308 /// .with_name("test-app")
309 /// .with_version("1.0.0");
310 ///
311 /// // Build execution context (requires a valid PHP script)
312 /// # let script_path = Path::new("tests/php_scripts/hello.php");
313 /// # if script_path.exists() {
314 /// let context = adapter.build(script_path)?;
315 /// # }
316 /// # Ok::<(), Box<dyn std::error::Error>>(())
317 /// ```
318 pub struct MinimalAdapter {
319 app_name: Option<String>,
320 app_version: String,
321 }
322
323 impl MinimalAdapter {
324 /// Create a new minimal adapter with default settings.
325 pub fn new() -> Self {
326 Self {
327 app_name: None,
328 app_version: "1.0.0".to_string(),
329 }
330 }
331
332 /// Set the application name.
333 pub fn with_name(mut self, name: impl Into<String>) -> Self {
334 self.app_name = Some(name.into());
335 self
336 }
337
338 /// Set the application version.
339 pub fn with_version(mut self, version: impl Into<String>) -> Self {
340 self.app_version = version.into();
341 self
342 }
343 }
344
345 impl PhpSapiAdapter for MinimalAdapter {
346 fn build(
347 self,
348 script_path: impl AsRef<Path>,
349 ) -> Result<crate::ExecutionContext, AdapterError> {
350 // Validate script path using trait utility
351 let validated_path = Self::validate_script_path(script_path)?;
352
353 // Validate required fields
354 let app_name = self.app_name.ok_or_else(|| {
355 AdapterError::MissingConfiguration("app_name".to_string())
356 })?;
357
358 // Validate app name is not empty
359 Self::validate_non_empty("app_name", &app_name)?;
360
361 // Build execution context
362 Ok(crate::ExecutionContext::script(validated_path)
363 .var("APP_NAME", app_name)
364 .var("APP_VERSION", self.app_version)
365 .env("APPLICATION_ENV", "custom"))
366 }
367 }
368
369 /// A more complex adapter showing advanced validation and configuration.
370 ///
371 /// This example demonstrates:
372 /// - Complex field validation with predicates
373 /// - Multiple configuration sources
374 /// - Environment variable handling
375 /// - INI override patterns
376 ///
377 /// ```
378 /// # use ripht_php_sapi::adapters::{PhpSapiAdapter, examples::AdvancedAdapter};
379 /// # use std::path::Path;
380 ///
381 /// let adapter = AdvancedAdapter::new()
382 /// .with_database_url("postgres://localhost:5432/mydb")
383 /// .with_max_connections(50)
384 /// .with_debug_mode(true)
385 /// .with_env("CUSTOM_VAR", "value");
386 ///
387 /// # let script_path = Path::new("tests/php_scripts/hello.php");
388 /// # if script_path.exists() {
389 /// let context = adapter.build(script_path)?;
390 /// # }
391 /// # Ok::<(), Box<dyn std::error::Error>>(())
392 /// ```
393 pub struct AdvancedAdapter {
394 database_url: Option<String>,
395 max_connections: u32,
396 debug_mode: bool,
397 env_vars: Vec<(String, String)>,
398 working_dir: Option<PathBuf>,
399 }
400
401 impl AdvancedAdapter {
402 /// Create a new advanced adapter with production defaults.
403 pub fn new() -> Self {
404 Self {
405 database_url: None,
406 max_connections: 10,
407 debug_mode: false,
408 env_vars: Vec::new(),
409 working_dir: None,
410 }
411 }
412
413 /// Set the database connection URL.
414 pub fn with_database_url(mut self, url: impl Into<String>) -> Self {
415 self.database_url = Some(url.into());
416 self
417 }
418
419 /// Set the maximum database connections.
420 pub fn with_max_connections(mut self, max: u32) -> Self {
421 self.max_connections = max;
422 self
423 }
424
425 /// Enable or disable debug mode.
426 pub fn with_debug_mode(mut self, debug: bool) -> Self {
427 self.debug_mode = debug;
428 self
429 }
430
431 /// Add an environment variable.
432 pub fn with_env(
433 mut self,
434 key: impl Into<String>,
435 value: impl Into<String>,
436 ) -> Self {
437 self.env_vars
438 .push((key.into(), value.into()));
439 self
440 }
441
442 /// Set the working directory.
443 pub fn with_working_dir(mut self, path: impl Into<PathBuf>) -> Self {
444 self.working_dir = Some(path.into());
445 self
446 }
447 }
448
449 impl PhpSapiAdapter for AdvancedAdapter {
450 fn build(
451 self,
452 script_path: impl AsRef<Path>,
453 ) -> Result<crate::ExecutionContext, AdapterError> {
454 // Validate script path
455 let validated_path = Self::validate_script_path(script_path)?;
456
457 // Validate database URL if provided
458 if let Some(ref db_url) = self.database_url {
459 Self::validate_field(
460 "database_url",
461 db_url,
462 |url| {
463 url.starts_with("postgres://")
464 || url.starts_with("mysql://")
465 },
466 "must start with postgres:// or mysql://",
467 )?;
468 }
469
470 // Validate max connections
471 Self::validate_field(
472 "max_connections",
473 &self.max_connections,
474 |&max| max > 0 && max <= 1000,
475 "must be between 1 and 1000",
476 )?;
477
478 // Build execution context
479 let mut ctx = crate::ExecutionContext::script(validated_path)
480 .var(
481 "MAX_CONNECTIONS",
482 self.max_connections
483 .to_string(),
484 )
485 .var("DEBUG_MODE", if self.debug_mode { "1" } else { "0" })
486 .ini("log_errors", if self.debug_mode { "1" } else { "0" })
487 .ini("display_errors", if self.debug_mode { "1" } else { "0" });
488
489 // Add database URL if configured
490 if let Some(db_url) = self.database_url {
491 ctx = ctx.var("DATABASE_URL", db_url);
492 }
493
494 // Add custom environment variables
495 ctx = ctx.envs(self.env_vars);
496
497 // Set working directory if specified
498 if let Some(wd) = self.working_dir {
499 ctx = ctx.env("PWD", wd.to_string_lossy());
500 }
501
502 Ok(ctx)
503 }
504 }
505}