v0.1.3-pre.009

This commit is contained in:
2026-08-15 21:25:08 +02:00
parent d1196c03e5
commit a5b4c748ea
17 changed files with 1121 additions and 42 deletions

View File

@@ -0,0 +1,309 @@
// 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;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/document.rs
// version: 2
// version: 3
/// A Config-managed JSON document that has passed syntax, schema and current semantic validation.
#[derive(Clone, Debug, PartialEq)]
@@ -97,7 +97,7 @@ impl ConfigDocumentEngine {
std::result::Result::Ok(()) => {},
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let semantic_validation = validate_document_semantics(&document);
let semantic_validation = validate_document_semantics(self, &document);
return match semantic_validation {
std::result::Result::Ok(()) => std::result::Result::Ok(document),
std::result::Result::Err(error) => std::result::Result::Err(error),
@@ -207,11 +207,15 @@ fn validate_instance(document: &ConfigJsonDocument, schema: &ConfigJsonDocument)
};
}
fn validate_document_semantics(document: &ConfigJsonDocument) -> ksp_core_lib::Result<()> {
fn validate_document_semantics(engine: &ConfigDocumentEngine, document: &ConfigJsonDocument) -> ksp_core_lib::Result<()> {
let profile_validation = crate::profile::validate_document_profile_contract(document);
if let std::result::Result::Err(error) = profile_validation {
return std::result::Result::Err(error);
}
let composite_validation = crate::composite::validate_composite_document_contract(engine, document);
if let std::result::Result::Err(error) = composite_validation {
return std::result::Result::Err(error);
}
if document.file_id().as_str() != crate::FILE_ID_STD_LOGGING {
return std::result::Result::Ok(());
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/error.rs
// version: 4
// version: 5
/// Error code used when a Config bootstrap argument is missing its value.
pub const ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "bootstrap_argument_missing_value");
@@ -36,3 +36,6 @@ pub const ERROR_CODE_DOCUMENT_SEMANTIC_INVALID: ksp_core_lib::ErrorCode = ksp_co
/// Error code used when an explicitly requested Config profile does not exist in a validated document.
pub const ERROR_CODE_PROFILE_NOT_FOUND: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "profile_not_found");
/// Error code used when a composite document references an invalid, unknown, or unsupported Config document.
pub const ERROR_CODE_COMPOSITE_REFERENCE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "composite_reference_invalid");

View File

