use anyhow::{Context, Result, bail};
use std::path::Path;
use super::{
SecretBackendKind,
types::{PRODEX_POLICY_VERSION, RuntimePolicyFile},
};
pub(crate) fn parse_secret_backend_kind(value: &str) -> Result<SecretBackendKind> {
value
.parse::<SecretBackendKind>()
.map_err(anyhow::Error::new)
}
pub(crate) fn validate_runtime_policy_file(policy: &RuntimePolicyFile, path: &Path) -> Result<()> {
if policy.version != PRODEX_POLICY_VERSION {
bail!(
"unsupported prodex policy version {} in {}; expected {}",
policy.version,
path.display(),
PRODEX_POLICY_VERSION
);
}
if let Some(log_dir) = policy.runtime.log_dir.as_deref()
&& log_dir.trim().is_empty()
{
bail!("runtime.log_dir in {} cannot be empty", path.display());
}
let secret_backend = if let Some(backend) = policy.secrets.backend.as_deref() {
Some(
parse_secret_backend_kind(backend)
.with_context(|| format!("invalid secrets.backend in {}", path.display()))?,
)
} else {
None
};
if secret_backend == Some(SecretBackendKind::Keyring)
&& policy
.secrets
.keyring_service
.as_deref()
.map(str::trim)
.is_none_or(|value| value.is_empty())
{
bail!(
"secrets.keyring_service in {} is required when secrets.backend=keyring",
path.display()
);
}
if let Some(service) = policy.secrets.keyring_service.as_deref()
&& service.trim().is_empty()
{
bail!(
"secrets.keyring_service in {} cannot be empty",
path.display()
);
}
validate_runtime_proxy_policy(policy, path)?;
Ok(())
}
fn validate_runtime_proxy_policy(policy: &RuntimePolicyFile, path: &Path) -> Result<()> {
validate_optional_usize(
policy.runtime_proxy.worker_count,
path,
"runtime_proxy.worker_count",
)?;
validate_optional_usize(
policy.runtime_proxy.long_lived_worker_count,
path,
"runtime_proxy.long_lived_worker_count",
)?;
validate_optional_usize(
policy.runtime_proxy.probe_refresh_worker_count,
path,
"runtime_proxy.probe_refresh_worker_count",
)?;
validate_optional_usize(
policy.runtime_proxy.async_worker_count,
path,
"runtime_proxy.async_worker_count",
)?;
validate_optional_usize(
policy.runtime_proxy.long_lived_queue_capacity,
path,
"runtime_proxy.long_lived_queue_capacity",
)?;
validate_optional_usize(
policy.runtime_proxy.active_request_limit,
path,
"runtime_proxy.active_request_limit",
)?;
validate_optional_usize(
policy.runtime_proxy.profile_inflight_soft_limit,
path,
"runtime_proxy.profile_inflight_soft_limit",
)?;
validate_optional_usize(
policy.runtime_proxy.profile_inflight_hard_limit,
path,
"runtime_proxy.profile_inflight_hard_limit",
)?;
validate_optional_usize(
policy.runtime_proxy.responses_active_limit,
path,
"runtime_proxy.responses_active_limit",
)?;
validate_optional_usize(
policy.runtime_proxy.compact_active_limit,
path,
"runtime_proxy.compact_active_limit",
)?;
validate_optional_usize(
policy.runtime_proxy.websocket_active_limit,
path,
"runtime_proxy.websocket_active_limit",
)?;
validate_optional_usize(
policy.runtime_proxy.standard_active_limit,
path,
"runtime_proxy.standard_active_limit",
)?;
validate_optional_u64(
policy.runtime_proxy.http_connect_timeout_ms,
path,
"runtime_proxy.http_connect_timeout_ms",
)?;
validate_optional_u64(
policy.runtime_proxy.stream_idle_timeout_ms,
path,
"runtime_proxy.stream_idle_timeout_ms",
)?;
validate_optional_u64(
policy.runtime_proxy.sse_lookahead_timeout_ms,
path,
"runtime_proxy.sse_lookahead_timeout_ms",
)?;
validate_optional_u64(
policy.runtime_proxy.prefetch_backpressure_retry_ms,
path,
"runtime_proxy.prefetch_backpressure_retry_ms",
)?;
validate_optional_u64(
policy.runtime_proxy.prefetch_backpressure_timeout_ms,
path,
"runtime_proxy.prefetch_backpressure_timeout_ms",
)?;
validate_optional_usize(
policy.runtime_proxy.prefetch_max_buffered_bytes,
path,
"runtime_proxy.prefetch_max_buffered_bytes",
)?;
validate_optional_u64(
policy.runtime_proxy.websocket_connect_timeout_ms,
path,
"runtime_proxy.websocket_connect_timeout_ms",
)?;
validate_optional_u64(
policy.runtime_proxy.websocket_happy_eyeballs_delay_ms,
path,
"runtime_proxy.websocket_happy_eyeballs_delay_ms",
)?;
validate_optional_u64(
policy.runtime_proxy.websocket_precommit_progress_timeout_ms,
path,
"runtime_proxy.websocket_precommit_progress_timeout_ms",
)?;
validate_optional_u64(
policy.runtime_proxy.broker_ready_timeout_ms,
path,
"runtime_proxy.broker_ready_timeout_ms",
)?;
validate_optional_u64(
policy.runtime_proxy.broker_health_connect_timeout_ms,
path,
"runtime_proxy.broker_health_connect_timeout_ms",
)?;
validate_optional_u64(
policy.runtime_proxy.broker_health_read_timeout_ms,
path,
"runtime_proxy.broker_health_read_timeout_ms",
)?;
validate_optional_u64(
policy
.runtime_proxy
.websocket_previous_response_reuse_stale_ms,
path,
"runtime_proxy.websocket_previous_response_reuse_stale_ms",
)?;
validate_optional_u64(
policy.runtime_proxy.admission_wait_budget_ms,
path,
"runtime_proxy.admission_wait_budget_ms",
)?;
validate_optional_u64(
policy.runtime_proxy.pressure_admission_wait_budget_ms,
path,
"runtime_proxy.pressure_admission_wait_budget_ms",
)?;
validate_optional_u64(
policy.runtime_proxy.long_lived_queue_wait_budget_ms,
path,
"runtime_proxy.long_lived_queue_wait_budget_ms",
)?;
validate_optional_u64(
policy
.runtime_proxy
.pressure_long_lived_queue_wait_budget_ms,
path,
"runtime_proxy.pressure_long_lived_queue_wait_budget_ms",
)?;
validate_optional_u64(
policy.runtime_proxy.sync_probe_pressure_pause_ms,
path,
"runtime_proxy.sync_probe_pressure_pause_ms",
)?;
validate_optional_i64_percent(
policy.runtime_proxy.responses_critical_floor_percent,
path,
"runtime_proxy.responses_critical_floor_percent",
)?;
validate_optional_usize(
policy.runtime_proxy.startup_sync_probe_warm_limit,
path,
"runtime_proxy.startup_sync_probe_warm_limit",
)?;
Ok(())
}
fn validate_optional_usize(value: Option<usize>, path: &Path, field: &str) -> Result<()> {
if matches!(value, Some(0)) {
bail!("{field} in {} must be greater than 0", path.display());
}
Ok(())
}
fn validate_optional_u64(value: Option<u64>, path: &Path, field: &str) -> Result<()> {
if matches!(value, Some(0)) {
bail!("{field} in {} must be greater than 0", path.display());
}
Ok(())
}
fn validate_optional_i64_percent(value: Option<i64>, path: &Path, field: &str) -> Result<()> {
if let Some(value) = value
&& !(1..=10).contains(&value)
{
bail!("{field} in {} must be between 1 and 10", path.display());
}
Ok(())
}