295 lines
13 KiB
Rust
295 lines
13 KiB
Rust
// file: crates/ksp-config-lib/src/profile.rs
|
|
// version: 5
|
|
|
|
/// Origin of one top-level value in a resolved standard Config profile.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum ConfigValueOrigin {
|
|
/// Value comes from the global section of the specialized document.
|
|
Global,
|
|
/// Value comes from the selected profile object.
|
|
Profile,
|
|
}
|
|
|
|
/// Source that selected the effective profile.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum ConfigProfileSelectionSource {
|
|
/// The document's autonomous `default_profile` selected the profile.
|
|
DefaultProfile,
|
|
/// A caller explicitly requested the profile by `profile_id`.
|
|
Explicit,
|
|
/// A composite document selected the referenced standard document profile.
|
|
Composite,
|
|
}
|
|
|
|
/// Validated standard document resolved to one profile while retaining global/profile provenance.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct ResolvedConfigProfile {
|
|
file_id: crate::ConfigFileId,
|
|
path: std::path::PathBuf,
|
|
profile_id: String,
|
|
selection_source: ConfigProfileSelectionSource,
|
|
globals: serde_json::Map<String, serde_json::Value>,
|
|
profile: serde_json::Map<String, serde_json::Value>,
|
|
effective: serde_json::Map<String, serde_json::Value>,
|
|
origins: std::collections::BTreeMap<String, ConfigValueOrigin>,
|
|
}
|
|
|
|
impl ResolvedConfigProfile {
|
|
/// Returns the logical document identifier from which this profile was resolved.
|
|
#[must_use]
|
|
pub fn file_id(&self) -> &crate::ConfigFileId {
|
|
return &self.file_id;
|
|
}
|
|
|
|
/// Returns the physical path of the validated source document.
|
|
#[must_use]
|
|
pub fn path(&self) -> &std::path::Path {
|
|
return self.path.as_path();
|
|
}
|
|
|
|
/// Returns the selected unique profile identifier.
|
|
#[must_use]
|
|
pub fn profile_id(&self) -> &str {
|
|
return self.profile_id.as_str();
|
|
}
|
|
|
|
/// Returns whether selection came from `default_profile` or an explicit caller request.
|
|
#[must_use]
|
|
pub const fn selection_source(&self) -> ConfigProfileSelectionSource {
|
|
return self.selection_source;
|
|
}
|
|
|
|
/// Returns document-global values, excluding the reserved `default_profile` and `profiles` keys.
|
|
#[must_use]
|
|
pub fn globals(&self) -> &serde_json::Map<String, serde_json::Value> {
|
|
return &self.globals;
|
|
}
|
|
|
|
/// Returns the selected profile object including its `profile_id`.
|
|
#[must_use]
|
|
pub fn profile(&self) -> &serde_json::Map<String, serde_json::Value> {
|
|
return &self.profile;
|
|
}
|
|
|
|
/// Returns a deterministic top-level effective view in which selected profile keys override same-named global keys.
|
|
#[must_use]
|
|
pub fn effective(&self) -> &serde_json::Map<String, serde_json::Value> {
|
|
return &self.effective;
|
|
}
|
|
|
|
/// Returns the top-level provenance for an effective key.
|
|
#[must_use]
|
|
pub fn origin(&self, key: &str) -> std::option::Option<ConfigValueOrigin> {
|
|
return self.origins.get(key).copied();
|
|
}
|
|
|
|
/// Resolves environment placeholders in the effective view and returns only the real runtime map.
|
|
///
|
|
/// Use [`Self::resolve_effective_environment_detailed`] when safe value, sensitivity and environment provenance are required. Global/Profile provenance on
|
|
/// this source profile remains unchanged in both cases.
|
|
pub fn resolve_effective_environment(&self, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result<serde_json::Map<String, serde_json::Value>> {
|
|
let resolved = self.resolve_effective_environment_detailed(environment);
|
|
let resolved = match resolved {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return match resolved.value() {
|
|
serde_json::Value::Object(value) => std::result::Result::Ok(value.clone()),
|
|
_ => std::result::Result::Err(ksp_core_lib::Error::new(
|
|
crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID,
|
|
"resolved Config profile effective view changed JSON shape",
|
|
)),
|
|
};
|
|
}
|
|
|
|
/// Resolves environment placeholders while preserving real/safe JSON trees, strongest sensitivity and JSON-Pointer environment provenance.
|
|
///
|
|
/// Top-level Global/Profile provenance remains available through [`Self::origin`]; the returned value adds literal/process/`.env`/fallback provenance for
|
|
/// the environment-resolution stage.
|
|
pub fn resolve_effective_environment_detailed(&self, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result<crate::ResolvedConfigJson> {
|
|
return environment.resolve_json_detailed(&serde_json::Value::Object(self.effective.clone()));
|
|
}
|
|
}
|
|
|
|
impl crate::ConfigDocumentEngine {
|
|
/// Loads, validates and resolves one standard Config document to its default or explicitly requested profile.
|
|
///
|
|
/// Passing `None` selects the autonomous `default_profile` declared by the document. Passing `Some(profile_id)` selects that profile explicitly.
|
|
/// Environment interpolation is intentionally not applied implicitly by profile selection. Call `ResolvedConfigProfile::resolve_effective_environment` for
|
|
/// a real runtime map or `ResolvedConfigProfile::resolve_effective_environment_detailed` when safe value, sensitivity and provenance are also required.
|
|
pub fn load_resolved_profile(
|
|
&self,
|
|
file_id: &crate::ConfigFileId,
|
|
requested_profile: std::option::Option<&str>,
|
|
) -> ksp_core_lib::Result<ResolvedConfigProfile> {
|
|
let document = self.load_validated_document(file_id);
|
|
let document = match document {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let source = match requested_profile {
|
|
std::option::Option::Some(_) => ConfigProfileSelectionSource::Explicit,
|
|
std::option::Option::None => ConfigProfileSelectionSource::DefaultProfile,
|
|
};
|
|
return resolve_document_profile(&document, requested_profile, source);
|
|
}
|
|
}
|
|
|
|
/// Loads resolved profile with source.
|
|
pub(crate) fn load_resolved_profile_with_source(
|
|
engine: &crate::ConfigDocumentEngine,
|
|
file_id: &crate::ConfigFileId,
|
|
requested_profile: std::option::Option<&str>,
|
|
explicit_source: ConfigProfileSelectionSource,
|
|
) -> ksp_core_lib::Result<ResolvedConfigProfile> {
|
|
let document = engine.load_validated_document(file_id);
|
|
let document = match document {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let source = match requested_profile {
|
|
std::option::Option::Some(_) => explicit_source,
|
|
std::option::Option::None => ConfigProfileSelectionSource::DefaultProfile,
|
|
};
|
|
return resolve_document_profile(&document, requested_profile, source);
|
|
}
|
|
|
|
/// Validates document profile contract.
|
|
pub(crate) fn validate_document_profile_contract(document: &crate::ConfigJsonDocument) -> ksp_core_lib::Result<()> {
|
|
let root = match document.value().as_object() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::result::Result::Ok(()),
|
|
};
|
|
let default_profile = root.get("default_profile");
|
|
let profiles = root.get("profiles");
|
|
if default_profile.is_none() && profiles.is_none() {
|
|
return std::result::Result::Ok(());
|
|
}
|
|
let default_profile = match default_profile.and_then(serde_json::Value::as_str) {
|
|
std::option::Option::Some(value) if !value.trim().is_empty() => value,
|
|
_ => return std::result::Result::Err(profile_semantic_error(document, "default_profile must identify a non-empty profile_id")),
|
|
};
|
|
let profiles = match profiles.and_then(serde_json::Value::as_array) {
|
|
std::option::Option::Some(value) if !value.is_empty() => value,
|
|
_ => return std::result::Result::Err(profile_semantic_error(document, "profiles must contain at least one profile object")),
|
|
};
|
|
let mut ids = std::collections::BTreeSet::<String>::new();
|
|
let mut default_found = false;
|
|
for (index, profile) in profiles.iter().enumerate() {
|
|
let object = match profile.as_object() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(
|
|
profile_semantic_error(document, "profile entry must be an object").with_context("profile_index", index.to_string()),
|
|
);
|
|
},
|
|
};
|
|
let profile_id = match object.get("profile_id").and_then(serde_json::Value::as_str) {
|
|
std::option::Option::Some(value) if !value.trim().is_empty() => value,
|
|
_ => {
|
|
return std::result::Result::Err(
|
|
profile_semantic_error(document, "profile entry must declare a non-empty profile_id").with_context("profile_index", index.to_string()),
|
|
);
|
|
},
|
|
};
|
|
if ids.contains(profile_id) {
|
|
return std::result::Result::Err(
|
|
profile_semantic_error(document, "profile_id values must be unique")
|
|
.with_context("profile_index", index.to_string())
|
|
.with_context("profile_id", profile_id),
|
|
);
|
|
}
|
|
ids.insert(profile_id.to_owned());
|
|
if profile_id == default_profile {
|
|
default_found = true;
|
|
}
|
|
}
|
|
if !default_found {
|
|
return std::result::Result::Err(
|
|
profile_semantic_error(document, "default_profile must reference an existing profile_id").with_context("default_profile", default_profile),
|
|
);
|
|
}
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
fn resolve_document_profile(
|
|
document: &crate::ConfigJsonDocument,
|
|
requested_profile: std::option::Option<&str>,
|
|
explicit_source: ConfigProfileSelectionSource,
|
|
) -> ksp_core_lib::Result<ResolvedConfigProfile> {
|
|
let root = match document.value().as_object() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::result::Result::Err(profile_semantic_error(document, "profile resolution requires an object document")),
|
|
};
|
|
let default_profile = match root.get("default_profile").and_then(serde_json::Value::as_str) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::result::Result::Err(profile_semantic_error(document, "profile resolution requires default_profile")),
|
|
};
|
|
let (selected_profile, selection_source) = match requested_profile {
|
|
std::option::Option::Some(value) => (value, explicit_source),
|
|
std::option::Option::None => (default_profile, ConfigProfileSelectionSource::DefaultProfile),
|
|
};
|
|
let profiles = match root.get("profiles").and_then(serde_json::Value::as_array) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::result::Result::Err(profile_semantic_error(document, "profile resolution requires profiles")),
|
|
};
|
|
let mut selected: std::option::Option<&serde_json::Map<String, serde_json::Value>> = std::option::Option::None;
|
|
for profile in profiles {
|
|
let object = match profile.as_object() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => continue,
|
|
};
|
|
if object.get("profile_id").and_then(serde_json::Value::as_str) == std::option::Option::Some(selected_profile) {
|
|
selected = std::option::Option::Some(object);
|
|
break;
|
|
}
|
|
}
|
|
let selected = match selected {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(
|
|
ksp_core_lib::Error::new(crate::ERROR_CODE_PROFILE_NOT_FOUND, "requested Config profile does not exist")
|
|
.with_context("file_id", document.file_id().as_str())
|
|
.with_context("path", document.path().to_string_lossy().into_owned())
|
|
.with_context("profile_id", selected_profile),
|
|
);
|
|
},
|
|
};
|
|
let mut globals = serde_json::Map::<String, serde_json::Value>::new();
|
|
let mut effective = serde_json::Map::<String, serde_json::Value>::new();
|
|
let mut origins = std::collections::BTreeMap::<String, ConfigValueOrigin>::new();
|
|
for (key, value) in root {
|
|
if key != "default_profile" && key != "profiles" {
|
|
globals.insert(key.clone(), value.clone());
|
|
effective.insert(key.clone(), value.clone());
|
|
origins.insert(key.clone(), ConfigValueOrigin::Global);
|
|
}
|
|
}
|
|
let profile = (*selected).clone();
|
|
for (key, value) in &profile {
|
|
effective.insert(key.clone(), value.clone());
|
|
origins.insert(key.clone(), ConfigValueOrigin::Profile);
|
|
}
|
|
return std::result::Result::Ok(ResolvedConfigProfile {
|
|
file_id: document.file_id().clone(),
|
|
path: document.path().to_path_buf(),
|
|
profile_id: selected_profile.to_owned(),
|
|
selection_source,
|
|
globals,
|
|
profile,
|
|
effective,
|
|
origins,
|
|
});
|
|
}
|
|
|
|
fn profile_semantic_error(document: &crate::ConfigJsonDocument, reason: &'static str) -> ksp_core_lib::Error {
|
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID, "Config document violates KSP profile invariants")
|
|
.with_context("file_id", document.file_id().as_str())
|
|
.with_context("path", document.path().to_string_lossy().into_owned())
|
|
.with_context("reason", reason);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "../unit_tests/profile.rs"]
|
|
mod tests;
|