@@ -1,16 +1,17 @@
// file: crates/ksp-config-lib/src/lib.rs
// version: 4
// version: 5
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! KSP-owned application configuration facade.
//!
//! `0.1.3-pre.008` owns bootstrap roots, the logical file registry, generic JSON/JSON Schema loading and standard-document profile resolution. The standard
//! Logging document is the first registered runtime document. Composite resolution, environment substitution, sensitivity and persistence remain in later bounded
//! prereleases.
//! `0.1.3-pre.009` owns bootstrap roots, the logical file registry, generic JSON/JSON Schema loading, standard-document profile resolution and generic composite
//! resolution by stable `file_id`. The standard Logging document remains the first registered runtime document. Environment substitution, sensitivity and persistence
//! remain in later bounded prereleases.
mod bootstrap;
mod composite;
mod document;
mod error;
mod profile;
@@ -26,6 +27,10 @@ pub use self::bootstrap::ConfigBootstrapOptions;
pub use self::bootstrap::DEFAULT_CFG_PATH;
/// Default root containing KSP JSON schemas.
pub use self::bootstrap::DEFAULT_SCHEMA_PATH;
/// One resolved document component selected by a composite profile.
pub use self::composite::ResolvedCompositeComponent;
/// Validated composite document resolved to one profile and its referenced standard documents.
pub use self::composite::ResolvedConfigComposite;
/// Generic JSON/JSON Schema engine owned by Config.
pub use self::document::ConfigDocumentEngine;
/// A Config-managed JSON document after syntax, schema and current semantic validation.
@@ -34,6 +39,8 @@ pub use self::document::ConfigJsonDocument;
pub use self::error::ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE;
/// Error code used when a Config bootstrap path is empty, inaccessible, or resolves to an existing non-directory path.
pub use self::error::ERROR_CODE_BOOTSTRAP_INVALID_PATH;
/// Error code used when a composite document contains an invalid or unsupported document reference.
pub use self::error::ERROR_CODE_COMPOSITE_REFERENCE_INVALID;
/// Error code used when a schema-valid Config document violates KSP semantic invariants.
pub use self::error::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID;
/// Error code used when the same logical Config file identifier is registered more than once.
@@ -70,10 +77,14 @@ pub use self::registry::ConfigFileId;
pub use self::registry::ConfigFileKind;
/// Registry of KSP-known logical Config files and their replaceable physical filenames.
pub use self::registry::ConfigFileRegistry;
/// Default physical filename for the generic composite JSON Schema document.
pub use self::registry::DEFAULT_COMPOSITE_SCHEMA_FILENAME;
/// Default physical filename for the standard Logging configuration document.
pub use self::registry::DEFAULT_STD_LOGGING_FILENAME;
/// Default physical filename for the standard Logging JSON Schema document.
pub use self::registry::DEFAULT_STD_LOGGING_SCHEMA_FILENAME;
/// Logical file identifier for the generic composite JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_COMPOSITE;
/// Logical file identifier for the standard Logging JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_STD_LOGGING;
/// Logical file identifier for the standard Logging configuration document.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/profile.rs
// version: 1
// version: 2
/// Origin of one top-level value in a resolved standard Config profile.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -17,6 +17,8 @@ pub enum ConfigProfileSelectionSource {
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.
@@ -86,7 +88,7 @@ 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 and composite overrides are intentionally not applied by this prerelease.
/// Environment interpolation is intentionally not applied by this prerelease.
pub fn load_resolved_profile(
&self,
file_id: &crate::ConfigFileId,
@@ -97,10 +99,32 @@ impl crate::ConfigDocumentEngine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return resolve_document_profile(&document, requested_profile);
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<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);
}
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,
@@ -158,7 +182,11 @@ pub(crate) fn validate_document_profile_contract(document: &crate::ConfigJsonDoc
return std::result::Result::Ok(());
}
fn resolve_document_profile(document: &crate::ConfigJsonDocument, requested_profile: std::option::Option<&str>) -> ksp_core_lib::Result<ResolvedConfigProfile> {
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")),
@@ -168,7 +196,7 @@ fn resolve_document_profile(document: &crate::ConfigJsonDocument, requested_prof
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, ConfigProfileSelectionSource::Explicit),
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) {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/registry.rs
// version: 2
// version: 3
/// Bootstrap argument used to replace a known Config filename mapping.
pub const ARG_FILE_MAP: &str = "--filemap";
@@ -7,10 +7,14 @@ pub const ARG_FILE_MAP: &str = "--filemap";
pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging";
/// Logical file identifier for the standard Logging JSON Schema document.
pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging";
/// Logical file identifier for the generic composite JSON Schema document.
pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite";
/// Default physical filename for the standard Logging configuration document.
pub const DEFAULT_STD_LOGGING_FILENAME: &str = "std.logging.json";
/// Default physical filename for the standard Logging JSON Schema document.
pub const DEFAULT_STD_LOGGING_SCHEMA_FILENAME: &str = "std.logging.schema.json";
/// Default physical filename for the generic composite JSON Schema document.
pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json";
/// Stable logical identifier for a Config-managed file.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
@@ -77,7 +81,7 @@ impl ConfigFileDescriptor {
return self.schema_file_id.as_ref();
}
fn new(
pub(crate) fn new(
file_id: &'static str,
kind: ConfigFileKind,
filename: &'static str,
@@ -131,7 +135,13 @@ impl ConfigFileRegistry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return build_registry([logging, logging_schema]);
let composite_schema =
ConfigFileDescriptor::new(FILE_ID_SCHEMA_COMPOSITE, ConfigFileKind::Schema, DEFAULT_COMPOSITE_SCHEMA_FILENAME, std::option::Option::None);
let composite_schema = match composite_schema {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return build_registry([logging, logging_schema, composite_schema]);
}
/// Creates the default registry and applies repeatable `--filemap=<file_id>=<filename>` overrides from raw process arguments.
@@ -208,7 +218,7 @@ impl ConfigFileRegistry {
}
}
fn build_registry<const N: usize>(descriptors: [ConfigFileDescriptor; N]) -> ksp_core_lib::Result<ConfigFileRegistry> {
pub(crate) fn build_registry<const N: usize>(descriptors: [ConfigFileDescriptor; N]) -> ksp_core_lib::Result<ConfigFileRegistry> {
let mut registry = ConfigFileRegistry { descriptors: std::collections::BTreeMap::new() };
for descriptor in descriptors {
let file_id = descriptor.file_id.clone();

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-config-lib/tests/public_api.rs
// version: 5
// version: 6
//! Integration tests for the public `ksp-config-lib` bootstrap, file registry, validated JSON document and profile-resolution contracts.
//! Integration tests for the public `ksp-config-lib` bootstrap, file registry, validated JSON document, profile-resolution and composite contracts.
#[test]
fn bootstrap_contract_is_available_from_crate_root() {
@@ -116,3 +116,20 @@ fn resolved_profile_contract_is_available_from_crate_root() {
}
}
}
#[test]
fn composite_schema_and_provenance_contracts_are_available_from_crate_root() {
let registry = ksp_config_lib::ConfigFileRegistry::defaults();
let schema_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE);
assert!(registry.is_ok(), "public registry should remain constructible: {registry:?}");
assert!(schema_id.is_ok(), "public composite schema file_id should be valid: {schema_id:?}");
if let (std::result::Result::Ok(registry), std::result::Result::Ok(schema_id)) = (registry, schema_id) {
let descriptor = registry.descriptor(&schema_id);
assert!(descriptor.is_ok(), "public composite schema descriptor should exist: {descriptor:?}");
if let std::result::Result::Ok(descriptor) = descriptor {
assert_eq!(descriptor.filename(), std::path::Path::new(ksp_config_lib::DEFAULT_COMPOSITE_SCHEMA_FILENAME));
assert_eq!(descriptor.kind(), ksp_config_lib::ConfigFileKind::Schema);
}
}
assert_ne!(ksp_config_lib::ConfigProfileSelectionSource::Composite, ksp_config_lib::ConfigProfileSelectionSource::Explicit);
}

View File

@@ -0,0 +1,247 @@
// file: crates/ksp-config-lib/unit_tests/composite.rs
// version: 1
const TEST_COMPOSITE_FILE_ID: &str = "cfg.composite.test";
const TEST_COMPOSITE_FILENAME: &str = "examples/composite.example.json";
#[test]
fn committed_composite_example_resolves_default_document_profile() {
let engine = committed_engine();
let file_id = crate::ConfigFileId::new(TEST_COMPOSITE_FILE_ID);
assert!(engine.is_ok(), "composite test engine should be constructible: {engine:?}");
assert!(file_id.is_ok(), "composite test file_id should be valid: {file_id:?}");
if let (std::result::Result::Ok(engine), std::result::Result::Ok(file_id)) = (engine, file_id) {
let resolved = engine.load_resolved_composite(&file_id, std::option::Option::None);
assert!(resolved.is_ok(), "committed composite example should resolve: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved {
assert_eq!(resolved.profile_id(), "local_default");
assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::DefaultProfile);
let logging = resolved.component("logging");
assert!(logging.is_some(), "logging component should be resolved");
if let std::option::Option::Some(logging) = logging {
assert_eq!(logging.resolved().file_id().as_str(), crate::FILE_ID_STD_LOGGING);
assert_eq!(logging.resolved().profile_id(), "local_dev");
assert_eq!(logging.resolved().selection_source(), crate::ConfigProfileSelectionSource::DefaultProfile);
assert_eq!(logging.resolved().origin("logs_directory"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
}
}
}
}
#[test]
fn composite_profile_override_marks_referenced_profile_selection_as_composite() {
let engine = committed_engine();
let file_id = crate::ConfigFileId::new(TEST_COMPOSITE_FILE_ID);
assert!(engine.is_ok(), "composite test engine should be constructible: {engine:?}");
assert!(file_id.is_ok(), "composite test file_id should be valid: {file_id:?}");
if let (std::result::Result::Ok(engine), std::result::Result::Ok(file_id)) = (engine, file_id) {
let resolved = engine.load_resolved_composite(&file_id, std::option::Option::Some("local_explicit"));
assert!(resolved.is_ok(), "explicit composite profile should resolve: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved {
assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::Explicit);
let logging = resolved.component("logging");
assert!(logging.is_some(), "logging component should be resolved");
if let std::option::Option::Some(logging) = logging {
assert_eq!(logging.resolved().profile_id(), "local_dev");
assert_eq!(logging.resolved().selection_source(), crate::ConfigProfileSelectionSource::Composite);
}
}
}
}
#[test]
fn unknown_composite_profile_has_profile_not_found_error() {
let engine = committed_engine();
let file_id = crate::ConfigFileId::new(TEST_COMPOSITE_FILE_ID);
assert!(engine.is_ok(), "composite test engine should be constructible: {engine:?}");
assert!(file_id.is_ok(), "composite test file_id should be valid: {file_id:?}");
if let (std::result::Result::Ok(engine), std::result::Result::Ok(file_id)) = (engine, file_id) {
let result = engine.load_resolved_composite(&file_id, std::option::Option::Some("missing"));
assert!(result.is_err(), "unknown composite profile must be rejected: {result:?}");
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_PROFILE_NOT_FOUND);
}
}
}
#[test]
fn unknown_referenced_file_id_is_rejected_during_composite_validation() {
let fixture = fixture_roots("unknown-reference");
let composite = r#"{
"format_version": 1,
"default_profile": "test",
"profiles": [{
"profile_id": "test",
"documents": [{"component_id": "missing", "file_id": "cfg.std.missing"}]
}]
}"#;
let prepared = prepare_fixture(&fixture, composite);
assert!(prepared.is_ok(), "composite fixture should be writable: {prepared:?}");
if prepared.is_ok() {
let engine = fixture_engine(&fixture);
let file_id = crate::ConfigFileId::new(TEST_COMPOSITE_FILE_ID);
assert!(engine.is_ok(), "fixture engine should be constructible: {engine:?}");
if let (std::result::Result::Ok(engine), std::result::Result::Ok(file_id)) = (engine, file_id) {
let result = engine.load_validated_document(&file_id);
assert!(result.is_err(), "unknown composite reference must be rejected: {result:?}");
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_COMPOSITE_REFERENCE_INVALID);
}
}
}
cleanup_fixture(&fixture);
}
#[test]
fn duplicate_component_ids_are_rejected_inside_one_composite_profile() {
let fixture = fixture_roots("duplicate-component");
let composite = r#"{
"format_version": 1,
"default_profile": "test",
"profiles": [{
"profile_id": "test",
"documents": [
{"component_id": "logging", "file_id": "cfg.std.logging"},
{"component_id": "logging", "file_id": "cfg.std.logging"}
]
}]
}"#;
let prepared = prepare_fixture(&fixture, composite);
assert!(prepared.is_ok(), "composite fixture should be writable: {prepared:?}");
if prepared.is_ok() {
let engine = fixture_engine(&fixture);
let file_id = crate::ConfigFileId::new(TEST_COMPOSITE_FILE_ID);
assert!(engine.is_ok(), "fixture engine should be constructible: {engine:?}");
if let (std::result::Result::Ok(engine), std::result::Result::Ok(file_id)) = (engine, file_id) {
let result = engine.load_validated_document(&file_id);
assert!(result.is_err(), "duplicate component_id must be rejected: {result:?}");
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID);
}
}
}
cleanup_fixture(&fixture);
}
fn committed_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
let workspace = workspace_root();
let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
let bootstrap = match bootstrap {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let registry = test_registry(TEST_COMPOSITE_FILENAME);
return match registry {
std::result::Result::Ok(value) => std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, value)),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
fn fixture_engine(fixture: &FixtureRoots) -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
let bootstrap = crate::ConfigBootstrapOptions::from_paths(fixture.config.as_path(), fixture.schemas.as_path());
let bootstrap = match bootstrap {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let registry = test_registry("composite.test.json");
return match registry {
std::result::Result::Ok(value) => std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, value)),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
fn test_registry(composite_filename: &'static str) -> ksp_core_lib::Result<crate::ConfigFileRegistry> {
let logging = crate::registry::ConfigFileDescriptor::new(
crate::FILE_ID_STD_LOGGING,
crate::ConfigFileKind::Config,
crate::DEFAULT_STD_LOGGING_FILENAME,
std::option::Option::Some(crate::FILE_ID_SCHEMA_STD_LOGGING),
);
let logging = match logging {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let logging_schema = crate::registry::ConfigFileDescriptor::new(
crate::FILE_ID_SCHEMA_STD_LOGGING,
crate::ConfigFileKind::Schema,
crate::DEFAULT_STD_LOGGING_SCHEMA_FILENAME,
std::option::Option::None,
);
let logging_schema = match logging_schema {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let composite_schema = crate::registry::ConfigFileDescriptor::new(
crate::FILE_ID_SCHEMA_COMPOSITE,
crate::ConfigFileKind::Schema,
crate::DEFAULT_COMPOSITE_SCHEMA_FILENAME,
std::option::Option::None,
);
let composite_schema = match composite_schema {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let composite = crate::registry::ConfigFileDescriptor::new(
TEST_COMPOSITE_FILE_ID,
crate::ConfigFileKind::Config,
composite_filename,
std::option::Option::Some(crate::FILE_ID_SCHEMA_COMPOSITE),
);
let composite = match composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::registry::build_registry([logging, logging_schema, composite_schema, composite]);
}
fn prepare_fixture(fixture: &FixtureRoots, composite: &str) -> std::io::Result<()> {
cleanup_fixture(fixture);
let config = std::fs::create_dir_all(fixture.config.as_path());
if let std::result::Result::Err(error) = config {
return std::result::Result::Err(error);
}
let schemas = std::fs::create_dir_all(fixture.schemas.as_path());
if let std::result::Result::Err(error) = schemas {
return std::result::Result::Err(error);
}
let workspace = workspace_root();
let logging = std::fs::copy(workspace.join("config/std.logging.json"), fixture.config.join(crate::DEFAULT_STD_LOGGING_FILENAME));
if let std::result::Result::Err(error) = logging {
return std::result::Result::Err(error);
}
let logging_schema =
std::fs::copy(workspace.join("config/schemas/std.logging.schema.json"), fixture.schemas.join(crate::DEFAULT_STD_LOGGING_SCHEMA_FILENAME));
if let std::result::Result::Err(error) = logging_schema {
return std::result::Result::Err(error);
}
let composite_schema =
std::fs::copy(workspace.join("config/schemas/composite.schema.json"), fixture.schemas.join(crate::DEFAULT_COMPOSITE_SCHEMA_FILENAME));
if let std::result::Result::Err(error) = composite_schema {
return std::result::Result::Err(error);
}
return std::fs::write(fixture.config.join("composite.test.json"), composite);
}
fn workspace_root() -> std::path::PathBuf {
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
}
struct FixtureRoots {
root: std::path::PathBuf,
config: std::path::PathBuf,
schemas: std::path::PathBuf,
}
fn fixture_roots(name: &str) -> FixtureRoots {
let root = std::env::temp_dir().join(format!("ksp-config-composite-{name}-{}", std::process::id()));
return FixtureRoots { config: root.join("config"), schemas: root.join("schemas"), root };
}
fn cleanup_fixture(fixture: &FixtureRoots) {
let removal = std::fs::remove_dir_all(fixture.root.as_path());
if let std::result::Result::Err(error) = removal {
if error.kind() != std::io::ErrorKind::NotFound {
eprintln!("unable to cleanup Config composite fixture {}: {error}", fixture.root.display());
}
}
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/unit_tests/registry.rs
// version: 2
// version: 3
#[test]
fn defaults_register_logging_document_and_schema_with_distinct_roots() {
@@ -195,3 +195,26 @@ fn absolute_fixture_path() -> std::path::PathBuf {
path.push("ksp-config-lib-absolute-mapping.json");
return path;
}
#[test]
fn defaults_register_generic_composite_schema_without_runtime_composite() {
let registry = super::ConfigFileRegistry::defaults();
let schema_id = super::ConfigFileId::new(super::FILE_ID_SCHEMA_COMPOSITE);
let runtime_id = super::ConfigFileId::new("cfg.composite.ksp-app-wallet-desk");
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
assert!(schema_id.is_ok(), "composite schema file_id should be valid: {schema_id:?}");
assert!(runtime_id.is_ok(), "future composite runtime file_id syntax should be valid: {runtime_id:?}");
if let (std::result::Result::Ok(registry), std::result::Result::Ok(schema_id), std::result::Result::Ok(runtime_id)) = (registry, schema_id, runtime_id) {
let schema = registry.descriptor(&schema_id);
let runtime = registry.descriptor(&runtime_id);
assert!(schema.is_ok(), "generic composite schema should be registered: {schema:?}");
assert!(runtime.is_err(), "no fictitious runtime composite should be registered");
if let std::result::Result::Ok(schema) = schema {
assert_eq!(schema.kind(), super::ConfigFileKind::Schema);
assert_eq!(schema.filename(), std::path::Path::new(super::DEFAULT_COMPOSITE_SCHEMA_FILENAME));
}
if let std::result::Result::Err(error) = runtime {
assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_UNKNOWN);
}
}
}