310 lines
13 KiB
Rust
310 lines
13 KiB
Rust
// file: crates/ksp-config-lib/src/composite.rs
|
|
// version: 1
|
|
|
|
/// 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<String, ResolvedCompositeComponent>,
|
|
}
|
|
|
|
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<String, ResolvedCompositeComponent> {
|
|
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<ResolvedConfigComposite> {
|
|
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<CompositeProfileSource>,
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct CompositeProfileSource {
|
|
profile_id: String,
|
|
documents: std::vec::Vec<CompositeDocumentReferenceSource>,
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct CompositeDocumentReferenceSource {
|
|
component_id: String,
|
|
file_id: String,
|
|
profile_id: std::option::Option<String>,
|
|
}
|
|
|
|
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::<String>::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<ResolvedConfigComposite> {
|
|
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::<String, ResolvedCompositeComponent>::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::profile::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::profile::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<CompositeDocumentSource> {
|
|
let source = serde_json::from_value::<CompositeDocumentSource>(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;
|