use std::env::{self, VarError};
use std::fs::File;
use std::path::Path;
use std::time::Duration;
use crate::fdb::error::{ConfigErrorReason, ConfigField, FdbError, FdbResult};
const DEFAULT_API_VERSION: i32 = 730;
const MIN_API_VERSION: i32 = 730;
const MAX_API_VERSION: i32 = 730;
const DEFAULT_HEALTH_CHECK_TIMEOUT_MILLIS: u64 = 5_000;
const MAX_HEALTH_CHECK_TIMEOUT_MILLIS: u64 = 60_000;
const MAX_CLUSTER_FILE_PATH_BYTES: usize = 4_096;
const MAX_ENVIRONMENT_PREFIX_BYTES: usize = 128;
#[derive(Clone, PartialEq, Eq)]
pub struct FdbConfigInput {
pub fdb_cluster_file: Option<String>,
pub fdb_api_version: i32,
pub health_check_timeout_millis: u64,
}
impl Default for FdbConfigInput {
fn default() -> Self {
Self {
fdb_cluster_file: None,
fdb_api_version: DEFAULT_API_VERSION,
health_check_timeout_millis: DEFAULT_HEALTH_CHECK_TIMEOUT_MILLIS,
}
}
}
impl std::fmt::Debug for FdbConfigInput {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("FdbConfigInput")
.field(
"fdb_cluster_file",
&self.fdb_cluster_file.as_ref().map(|_| "<configured>"),
)
.field("fdb_api_version", &self.fdb_api_version)
.field(
"health_check_timeout_millis",
&self.health_check_timeout_millis,
)
.finish()
}
}
#[derive(Clone)]
pub struct FdbConfig {
fdb_cluster_file: Option<String>,
fdb_api_version: i32,
health_check_timeout: Duration,
}
impl FdbConfig {
pub fn new(input: FdbConfigInput) -> FdbResult<Self> {
validate_api_version(input.fdb_api_version)?;
validate_positive_bounded_u64(
input.health_check_timeout_millis,
MAX_HEALTH_CHECK_TIMEOUT_MILLIS,
ConfigField::HealthCheckTimeoutMillis,
)?;
let fdb_cluster_file = input
.fdb_cluster_file
.map(validate_cluster_file_path)
.transpose()?;
Ok(Self {
fdb_cluster_file,
fdb_api_version: input.fdb_api_version,
health_check_timeout: Duration::from_millis(input.health_check_timeout_millis),
})
}
pub fn from_env() -> FdbResult<Self> {
Self::from_env_names(
"FDB_CLUSTER_FILE",
"FDB_API_VERSION",
"FDB_HEALTH_CHECK_TIMEOUT_MILLIS",
)
}
pub fn from_env_prefix(prefix: &str) -> FdbResult<Self> {
let cluster_file = env_name(prefix, "FDB_CLUSTER_FILE")?;
let api_version = env_name(prefix, "FDB_API_VERSION")?;
let health_timeout = env_name(prefix, "FDB_HEALTH_CHECK_TIMEOUT_MILLIS")?;
Self::from_env_names(&cluster_file, &api_version, &health_timeout)
}
fn from_env_names(
cluster_file_name: &str,
api_version_name: &str,
health_timeout_name: &str,
) -> FdbResult<Self> {
Self::new(FdbConfigInput {
fdb_cluster_file: optional_env(cluster_file_name, ConfigField::ClusterFile)?,
fdb_api_version: parse_env_i32(
api_version_name,
DEFAULT_API_VERSION,
ConfigField::ApiVersion,
)?,
health_check_timeout_millis: parse_env_u64(
health_timeout_name,
DEFAULT_HEALTH_CHECK_TIMEOUT_MILLIS,
ConfigField::HealthCheckTimeoutMillis,
)?,
})
}
pub fn fdb_cluster_file(&self) -> Option<&str> {
self.fdb_cluster_file.as_deref()
}
pub const fn fdb_api_version(&self) -> i32 {
self.fdb_api_version
}
pub const fn health_check_timeout(&self) -> Duration {
self.health_check_timeout
}
}
impl std::fmt::Debug for FdbConfig {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("FdbConfig")
.field(
"fdb_cluster_file",
&self.fdb_cluster_file.as_ref().map(|_| "<configured>"),
)
.field("fdb_api_version", &self.fdb_api_version)
.field("health_check_timeout", &self.health_check_timeout)
.finish()
}
}
fn validate_api_version(value: i32) -> FdbResult<()> {
if !(MIN_API_VERSION..=MAX_API_VERSION).contains(&value) {
return Err(config_error(ConfigErrorReason::ValueOutOfBounds {
field: ConfigField::ApiVersion,
}));
}
Ok(())
}
fn validate_cluster_file_path(path: String) -> FdbResult<String> {
if path.is_empty() {
return Err(config_error(ConfigErrorReason::Empty));
}
if path.len() > MAX_CLUSTER_FILE_PATH_BYTES || path.contains('\0') {
return Err(config_error(ConfigErrorReason::InvalidClusterFilePath));
}
let file = File::open(Path::new(&path))
.map_err(|_| config_error(ConfigErrorReason::MissingClusterFile))?;
let metadata = file
.metadata()
.map_err(|_| config_error(ConfigErrorReason::MissingClusterFile))?;
if !metadata.is_file() {
return Err(config_error(ConfigErrorReason::InvalidClusterFilePath));
}
Ok(path)
}
fn validate_positive_bounded_u64(value: u64, maximum: u64, field: ConfigField) -> FdbResult<()> {
if value == 0 || value > maximum {
return Err(config_error(ConfigErrorReason::ValueOutOfBounds { field }));
}
Ok(())
}
fn env_name(prefix: &str, suffix: &str) -> FdbResult<String> {
if prefix.is_empty()
|| prefix.len() > MAX_ENVIRONMENT_PREFIX_BYTES
|| !prefix
.bytes()
.all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
{
return Err(config_error(ConfigErrorReason::InvalidEnvironmentPrefix));
}
Ok(format!("{prefix}_{suffix}"))
}
fn optional_env(name: &str, field: ConfigField) -> FdbResult<Option<String>> {
match env::var(name) {
Ok(value) => Ok(Some(value)),
Err(VarError::NotPresent) => Ok(None),
Err(VarError::NotUnicode(_)) => {
Err(config_error(ConfigErrorReason::InvalidEncoding { field }))
}
}
}
fn parse_env_i32(name: &str, default: i32, field: ConfigField) -> FdbResult<i32> {
optional_env(name, field)?
.map(|value| {
value
.parse::<i32>()
.map_err(|_| config_error(ConfigErrorReason::InvalidInteger { field }))
})
.transpose()
.map(|value| value.unwrap_or(default))
}
fn parse_env_u64(name: &str, default: u64, field: ConfigField) -> FdbResult<u64> {
optional_env(name, field)?
.map(|value| {
value
.parse::<u64>()
.map_err(|_| config_error(ConfigErrorReason::InvalidInteger { field }))
})
.transpose()
.map(|value| value.unwrap_or(default))
}
const fn config_error(reason: ConfigErrorReason) -> FdbError {
FdbError::Config { reason }
}
#[cfg(test)]
#[path = "config_tests.rs"]
mod tests;