// file: crates/ksp-config-lib/src/composite.rs // version: 2 /// One resolved document component selected by a composite profile. #[derive(Clone, Debug, PartialEq)] pub struct ResolvedCompositeComponent { component_id: String, resolved: crate::ResolvedConfigProfile, } impl ResolvedCompositeComponent { /// Returns the component identifier unique inside the selected composite profile. #[must_use] pub fn component_id(&self) -> &str { return self.component_id.as_str(); } /// Returns the referenced standard document after global/profile resolution. #[must_use] pub const fn resolved(&self) -> &crate::ResolvedConfigProfile { return &self.resolved; } } /// Validated composite document resolved to one composite profile and all of its referenced standard documents. #[derive(Clone, Debug, PartialEq)] pub struct ResolvedConfigComposite { file_id: crate::ConfigFileId, path: std::path::PathBuf, profile_id: String, selection_source: crate::ConfigProfileSelectionSource, components: std::collections::BTreeMap, } impl ResolvedConfigComposite { /// Returns the logical composite file identifier. #[must_use] pub fn file_id(&self) -> &crate::ConfigFileId { return &self.file_id; } /// Returns the physical path of the validated composite source. #[must_use] pub fn path(&self) -> &std::path::Path { return self.path.as_path(); } /// Returns the selected composite profile identifier. #[must_use] pub fn profile_id(&self) -> &str { return self.profile_id.as_str(); } /// Returns whether the composite profile came from `default_profile` or an explicit caller selection. #[must_use] pub const fn selection_source(&self) -> crate::ConfigProfileSelectionSource { return self.selection_source; } /// Returns all resolved components keyed by their composite-local `component_id`. #[must_use] pub const fn components(&self) -> &std::collections::BTreeMap { return &self.components; } /// Returns one resolved component by its composite-local identifier. #[must_use] pub fn component(&self, component_id: &str) -> std::option::Option<&ResolvedCompositeComponent> { return self.components.get(component_id); } } impl crate::ConfigDocumentEngine { /// Loads, validates and resolves one registered composite Config document. /// /// Composite descriptors are introduced only when a concrete consumer exists. The generic contract and schema are available from `0.1.3-pre.009` onward. /// Passing `None` selects the composite's `default_profile`; passing `Some(profile_id)` selects a composite profile explicitly. pub fn load_resolved_composite( &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), }; if !document.file_id().as_str().starts_with("cfg.composite.") { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_COMPOSITE_REFERENCE_INVALID, "requested Config document is not a composite") .with_context("file_id", document.file_id().as_str()), ); } return resolve_composite_document(self, &document, requested_profile); } } #[derive(serde::Deserialize)] #[serde(deny_unknown_fields)] struct CompositeDocumentSource { format_version: u32, default_profile: String, profiles: std::vec::Vec, } #[derive(serde::Deserialize)] #[serde(deny_unknown_fields)] struct CompositeProfileSource { profile_id: String, documents: std::vec::Vec, } #[derive(serde::Deserialize)] #[serde(deny_unknown_fields)] struct CompositeDocumentReferenceSource { component_id: String, file_id: String, profile_id: std::option::Option, } /// Validates composite document contract. pub(crate) fn validate_composite_document_contract(engine: &crate::ConfigDocumentEngine, document: &crate::ConfigJsonDocument) -> ksp_core_lib::Result<()> { if !document.file_id().as_str().starts_with("cfg.composite.") { return std::result::Result::Ok(()); } let source = parse_composite_source(document); let source = match source { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if source.format_version != 1 { return std::result::Result::Err(composite_semantic_error(document, "unsupported composite format_version")); } if source.default_profile.trim().is_empty() { return std::result::Result::Err(composite_semantic_error(document, "composite default_profile must not be empty")); } for (profile_index, profile) in source.profiles.iter().enumerate() { if profile.profile_id.trim().is_empty() { return std::result::Result::Err( composite_semantic_error(document, "composite profile_id must not be empty").with_context("profile_index", profile_index.to_string()), ); } let mut component_ids = std::collections::BTreeSet::::new(); for (component_index, reference) in profile.documents.iter().enumerate() { if component_ids.contains(reference.component_id.as_str()) { return std::result::Result::Err( composite_semantic_error(document, "component_id values must be unique inside one composite profile") .with_context("profile_id", profile.profile_id.as_str()) .with_context("component_index", component_index.to_string()) .with_context("component_id", reference.component_id.as_str()), ); } component_ids.insert(reference.component_id.clone()); let validation = validate_reference(engine, document, profile, reference, component_index); if let std::result::Result::Err(error) = validation { return std::result::Result::Err(error); } } } return std::result::Result::Ok(()); } fn resolve_composite_document( engine: &crate::ConfigDocumentEngine, document: &crate::ConfigJsonDocument, requested_profile: std::option::Option<&str>, ) -> ksp_core_lib::Result { let source = parse_composite_source(document); let source = match source { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let (selected_profile_id, selection_source) = match requested_profile { std::option::Option::Some(value) => (value, crate::ConfigProfileSelectionSource::Explicit), std::option::Option::None => (source.default_profile.as_str(), crate::ConfigProfileSelectionSource::DefaultProfile), }; let mut selected: std::option::Option<&CompositeProfileSource> = std::option::Option::None; for profile in &source.profiles { if profile.profile_id == selected_profile_id { selected = std::option::Option::Some(profile); 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 composite 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_id), ); }, }; let mut components = std::collections::BTreeMap::::new(); for reference in &selected.documents { let file_id = crate::ConfigFileId::new(reference.file_id.as_str()); let file_id = match file_id { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let resolved = crate::load_resolved_profile_with_source(engine, &file_id, reference.profile_id.as_deref(), crate::ConfigProfileSelectionSource::Composite); let resolved = match resolved { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let component = ResolvedCompositeComponent { component_id: reference.component_id.clone(), resolved }; components.insert(reference.component_id.clone(), component); } return std::result::Result::Ok(ResolvedConfigComposite { file_id: document.file_id().clone(), path: document.path().to_path_buf(), profile_id: selected_profile_id.to_owned(), selection_source, components, }); } fn validate_reference( engine: &crate::ConfigDocumentEngine, document: &crate::ConfigJsonDocument, profile: &CompositeProfileSource, reference: &CompositeDocumentReferenceSource, component_index: usize, ) -> ksp_core_lib::Result<()> { if reference.component_id.trim().is_empty() { return std::result::Result::Err(composite_reference_error(document, profile, reference, component_index, "component_id must not be empty")); } let file_id = crate::ConfigFileId::new(reference.file_id.as_str()); let file_id = match file_id { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { return std::result::Result::Err(composite_reference_error(document, profile, reference, component_index, "referenced file_id is invalid")); }, }; if !file_id.as_str().starts_with("cfg.std.") { return std::result::Result::Err(composite_reference_error( document, profile, reference, component_index, "composite references must target standard Config document file_ids", )); } let descriptor = engine.registry().descriptor(&file_id); let descriptor = match descriptor { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { return std::result::Result::Err(composite_reference_error(document, profile, reference, component_index, "referenced file_id is not registered")); }, }; if descriptor.kind() != crate::ConfigFileKind::Config { return std::result::Result::Err(composite_reference_error( document, profile, reference, component_index, "referenced file_id is not a Config document", )); } let resolved = crate::load_resolved_profile_with_source(engine, &file_id, reference.profile_id.as_deref(), crate::ConfigProfileSelectionSource::Composite); if let std::result::Result::Err(error) = resolved { return std::result::Result::Err(error); } return std::result::Result::Ok(()); } fn parse_composite_source(document: &crate::ConfigJsonDocument) -> ksp_core_lib::Result { let source = serde_json::from_value::(document.value().clone()); return match source { std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Err(error) => std::result::Result::Err( composite_semantic_error(document, "schema-valid composite document cannot be decoded into the KSP source contract").with_source(error), ), }; } fn composite_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 composite violates KSP semantic invariants") .with_context("file_id", document.file_id().as_str()) .with_context("path", document.path().to_string_lossy().into_owned()) .with_context("reason", reason); } fn composite_reference_error( document: &crate::ConfigJsonDocument, profile: &CompositeProfileSource, reference: &CompositeDocumentReferenceSource, component_index: usize, reason: &'static str, ) -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_COMPOSITE_REFERENCE_INVALID, "Config composite contains an invalid document reference") .with_context("file_id", document.file_id().as_str()) .with_context("path", document.path().to_string_lossy().into_owned()) .with_context("profile_id", profile.profile_id.as_str()) .with_context("component_index", component_index.to_string()) .with_context("component_id", reference.component_id.as_str()) .with_context("referenced_file_id", reference.file_id.as_str()) .with_context("reason", reason); } #[cfg(test)] #[path = "../unit_tests/composite.rs"] mod tests;