96 lines
3.8 KiB
Rust
96 lines
3.8 KiB
Rust
// file: ks-config/src/environment.rs
|
|
// version: 8
|
|
|
|
//! Environment-file loading and configuration placeholder resolution.
|
|
|
|
/// Result of loading one optional workspace environment file.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct EnvironmentLoadReport {
|
|
/// Explicit or default environment file that was loaded.
|
|
pub loaded_path: std::option::Option<std::path::PathBuf>,
|
|
}
|
|
|
|
/// Loads the selected environment file without overriding process variables.
|
|
///
|
|
/// Resolution order is: existing process environment, selected `.env`, then
|
|
/// fallback text declared with `${NAME:-fallback}` placeholders.
|
|
pub fn load_workspace_environment(
|
|
workspace_root: &std::path::Path,
|
|
) -> ks_core::Result<crate::EnvironmentLoadReport> {
|
|
let explicit_path = std::env::var("KS_ENV_FILE").ok();
|
|
let selected_path = match explicit_path {
|
|
std::option::Option::Some(path) if !path.trim().is_empty() => {
|
|
let candidate = std::path::PathBuf::from(path);
|
|
if candidate.is_absolute() { candidate } else { workspace_root.join(candidate) }
|
|
},
|
|
_ => workspace_root.join(".env"),
|
|
};
|
|
if !selected_path.exists() {
|
|
return std::result::Result::Ok(crate::EnvironmentLoadReport { loaded_path: None });
|
|
}
|
|
return match dotenvy::from_path(&selected_path) {
|
|
std::result::Result::Ok(()) => std::result::Result::Ok(crate::EnvironmentLoadReport {
|
|
loaded_path: std::option::Option::Some(selected_path),
|
|
}),
|
|
std::result::Result::Err(_) => std::result::Result::Err(ks_core::Error::new(
|
|
"config_env_file_load_failed",
|
|
format!("failed to load environment file {}", selected_path.display()),
|
|
)),
|
|
};
|
|
}
|
|
|
|
/// Resolves `${NAME}` and `${NAME:-fallback}` placeholders in arbitrary text.
|
|
/// Missing variables without fallbacks are preserved for lazy consumers.
|
|
pub fn resolve_environment_placeholders(raw: &str) -> std::string::String {
|
|
let mut output = std::string::String::with_capacity(raw.len());
|
|
let bytes = raw.as_bytes();
|
|
let mut index = 0_usize;
|
|
while index < bytes.len() {
|
|
if bytes[index] == b'$' && index + 1 < bytes.len() && bytes[index + 1] == b'{' {
|
|
let start = index;
|
|
let mut end = index + 2;
|
|
while end < bytes.len() && bytes[end] != b'}' {
|
|
end += 1;
|
|
}
|
|
if end < bytes.len() {
|
|
let expression = &raw[index + 2..end];
|
|
let (name, fallback) = match expression.split_once(":-") {
|
|
std::option::Option::Some((name, fallback)) => {
|
|
(name, std::option::Option::Some(fallback))
|
|
},
|
|
std::option::Option::None => (expression, std::option::Option::None),
|
|
};
|
|
let resolved = std::env::var(name)
|
|
.ok()
|
|
.or_else(|| return fallback.map(|value| return value.to_string()));
|
|
match resolved {
|
|
std::option::Option::Some(value) => output.push_str(&value),
|
|
std::option::Option::None => output.push_str(&raw[start..=end]),
|
|
}
|
|
index = end + 1;
|
|
continue;
|
|
}
|
|
}
|
|
output.push(bytes[index] as char);
|
|
index += 1;
|
|
}
|
|
return output;
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
#[test]
|
|
fn fallback_is_used_when_variable_is_absent() {
|
|
let resolved =
|
|
crate::resolve_environment_placeholders("${KS_CONFIG_TEST_MISSING:-fallback}");
|
|
assert_eq!(resolved, "fallback");
|
|
}
|
|
|
|
#[test]
|
|
fn unresolved_required_placeholder_is_preserved() {
|
|
let resolved =
|
|
crate::resolve_environment_placeholders("prefix-${KS_CONFIG_TEST_MISSING}-suffix");
|
|
assert_eq!(resolved, "prefix-${KS_CONFIG_TEST_MISSING}-suffix");
|
|
}
|
|
}
|