pub mod cli;
pub mod web;
use std::path::Path;
pub use cli::{CliRequest, CliRequestError};
pub use web::{Method, WebRequest, WebRequestError};
#[cfg(feature = "http")]
pub use web::{from_http_parts, from_http_request};
use crate::ExecutionContext;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum AdapterError {
ScriptNotFound(std::path::PathBuf),
MissingConfiguration(String),
InvalidConfiguration {
field: String,
value: String,
reason: String,
},
Web(WebRequestError),
Cli(CliRequestError),
}
impl std::fmt::Display for AdapterError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ScriptNotFound(path) => {
write!(f, "Script not found: {}", path.display())
}
Self::MissingConfiguration(field) => {
write!(f, "Missing required configuration: {}", field)
}
Self::InvalidConfiguration {
field,
value,
reason,
} => {
write!(
f,
"Invalid configuration for '{}' = '{}': {}",
field, value, reason
)
}
Self::Web(err) => write!(f, "Web adapter error: {}", err),
Self::Cli(err) => write!(f, "CLI adapter error: {}", err),
}
}
}
impl std::error::Error for AdapterError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Web(err) => Some(err),
Self::Cli(err) => Some(err),
_ => None,
}
}
}
impl From<WebRequestError> for AdapterError {
fn from(err: WebRequestError) -> Self {
Self::Web(err)
}
}
impl From<CliRequestError> for AdapterError {
fn from(err: CliRequestError) -> Self {
Self::Cli(err)
}
}
pub trait PhpSapiAdapter {
fn build(
self,
script_path: impl AsRef<Path>,
) -> Result<ExecutionContext, AdapterError>;
fn validate_script_path(
script_path: impl AsRef<Path>,
) -> Result<std::path::PathBuf, AdapterError>
where
Self: Sized,
{
let path = script_path
.as_ref()
.to_path_buf();
if !path.exists() {
return Err(AdapterError::ScriptNotFound(path));
}
Ok(std::fs::canonicalize(&path).unwrap_or(path))
}
fn validate_non_empty(
field_name: &str,
value: &str,
) -> Result<(), AdapterError>
where
Self: Sized,
{
if value.is_empty() {
Err(AdapterError::MissingConfiguration(field_name.to_string()))
} else {
Ok(())
}
}
fn validate_field<T, F>(
field_name: &str,
value: &T,
predicate: F,
error_reason: &str,
) -> Result<(), AdapterError>
where
Self: Sized,
T: std::fmt::Display,
F: FnOnce(&T) -> bool,
{
if predicate(value) {
Ok(())
} else {
Err(AdapterError::InvalidConfiguration {
field: field_name.to_string(),
value: value.to_string(),
reason: error_reason.to_string(),
})
}
}
}
impl PhpSapiAdapter for WebRequest {
fn build(
self,
script_path: impl AsRef<Path>,
) -> Result<ExecutionContext, AdapterError> {
self.build(script_path)
.map_err(AdapterError::from)
}
}
impl PhpSapiAdapter for CliRequest {
fn build(
self,
script_path: impl AsRef<Path>,
) -> Result<ExecutionContext, AdapterError> {
self.build(script_path)
.map_err(AdapterError::from)
}
}
#[cfg(test)]
mod tests;
#[cfg(doc)]
pub mod examples {
use super::*;
use std::path::{Path, PathBuf};
pub struct MinimalAdapter {
app_name: Option<String>,
app_version: String,
}
impl MinimalAdapter {
pub fn new() -> Self {
Self {
app_name: None,
app_version: "1.0.0".to_string(),
}
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.app_name = Some(name.into());
self
}
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.app_version = version.into();
self
}
}
impl PhpSapiAdapter for MinimalAdapter {
fn build(
self,
script_path: impl AsRef<Path>,
) -> Result<crate::ExecutionContext, AdapterError> {
let validated_path = Self::validate_script_path(script_path)?;
let app_name = self.app_name.ok_or_else(|| {
AdapterError::MissingConfiguration("app_name".to_string())
})?;
Self::validate_non_empty("app_name", &app_name)?;
Ok(crate::ExecutionContext::script(validated_path)
.var("APP_NAME", app_name)
.var("APP_VERSION", self.app_version)
.env("APPLICATION_ENV", "custom"))
}
}
pub struct AdvancedAdapter {
database_url: Option<String>,
max_connections: u32,
debug_mode: bool,
env_vars: Vec<(String, String)>,
working_dir: Option<PathBuf>,
}
impl AdvancedAdapter {
pub fn new() -> Self {
Self {
database_url: None,
max_connections: 10,
debug_mode: false,
env_vars: Vec::new(),
working_dir: None,
}
}
pub fn with_database_url(mut self, url: impl Into<String>) -> Self {
self.database_url = Some(url.into());
self
}
pub fn with_max_connections(mut self, max: u32) -> Self {
self.max_connections = max;
self
}
pub fn with_debug_mode(mut self, debug: bool) -> Self {
self.debug_mode = debug;
self
}
pub fn with_env(
mut self,
key: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.env_vars
.push((key.into(), value.into()));
self
}
pub fn with_working_dir(mut self, path: impl Into<PathBuf>) -> Self {
self.working_dir = Some(path.into());
self
}
}
impl PhpSapiAdapter for AdvancedAdapter {
fn build(
self,
script_path: impl AsRef<Path>,
) -> Result<crate::ExecutionContext, AdapterError> {
let validated_path = Self::validate_script_path(script_path)?;
if let Some(ref db_url) = self.database_url {
Self::validate_field(
"database_url",
db_url,
|url| {
url.starts_with("postgres://")
|| url.starts_with("mysql://")
},
"must start with postgres:// or mysql://",
)?;
}
Self::validate_field(
"max_connections",
&self.max_connections,
|&max| max > 0 && max <= 1000,
"must be between 1 and 1000",
)?;
let mut ctx = crate::ExecutionContext::script(validated_path)
.var(
"MAX_CONNECTIONS",
self.max_connections
.to_string(),
)
.var("DEBUG_MODE", if self.debug_mode { "1" } else { "0" })
.ini("log_errors", if self.debug_mode { "1" } else { "0" })
.ini("display_errors", if self.debug_mode { "1" } else { "0" });
if let Some(db_url) = self.database_url {
ctx = ctx.var("DATABASE_URL", db_url);
}
ctx = ctx.envs(self.env_vars);
if let Some(wd) = self.working_dir {
ctx = ctx.env("PWD", wd.to_string_lossy());
}
Ok(ctx)
}
}
}