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/// ```ignore
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    /// ```ignore
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    /// ```ignore
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}