v0.1.3-pre.011
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/environment.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
/// Default local environment file read by Config from the process launch directory.
|
||||
pub const DEFAULT_DOTENV_PATH: &str = ".env";
|
||||
@@ -21,14 +21,13 @@ pub enum ConfigEnvironmentSource {
|
||||
Fallback,
|
||||
}
|
||||
|
||||
/// One resolved Config environment variable and its winning source.
|
||||
///
|
||||
/// This type intentionally does not implement `Debug`: environment values may contain secrets. Sensitivity-aware safe representations are added by the next
|
||||
/// bounded Config prerelease.
|
||||
/// 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,
|
||||
}
|
||||
|
||||
@@ -45,11 +44,41 @@ impl ConfigEnvironmentValue {
|
||||
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.
|
||||
@@ -84,25 +113,13 @@ impl ConfigEnvironment {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if let std::option::Option::Some(value) = self.process.get(variable_name) {
|
||||
return std::result::Result::Ok(ConfigEnvironmentValue {
|
||||
variable_name: variable_name.to_owned(),
|
||||
value: value.clone(),
|
||||
source: ConfigEnvironmentSource::Process,
|
||||
});
|
||||
return resolved_environment_value(variable_name, value.as_str(), ConfigEnvironmentSource::Process);
|
||||
}
|
||||
if let std::option::Option::Some(value) = self.dotenv.get(variable_name) {
|
||||
return std::result::Result::Ok(ConfigEnvironmentValue {
|
||||
variable_name: variable_name.to_owned(),
|
||||
value: value.clone(),
|
||||
source: ConfigEnvironmentSource::DotEnv,
|
||||
});
|
||||
return resolved_environment_value(variable_name, value.as_str(), ConfigEnvironmentSource::DotEnv);
|
||||
}
|
||||
if let std::option::Option::Some(value) = fallback {
|
||||
return std::result::Result::Ok(ConfigEnvironmentValue {
|
||||
variable_name: variable_name.to_owned(),
|
||||
value: value.to_owned(),
|
||||
source: ConfigEnvironmentSource::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));
|
||||
@@ -110,20 +127,49 @@ impl ConfigEnvironment {
|
||||
|
||||
/// Resolves `${NAME}` and `${NAME:-fallback}` placeholders embedded in one UTF-8 string.
|
||||
///
|
||||
/// Multiple placeholders are supported. Fallback text is literal in this prerelease and is not recursively interpreted as another placeholder expression.
|
||||
/// 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 mut output = String::new();
|
||||
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 => {
|
||||
output.push_str(remaining);
|
||||
return std::result::Result::Ok(output);
|
||||
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));
|
||||
},
|
||||
};
|
||||
output.push_str(&remaining[..start]);
|
||||
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 {
|
||||
@@ -141,45 +187,48 @@ impl ConfigEnvironment {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
output.push_str(resolved.value());
|
||||
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 string values while preserving keys and non-string JSON values.
|
||||
/// 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> {
|
||||
return match source {
|
||||
serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::Number(_) => std::result::Result::Ok(source.clone()),
|
||||
serde_json::Value::String(value) => {
|
||||
let resolved = self.resolve_text(value.as_str());
|
||||
match resolved {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(serde_json::Value::String(value)),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
}
|
||||
},
|
||||
serde_json::Value::Array(values) => resolve_array(self, values),
|
||||
serde_json::Value::Object(values) => {
|
||||
let resolved = self.resolve_map(values);
|
||||
match resolved {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(serde_json::Value::Object(value)),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
}
|
||||
},
|
||||
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 one JSON object map while leaving the map keys unchanged.
|
||||
/// 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 mut output = serde_json::Map::<String, serde_json::Value>::new();
|
||||
for (key, value) in source {
|
||||
let resolved = self.resolve_json(value);
|
||||
let resolved = match resolved {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
output.insert(key.clone(), resolved);
|
||||
}
|
||||
return std::result::Result::Ok(output);
|
||||
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")),
|
||||
};
|
||||
}
|
||||
|
||||
fn load_from_dotenv_path(dotenv_path: &std::path::Path) -> ksp_core_lib::Result<Self> {
|
||||
@@ -350,20 +399,117 @@ fn parse_placeholder_expression(expression: &str) -> ksp_core_lib::Result<(&str,
|
||||
return std::result::Result::Ok((variable_name, fallback));
|
||||
}
|
||||
|
||||
fn resolve_array(environment: &ConfigEnvironment, source: &[serde_json::Value]) -> ksp_core_lib::Result<serde_json::Value> {
|
||||
let mut output = std::vec::Vec::<serde_json::Value>::with_capacity(source.len());
|
||||
for value in source {
|
||||
let resolved = environment.resolve_json(value);
|
||||
let resolved = match resolved {
|
||||
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),
|
||||
};
|
||||
output.push(resolved);
|
||||
value.push(resolved_value);
|
||||
safe_value.push(resolved_safe_value);
|
||||
sensitivity = sensitivity.strongest(resolved_sensitivity);
|
||||
}
|
||||
return std::result::Result::Ok(serde_json::Value::Array(output));
|
||||
return std::result::Result::Ok((serde_json::Value::Array(value), serde_json::Value::Array(safe_value), sensitivity));
|
||||
}
|
||||
|
||||
fn validate_supported_variable_name(variable_name: &str) -> ksp_core_lib::Result<()> {
|
||||
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");
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user