610 lines
29 KiB
Rust
610 lines
29 KiB
Rust
// file: crates/ksp-config-lib/src/environment.rs
|
|
// version: 8
|
|
|
|
/// Versioned environment contract template expected at the repository/runtime root.
|
|
pub const DEFAULT_DOTENV_EXAMPLE_PATH: &str = ".env.example";
|
|
/// Default local environment file read by Config from the process launch directory.
|
|
pub const DEFAULT_DOTENV_PATH: &str = ".env";
|
|
|
|
const LOGGING_DOMAIN: &str = "config.environment";
|
|
|
|
/// Source that supplied one resolved Config environment variable.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum ConfigEnvironmentSource {
|
|
/// Value was present in the environment inherited by the current process.
|
|
Process,
|
|
/// Value was absent from the process environment and came from the local `.env` file.
|
|
DotEnv,
|
|
/// Value was absent from both external sources and came from the placeholder/API fallback.
|
|
Fallback,
|
|
}
|
|
|
|
/// One resolved Config environment variable with real/safe values, sensitivity and its winning source.
|
|
#[derive(Clone, Eq, PartialEq)]
|
|
pub struct ConfigEnvironmentValue {
|
|
variable_name: String,
|
|
value: String,
|
|
safe_value: String,
|
|
sensitivity: crate::ConfigSensitivity,
|
|
source: ConfigEnvironmentSource,
|
|
}
|
|
|
|
impl ConfigEnvironmentValue {
|
|
/// Returns the resolved variable name.
|
|
#[must_use]
|
|
pub fn variable_name(&self) -> &str {
|
|
return self.variable_name.as_str();
|
|
}
|
|
|
|
/// Returns the real resolved value.
|
|
#[must_use]
|
|
pub fn value(&self) -> &str {
|
|
return self.value.as_str();
|
|
}
|
|
|
|
/// Returns the representation safe for ordinary diagnostics.
|
|
#[must_use]
|
|
pub fn safe_value(&self) -> &str {
|
|
return self.safe_value.as_str();
|
|
}
|
|
|
|
/// Returns the sensitivity derived from the variable namespace.
|
|
#[must_use]
|
|
pub const fn sensitivity(&self) -> crate::ConfigSensitivity {
|
|
return self.sensitivity;
|
|
}
|
|
|
|
/// Returns the source that won process > `.env` > fallback resolution.
|
|
#[must_use]
|
|
pub const fn source(&self) -> ConfigEnvironmentSource {
|
|
return self.source;
|
|
}
|
|
|
|
/// Returns provenance without embedding the resolved value.
|
|
#[must_use]
|
|
pub fn provenance(&self) -> crate::ConfigValueProvenance {
|
|
return environment_provenance(self.variable_name.as_str(), self.source);
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for ConfigEnvironmentValue {
|
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
return formatter
|
|
.debug_struct("ConfigEnvironmentValue")
|
|
.field("variable_name", &self.variable_name)
|
|
.field("safe_value", &self.safe_value)
|
|
.field("sensitivity", &self.sensitivity)
|
|
.field("source", &self.source)
|
|
.finish();
|
|
}
|
|
}
|
|
|
|
/// Config-owned snapshot of KSP/KSPB process environment values and the local `.env` file.
|
|
///
|
|
/// The process environment is captured first and always has priority over `.env`. An absent `.env` file is equivalent to an empty local environment source.
|
|
/// Config never mutates the parent/process environment through this type.
|
|
#[derive(Clone, Eq, PartialEq)]
|
|
pub struct ConfigEnvironment {
|
|
process: std::collections::BTreeMap<String, String>,
|
|
dotenv: std::collections::BTreeMap<String, String>,
|
|
dotenv_path: std::path::PathBuf,
|
|
}
|
|
|
|
impl ConfigEnvironment {
|
|
/// Captures supported variables from the current process and reads `./.env` when it exists.
|
|
pub fn load() -> ksp_core_lib::Result<Self> {
|
|
return Self::load_from_dotenv_path(std::path::Path::new(DEFAULT_DOTENV_PATH));
|
|
}
|
|
|
|
/// Returns the local `.env` path used by this environment snapshot.
|
|
#[must_use]
|
|
pub fn dotenv_path(&self) -> &std::path::Path {
|
|
return self.dotenv_path.as_path();
|
|
}
|
|
|
|
/// Resolves one KSP/KSPB variable using process > `.env` > fallback priority.
|
|
///
|
|
/// The fallback is used only when the variable is absent. An explicitly defined empty string is a real value and therefore wins over the fallback.
|
|
pub fn resolve_variable(&self, variable_name: &str, fallback: std::option::Option<&str>) -> ksp_core_lib::Result<ConfigEnvironmentValue> {
|
|
let validation = validate_supported_variable_name(variable_name);
|
|
if let std::result::Result::Err(error) = validation {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
if let std::option::Option::Some(value) = self.process.get(variable_name) {
|
|
return resolved_environment_value(variable_name, value.as_str(), ConfigEnvironmentSource::Process);
|
|
}
|
|
if let std::option::Option::Some(value) = self.dotenv.get(variable_name) {
|
|
return resolved_environment_value(variable_name, value.as_str(), ConfigEnvironmentSource::DotEnv);
|
|
}
|
|
if let std::option::Option::Some(value) = fallback {
|
|
return resolved_environment_value(variable_name, value, ConfigEnvironmentSource::Fallback);
|
|
}
|
|
emit_missing_variable_warning(variable_name);
|
|
return std::result::Result::Err(missing_variable_error(variable_name));
|
|
}
|
|
|
|
/// Resolves `${NAME}` and `${NAME:-fallback}` placeholders embedded in one UTF-8 string.
|
|
///
|
|
/// This compatibility helper returns only the real runtime string. Use [`Self::resolve_text_detailed`] when safe value, sensitivity or provenance are
|
|
/// needed.
|
|
pub fn resolve_text(&self, source: &str) -> ksp_core_lib::Result<String> {
|
|
let resolved = self.resolve_text_detailed(source);
|
|
return match resolved {
|
|
std::result::Result::Ok(value) => std::result::Result::Ok(value.value().to_owned()),
|
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
|
};
|
|
}
|
|
|
|
/// Resolves one UTF-8 string while preserving real/safe representations, strongest sensitivity and ordered provenance.
|
|
///
|
|
/// Multiple placeholders are supported. Fallback text is literal and inherits the sensitivity of the referenced variable. Literal-only strings are
|
|
/// classified as `Internal`; when placeholders are present, the result sensitivity is the strongest placeholder sensitivity.
|
|
pub fn resolve_text_detailed(&self, source: &str) -> ksp_core_lib::Result<crate::ResolvedConfigText> {
|
|
let mut value = String::new();
|
|
let mut safe_value = String::new();
|
|
let mut provenance = std::vec::Vec::<crate::ConfigValueProvenance>::new();
|
|
let mut sensitivity = crate::ConfigSensitivity::Public;
|
|
let mut saw_placeholder = false;
|
|
let mut remaining = source;
|
|
loop {
|
|
let start = remaining.find("${");
|
|
let start = match start {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
if !remaining.is_empty() {
|
|
value.push_str(remaining);
|
|
safe_value.push_str(remaining);
|
|
provenance.push(crate::ConfigValueProvenance::DocumentLiteral);
|
|
}
|
|
if !saw_placeholder {
|
|
sensitivity = crate::ConfigSensitivity::Internal;
|
|
}
|
|
return std::result::Result::Ok(crate::ResolvedConfigText::new(value, safe_value, sensitivity, provenance));
|
|
},
|
|
};
|
|
let literal = &remaining[..start];
|
|
if !literal.is_empty() {
|
|
value.push_str(literal);
|
|
safe_value.push_str(literal);
|
|
provenance.push(crate::ConfigValueProvenance::DocumentLiteral);
|
|
}
|
|
let expression_and_tail = &remaining[start + 2..];
|
|
let end = expression_and_tail.find('}');
|
|
let end = match end {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::result::Result::Err(invalid_placeholder_error("placeholder is missing its closing '}'")),
|
|
};
|
|
let expression = &expression_and_tail[..end];
|
|
let parsed = parse_placeholder_expression(expression);
|
|
let (variable_name, fallback) = match parsed {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let resolved = self.resolve_variable(variable_name, fallback);
|
|
let resolved = match resolved {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
saw_placeholder = true;
|
|
sensitivity = sensitivity.strongest(resolved.sensitivity());
|
|
value.push_str(resolved.value());
|
|
safe_value.push_str(resolved.safe_value());
|
|
provenance.push(resolved.provenance());
|
|
remaining = &expression_and_tail[end + 1..];
|
|
}
|
|
}
|
|
|
|
/// Recursively resolves environment placeholders in JSON values and returns only the real runtime tree.
|
|
///
|
|
/// Use [`Self::resolve_json_detailed`] when safe value, sensitivity or per-location provenance are needed.
|
|
pub fn resolve_json(&self, source: &serde_json::Value) -> ksp_core_lib::Result<serde_json::Value> {
|
|
let resolved = self.resolve_json_detailed(source);
|
|
return match resolved {
|
|
std::result::Result::Ok(value) => std::result::Result::Ok(value.value().clone()),
|
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
|
};
|
|
}
|
|
|
|
/// Recursively resolves environment placeholders in JSON values while preserving real/safe trees, strongest sensitivity and JSON-Pointer provenance.
|
|
pub fn resolve_json_detailed(&self, source: &serde_json::Value) -> ksp_core_lib::Result<crate::ResolvedConfigJson> {
|
|
let mut provenance = std::collections::BTreeMap::<String, std::vec::Vec<crate::ConfigValueProvenance>>::new();
|
|
let resolved = resolve_json_node(self, source, "", &mut provenance);
|
|
let (value, safe_value, sensitivity) = match resolved {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return std::result::Result::Ok(crate::ResolvedConfigJson::new(value, safe_value, sensitivity, provenance));
|
|
}
|
|
|
|
/// Recursively resolves environment placeholders in one JSON object map while leaving map keys unchanged.
|
|
pub fn resolve_map(&self, source: &serde_json::Map<String, serde_json::Value>) -> ksp_core_lib::Result<serde_json::Map<String, serde_json::Value>> {
|
|
let resolved = self.resolve_json(&serde_json::Value::Object(source.clone()));
|
|
let resolved = match resolved {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return match resolved {
|
|
serde_json::Value::Object(value) => std::result::Result::Ok(value),
|
|
_ => std::result::Result::Err(invalid_placeholder_error("resolved JSON object changed shape")),
|
|
};
|
|
}
|
|
|
|
/// Loads from dotenv path.
|
|
pub(crate) fn load_from_dotenv_path(dotenv_path: &std::path::Path) -> ksp_core_lib::Result<Self> {
|
|
let process = collect_process_environment(std::env::vars_os());
|
|
let process = match process {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let dotenv = load_dotenv_file(dotenv_path);
|
|
let dotenv = match dotenv {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return std::result::Result::Ok(Self { process, dotenv, dotenv_path: dotenv_path.to_path_buf() });
|
|
}
|
|
|
|
/// Returns the current process values.
|
|
pub(crate) const fn process_values(&self) -> &std::collections::BTreeMap<String, String> {
|
|
return &self.process;
|
|
}
|
|
|
|
/// Returns the current dotenv values.
|
|
pub(crate) const fn dotenv_values(&self) -> &std::collections::BTreeMap<String, String> {
|
|
return &self.dotenv;
|
|
}
|
|
|
|
/// Builds `ConfigEnvironment` from maps.
|
|
#[cfg(test)]
|
|
pub(crate) fn from_maps(process: std::collections::BTreeMap<String, String>, dotenv: std::collections::BTreeMap<String, String>) -> Self {
|
|
return Self { process, dotenv, dotenv_path: std::path::PathBuf::from(DEFAULT_DOTENV_PATH) };
|
|
}
|
|
}
|
|
|
|
/// Executes the crate-internal parse dotenv content operation for the owning module.
|
|
pub(crate) fn parse_dotenv_content(path: &std::path::Path, content: &str) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>> {
|
|
let mut output = std::collections::BTreeMap::<String, String>::new();
|
|
for (line_index, raw_line) in content.lines().enumerate() {
|
|
let raw_line = if line_index == 0 { raw_line.trim_start_matches('\u{feff}') } else { raw_line };
|
|
let line = raw_line.trim();
|
|
if line.is_empty() || line.starts_with('#') {
|
|
continue;
|
|
}
|
|
let assignment = match line.strip_prefix("export ") {
|
|
std::option::Option::Some(value) => value.trim_start(),
|
|
std::option::Option::None => line,
|
|
};
|
|
let separator = assignment.find('=');
|
|
let separator = match separator {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::result::Result::Err(dotenv_syntax_error(path, line_index + 1, "assignment is missing '='")),
|
|
};
|
|
let variable_name = assignment[..separator].trim();
|
|
if !is_generic_dotenv_name(variable_name) {
|
|
return std::result::Result::Err(dotenv_syntax_error(path, line_index + 1, "variable name is invalid"));
|
|
}
|
|
let raw_value = assignment[separator + 1..].trim();
|
|
let value = parse_dotenv_value(path, line_index + 1, raw_value);
|
|
let value = match value {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if !has_supported_namespace(variable_name) {
|
|
continue;
|
|
}
|
|
let validation = validate_supported_variable_name(variable_name);
|
|
if let std::result::Result::Err(error) = validation {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
let entry = output.entry(variable_name.to_owned());
|
|
match entry {
|
|
std::collections::btree_map::Entry::Occupied(_) => {
|
|
return std::result::Result::Err(dotenv_duplicate_error(path, line_index + 1, variable_name));
|
|
},
|
|
std::collections::btree_map::Entry::Vacant(entry) => {
|
|
entry.insert(value);
|
|
},
|
|
}
|
|
}
|
|
return std::result::Result::Ok(output);
|
|
}
|
|
|
|
/// Validates supported variable name.
|
|
pub(crate) fn validate_supported_variable_name(variable_name: &str) -> ksp_core_lib::Result<()> {
|
|
if !has_supported_namespace(variable_name) {
|
|
return std::result::Result::Err(invalid_variable_error(variable_name, "variable must use the KSP_ or KSPB_ namespace"));
|
|
}
|
|
let prefix_length = if variable_name.starts_with("KSPB_") { 5 } else { 4 };
|
|
if variable_name.len() <= prefix_length {
|
|
return std::result::Result::Err(invalid_variable_error(variable_name, "variable namespace must be followed by a name"));
|
|
}
|
|
for byte in variable_name.bytes() {
|
|
let valid = byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_';
|
|
if !valid {
|
|
return std::result::Result::Err(invalid_variable_error(variable_name, "variable names use uppercase ASCII letters, digits and underscores"));
|
|
}
|
|
}
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
fn collect_process_environment<I>(values: I) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>>
|
|
where
|
|
I: std::iter::IntoIterator<Item = (std::ffi::OsString, std::ffi::OsString)>,
|
|
{
|
|
let mut output = std::collections::BTreeMap::<String, String>::new();
|
|
for (name, value) in values {
|
|
let name = match name.to_str() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => continue,
|
|
};
|
|
if !has_supported_namespace(name) {
|
|
continue;
|
|
}
|
|
let validation = validate_supported_variable_name(name);
|
|
if let std::result::Result::Err(error) = validation {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
let value = value.into_string();
|
|
let value = match value {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::result::Result::Err(invalid_environment_value_error(name)),
|
|
};
|
|
output.insert(name.to_owned(), value);
|
|
}
|
|
return std::result::Result::Ok(output);
|
|
}
|
|
|
|
fn load_dotenv_file(path: &std::path::Path) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>> {
|
|
let content = std::fs::read_to_string(path);
|
|
let content = match content {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => return std::result::Result::Ok(std::collections::BTreeMap::new()),
|
|
std::result::Result::Err(error) => return std::result::Result::Err(dotenv_read_error(path, error)),
|
|
};
|
|
return crate::parse_dotenv_content(path, content.as_str());
|
|
}
|
|
|
|
fn parse_dotenv_value(path: &std::path::Path, line_number: usize, raw_value: &str) -> ksp_core_lib::Result<String> {
|
|
if raw_value.starts_with('\'') && (raw_value.len() < 2 || !raw_value.ends_with('\'')) {
|
|
return std::result::Result::Err(dotenv_syntax_error(path, line_number, "single-quoted value is not terminated"));
|
|
}
|
|
if raw_value.starts_with('\'') {
|
|
return std::result::Result::Ok(raw_value[1..raw_value.len() - 1].to_owned());
|
|
}
|
|
if raw_value.starts_with('"') && (raw_value.len() < 2 || !raw_value.ends_with('"')) {
|
|
return std::result::Result::Err(dotenv_syntax_error(path, line_number, "double-quoted value is not terminated"));
|
|
}
|
|
if raw_value.starts_with('"') {
|
|
return parse_double_quoted_value(path, line_number, &raw_value[1..raw_value.len() - 1]);
|
|
}
|
|
let inline_comment = raw_value.find(" #");
|
|
let value = match inline_comment {
|
|
std::option::Option::Some(index) => raw_value[..index].trim_end(),
|
|
std::option::Option::None => raw_value,
|
|
};
|
|
return std::result::Result::Ok(value.to_owned());
|
|
}
|
|
|
|
fn parse_double_quoted_value(path: &std::path::Path, line_number: usize, source: &str) -> ksp_core_lib::Result<String> {
|
|
let mut output = String::new();
|
|
let mut escaped = false;
|
|
for character in source.chars() {
|
|
if escaped {
|
|
let mapped = match character {
|
|
'n' => '\n',
|
|
'r' => '\r',
|
|
't' => '\t',
|
|
'\\' => '\\',
|
|
'"' => '"',
|
|
_ => return std::result::Result::Err(dotenv_syntax_error(path, line_number, "double-quoted value contains an unsupported escape")),
|
|
};
|
|
output.push(mapped);
|
|
escaped = false;
|
|
} else if character == '\\' {
|
|
escaped = true;
|
|
} else {
|
|
output.push(character);
|
|
}
|
|
}
|
|
if escaped {
|
|
return std::result::Result::Err(dotenv_syntax_error(path, line_number, "double-quoted value ends with an incomplete escape"));
|
|
}
|
|
return std::result::Result::Ok(output);
|
|
}
|
|
|
|
fn parse_placeholder_expression(expression: &str) -> ksp_core_lib::Result<(&str, std::option::Option<&str>)> {
|
|
if expression.is_empty() || expression.contains("${") {
|
|
return std::result::Result::Err(invalid_placeholder_error("placeholder expression is empty or nested"));
|
|
}
|
|
let fallback_separator = expression.find(":-");
|
|
let (variable_name, fallback) = match fallback_separator {
|
|
std::option::Option::Some(index) => (&expression[..index], std::option::Option::Some(&expression[index + 2..])),
|
|
std::option::Option::None => (expression, std::option::Option::None),
|
|
};
|
|
let validation = validate_supported_variable_name(variable_name);
|
|
if let std::result::Result::Err(error) = validation {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
return std::result::Result::Ok((variable_name, fallback));
|
|
}
|
|
|
|
fn resolved_environment_value(variable_name: &str, value: &str, source: ConfigEnvironmentSource) -> ksp_core_lib::Result<ConfigEnvironmentValue> {
|
|
let sensitivity = crate::ConfigSensitivity::from_variable_name(variable_name);
|
|
let sensitivity = match sensitivity {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let safe_value = if sensitivity.is_secret() { crate::REDACTED_CONFIG_VALUE.to_owned() } else { value.to_owned() };
|
|
return std::result::Result::Ok(ConfigEnvironmentValue {
|
|
variable_name: variable_name.to_owned(),
|
|
value: value.to_owned(),
|
|
safe_value,
|
|
sensitivity,
|
|
source,
|
|
});
|
|
}
|
|
|
|
fn environment_provenance(variable_name: &str, source: ConfigEnvironmentSource) -> crate::ConfigValueProvenance {
|
|
return match source {
|
|
ConfigEnvironmentSource::Process => crate::ConfigValueProvenance::EnvironmentProcess { variable_name: variable_name.to_owned() },
|
|
ConfigEnvironmentSource::DotEnv => crate::ConfigValueProvenance::EnvironmentDotEnv { variable_name: variable_name.to_owned() },
|
|
ConfigEnvironmentSource::Fallback => crate::ConfigValueProvenance::EnvironmentFallback { variable_name: variable_name.to_owned() },
|
|
};
|
|
}
|
|
|
|
fn resolve_json_node(
|
|
environment: &ConfigEnvironment,
|
|
source: &serde_json::Value,
|
|
pointer: &str,
|
|
provenance: &mut std::collections::BTreeMap<String, std::vec::Vec<crate::ConfigValueProvenance>>,
|
|
) -> ksp_core_lib::Result<(serde_json::Value, serde_json::Value, crate::ConfigSensitivity)> {
|
|
return match source {
|
|
serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::Number(_) => {
|
|
provenance.insert(pointer.to_owned(), vec![crate::ConfigValueProvenance::DocumentLiteral]);
|
|
std::result::Result::Ok((source.clone(), source.clone(), crate::ConfigSensitivity::Internal))
|
|
},
|
|
serde_json::Value::String(value) => {
|
|
let resolved = environment.resolve_text_detailed(value.as_str());
|
|
let resolved = match resolved {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
provenance.insert(pointer.to_owned(), resolved.provenance().to_vec());
|
|
std::result::Result::Ok((
|
|
serde_json::Value::String(resolved.value().to_owned()),
|
|
serde_json::Value::String(resolved.safe_value().to_owned()),
|
|
resolved.sensitivity(),
|
|
))
|
|
},
|
|
serde_json::Value::Array(values) => resolve_json_array(environment, values, pointer, provenance),
|
|
serde_json::Value::Object(values) => resolve_json_object(environment, values, pointer, provenance),
|
|
};
|
|
}
|
|
|
|
fn resolve_json_array(
|
|
environment: &ConfigEnvironment,
|
|
source: &[serde_json::Value],
|
|
pointer: &str,
|
|
provenance: &mut std::collections::BTreeMap<String, std::vec::Vec<crate::ConfigValueProvenance>>,
|
|
) -> ksp_core_lib::Result<(serde_json::Value, serde_json::Value, crate::ConfigSensitivity)> {
|
|
let mut value = std::vec::Vec::<serde_json::Value>::with_capacity(source.len());
|
|
let mut safe_value = std::vec::Vec::<serde_json::Value>::with_capacity(source.len());
|
|
let mut sensitivity = crate::ConfigSensitivity::Public;
|
|
if source.is_empty() {
|
|
sensitivity = crate::ConfigSensitivity::Internal;
|
|
}
|
|
for (index, item) in source.iter().enumerate() {
|
|
let child_pointer = format!("{pointer}/{index}");
|
|
let resolved = resolve_json_node(environment, item, child_pointer.as_str(), provenance);
|
|
let (resolved_value, resolved_safe_value, resolved_sensitivity) = match resolved {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
value.push(resolved_value);
|
|
safe_value.push(resolved_safe_value);
|
|
sensitivity = sensitivity.strongest(resolved_sensitivity);
|
|
}
|
|
return std::result::Result::Ok((serde_json::Value::Array(value), serde_json::Value::Array(safe_value), sensitivity));
|
|
}
|
|
|
|
fn resolve_json_object(
|
|
environment: &ConfigEnvironment,
|
|
source: &serde_json::Map<String, serde_json::Value>,
|
|
pointer: &str,
|
|
provenance: &mut std::collections::BTreeMap<String, std::vec::Vec<crate::ConfigValueProvenance>>,
|
|
) -> ksp_core_lib::Result<(serde_json::Value, serde_json::Value, crate::ConfigSensitivity)> {
|
|
let mut value = serde_json::Map::<String, serde_json::Value>::new();
|
|
let mut safe_value = serde_json::Map::<String, serde_json::Value>::new();
|
|
let mut sensitivity = crate::ConfigSensitivity::Public;
|
|
if source.is_empty() {
|
|
sensitivity = crate::ConfigSensitivity::Internal;
|
|
}
|
|
for (key, item) in source {
|
|
let escaped_key = escape_json_pointer_token(key.as_str());
|
|
let child_pointer = format!("{pointer}/{escaped_key}");
|
|
let resolved = resolve_json_node(environment, item, child_pointer.as_str(), provenance);
|
|
let (resolved_value, resolved_safe_value, resolved_sensitivity) = match resolved {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
value.insert(key.clone(), resolved_value);
|
|
safe_value.insert(key.clone(), resolved_safe_value);
|
|
sensitivity = sensitivity.strongest(resolved_sensitivity);
|
|
}
|
|
return std::result::Result::Ok((serde_json::Value::Object(value), serde_json::Value::Object(safe_value), sensitivity));
|
|
}
|
|
|
|
fn escape_json_pointer_token(value: &str) -> String {
|
|
return value.replace('~', "~0").replace('/', "~1");
|
|
}
|
|
|
|
fn has_supported_namespace(variable_name: &str) -> bool {
|
|
return variable_name.starts_with("KSP_") || variable_name.starts_with("KSPB_");
|
|
}
|
|
|
|
fn is_generic_dotenv_name(variable_name: &str) -> bool {
|
|
let mut bytes = variable_name.bytes();
|
|
let first = match bytes.next() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return false,
|
|
};
|
|
if !(first.is_ascii_alphabetic() || first == b'_') {
|
|
return false;
|
|
}
|
|
for byte in bytes {
|
|
if !(byte.is_ascii_alphanumeric() || byte == b'_') {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
fn emit_missing_variable_warning(variable_name: &str) {
|
|
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = LOGGING_DOMAIN, variable_name = variable_name, "Config environment variable is missing");
|
|
}
|
|
|
|
fn missing_variable_error(variable_name: &str) -> ksp_core_lib::Error {
|
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING, "required Config environment variable is missing")
|
|
.with_context("variable_name", variable_name);
|
|
}
|
|
|
|
fn invalid_variable_error(variable_name: &str, reason: &'static str) -> ksp_core_lib::Error {
|
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_ENVIRONMENT_VARIABLE_INVALID, "Config environment variable name is invalid")
|
|
.with_context("variable_name", variable_name)
|
|
.with_context("reason", reason);
|
|
}
|
|
|
|
fn invalid_environment_value_error(variable_name: &str) -> ksp_core_lib::Error {
|
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_ENVIRONMENT_VALUE_INVALID, "Config environment variable value is not valid UTF-8")
|
|
.with_context("variable_name", variable_name);
|
|
}
|
|
|
|
fn invalid_placeholder_error(reason: &'static str) -> ksp_core_lib::Error {
|
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_ENVIRONMENT_PLACEHOLDER_INVALID, "Config environment placeholder is invalid")
|
|
.with_context("reason", reason);
|
|
}
|
|
|
|
fn dotenv_read_error(path: &std::path::Path, source: std::io::Error) -> ksp_core_lib::Error {
|
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_DOTENV_FILE_READ_FAILED, "Config cannot read the local .env file")
|
|
.with_context("path", path.to_string_lossy().into_owned())
|
|
.with_source(source);
|
|
}
|
|
|
|
fn dotenv_syntax_error(path: &std::path::Path, line_number: usize, reason: &'static str) -> ksp_core_lib::Error {
|
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_DOTENV_SYNTAX_INVALID, "Config local .env syntax is invalid")
|
|
.with_context("path", path.to_string_lossy().into_owned())
|
|
.with_context("line", line_number.to_string())
|
|
.with_context("reason", reason);
|
|
}
|
|
|
|
fn dotenv_duplicate_error(path: &std::path::Path, line_number: usize, variable_name: &str) -> ksp_core_lib::Error {
|
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_DOTENV_SYNTAX_INVALID, "Config local .env contains a duplicate KSP variable")
|
|
.with_context("path", path.to_string_lossy().into_owned())
|
|
.with_context("line", line_number.to_string())
|
|
.with_context("variable_name", variable_name);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "../unit_tests/environment.rs"]
|
|
mod tests;
|