// file: crates/ksp-config-lib/src/profile.rs // version: 4 /// 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, profile: serde_json::Map, effective: serde_json::Map, origins: std::collections::BTreeMap, } 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 { return &self.globals; } /// Returns the selected profile object including its `profile_id`. #[must_use] pub fn profile(&self) -> &serde_json::Map { 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 { return &self.effective; } /// Returns the top-level provenance for an effective key. #[must_use] pub fn origin(&self, key: &str) -> std::option::Option { 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> { 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 { 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 { 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); } } 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 { 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); } 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::::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 { 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> = 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::::new(); let mut effective = serde_json::Map::::new(); let mut origins = std::collections::BTreeMap::::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;