reallyme_foundationdb_kit/fdb/
config.rs1use std::env::{self, VarError};
7use std::fs::File;
8use std::path::Path;
9use std::time::Duration;
10
11use crate::fdb::error::{ConfigErrorReason, ConfigField, FdbError, FdbResult};
12
13const DEFAULT_API_VERSION: i32 = 730;
14const MIN_API_VERSION: i32 = 730;
19const MAX_API_VERSION: i32 = 730;
20const DEFAULT_HEALTH_CHECK_TIMEOUT_MILLIS: u64 = 5_000;
21const MAX_HEALTH_CHECK_TIMEOUT_MILLIS: u64 = 60_000;
22const MAX_CLUSTER_FILE_PATH_BYTES: usize = 4_096;
23const MAX_ENVIRONMENT_PREFIX_BYTES: usize = 128;
24
25#[derive(Clone, PartialEq, Eq)]
27pub struct FdbConfigInput {
28 pub fdb_cluster_file: Option<String>,
30 pub fdb_api_version: i32,
32 pub health_check_timeout_millis: u64,
34}
35
36impl Default for FdbConfigInput {
37 fn default() -> Self {
38 Self {
39 fdb_cluster_file: None,
40 fdb_api_version: DEFAULT_API_VERSION,
41 health_check_timeout_millis: DEFAULT_HEALTH_CHECK_TIMEOUT_MILLIS,
42 }
43 }
44}
45
46impl std::fmt::Debug for FdbConfigInput {
47 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 formatter
49 .debug_struct("FdbConfigInput")
50 .field(
51 "fdb_cluster_file",
52 &self.fdb_cluster_file.as_ref().map(|_| "<configured>"),
53 )
54 .field("fdb_api_version", &self.fdb_api_version)
55 .field(
56 "health_check_timeout_millis",
57 &self.health_check_timeout_millis,
58 )
59 .finish()
60 }
61}
62
63#[derive(Clone)]
69pub struct FdbConfig {
70 fdb_cluster_file: Option<String>,
71 fdb_api_version: i32,
72 health_check_timeout: Duration,
73}
74
75impl FdbConfig {
76 pub fn new(input: FdbConfigInput) -> FdbResult<Self> {
78 validate_api_version(input.fdb_api_version)?;
79 validate_positive_bounded_u64(
80 input.health_check_timeout_millis,
81 MAX_HEALTH_CHECK_TIMEOUT_MILLIS,
82 ConfigField::HealthCheckTimeoutMillis,
83 )?;
84 let fdb_cluster_file = input
85 .fdb_cluster_file
86 .map(validate_cluster_file_path)
87 .transpose()?;
88
89 Ok(Self {
90 fdb_cluster_file,
91 fdb_api_version: input.fdb_api_version,
92 health_check_timeout: Duration::from_millis(input.health_check_timeout_millis),
93 })
94 }
95
96 pub fn from_env() -> FdbResult<Self> {
101 Self::from_env_names(
102 "FDB_CLUSTER_FILE",
103 "FDB_API_VERSION",
104 "FDB_HEALTH_CHECK_TIMEOUT_MILLIS",
105 )
106 }
107
108 pub fn from_env_prefix(prefix: &str) -> FdbResult<Self> {
115 let cluster_file = env_name(prefix, "FDB_CLUSTER_FILE")?;
116 let api_version = env_name(prefix, "FDB_API_VERSION")?;
117 let health_timeout = env_name(prefix, "FDB_HEALTH_CHECK_TIMEOUT_MILLIS")?;
118 Self::from_env_names(&cluster_file, &api_version, &health_timeout)
119 }
120
121 fn from_env_names(
122 cluster_file_name: &str,
123 api_version_name: &str,
124 health_timeout_name: &str,
125 ) -> FdbResult<Self> {
126 Self::new(FdbConfigInput {
127 fdb_cluster_file: optional_env(cluster_file_name, ConfigField::ClusterFile)?,
128 fdb_api_version: parse_env_i32(
129 api_version_name,
130 DEFAULT_API_VERSION,
131 ConfigField::ApiVersion,
132 )?,
133 health_check_timeout_millis: parse_env_u64(
134 health_timeout_name,
135 DEFAULT_HEALTH_CHECK_TIMEOUT_MILLIS,
136 ConfigField::HealthCheckTimeoutMillis,
137 )?,
138 })
139 }
140
141 pub fn fdb_cluster_file(&self) -> Option<&str> {
143 self.fdb_cluster_file.as_deref()
144 }
145
146 pub const fn fdb_api_version(&self) -> i32 {
148 self.fdb_api_version
149 }
150
151 pub const fn health_check_timeout(&self) -> Duration {
153 self.health_check_timeout
154 }
155}
156
157impl std::fmt::Debug for FdbConfig {
158 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 formatter
160 .debug_struct("FdbConfig")
161 .field(
162 "fdb_cluster_file",
163 &self.fdb_cluster_file.as_ref().map(|_| "<configured>"),
164 )
165 .field("fdb_api_version", &self.fdb_api_version)
166 .field("health_check_timeout", &self.health_check_timeout)
167 .finish()
168 }
169}
170
171fn validate_api_version(value: i32) -> FdbResult<()> {
172 if !(MIN_API_VERSION..=MAX_API_VERSION).contains(&value) {
173 return Err(config_error(ConfigErrorReason::ValueOutOfBounds {
174 field: ConfigField::ApiVersion,
175 }));
176 }
177 Ok(())
178}
179
180fn validate_cluster_file_path(path: String) -> FdbResult<String> {
181 if path.is_empty() {
182 return Err(config_error(ConfigErrorReason::Empty));
183 }
184 if path.len() > MAX_CLUSTER_FILE_PATH_BYTES || path.contains('\0') {
185 return Err(config_error(ConfigErrorReason::InvalidClusterFilePath));
186 }
187
188 let file = File::open(Path::new(&path))
189 .map_err(|_| config_error(ConfigErrorReason::MissingClusterFile))?;
190 let metadata = file
191 .metadata()
192 .map_err(|_| config_error(ConfigErrorReason::MissingClusterFile))?;
193 if !metadata.is_file() {
194 return Err(config_error(ConfigErrorReason::InvalidClusterFilePath));
195 }
196
197 Ok(path)
198}
199
200fn validate_positive_bounded_u64(value: u64, maximum: u64, field: ConfigField) -> FdbResult<()> {
201 if value == 0 || value > maximum {
202 return Err(config_error(ConfigErrorReason::ValueOutOfBounds { field }));
203 }
204 Ok(())
205}
206
207fn env_name(prefix: &str, suffix: &str) -> FdbResult<String> {
208 if prefix.is_empty()
209 || prefix.len() > MAX_ENVIRONMENT_PREFIX_BYTES
210 || !prefix
211 .bytes()
212 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
213 {
214 return Err(config_error(ConfigErrorReason::InvalidEnvironmentPrefix));
215 }
216 Ok(format!("{prefix}_{suffix}"))
217}
218
219fn optional_env(name: &str, field: ConfigField) -> FdbResult<Option<String>> {
220 match env::var(name) {
221 Ok(value) => Ok(Some(value)),
222 Err(VarError::NotPresent) => Ok(None),
223 Err(VarError::NotUnicode(_)) => {
224 Err(config_error(ConfigErrorReason::InvalidEncoding { field }))
225 }
226 }
227}
228
229fn parse_env_i32(name: &str, default: i32, field: ConfigField) -> FdbResult<i32> {
230 optional_env(name, field)?
231 .map(|value| {
232 value
233 .parse::<i32>()
234 .map_err(|_| config_error(ConfigErrorReason::InvalidInteger { field }))
235 })
236 .transpose()
237 .map(|value| value.unwrap_or(default))
238}
239
240fn parse_env_u64(name: &str, default: u64, field: ConfigField) -> FdbResult<u64> {
241 optional_env(name, field)?
242 .map(|value| {
243 value
244 .parse::<u64>()
245 .map_err(|_| config_error(ConfigErrorReason::InvalidInteger { field }))
246 })
247 .transpose()
248 .map(|value| value.unwrap_or(default))
249}
250
251const fn config_error(reason: ConfigErrorReason) -> FdbError {
252 FdbError::Config { reason }
253}
254
255#[cfg(test)]
256#[path = "config_tests.rs"]
257mod tests;