v0.1.3-pre.009
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 49
|
||||
# version: 50
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.3-pre.8"
|
||||
version = "0.1.3-pre.9"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
25
config/examples/composite.example.json
Normal file
25
config/examples/composite.example.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"default_profile": "local_default",
|
||||
"profiles": [
|
||||
{
|
||||
"profile_id": "local_default",
|
||||
"documents": [
|
||||
{
|
||||
"component_id": "logging",
|
||||
"file_id": "cfg.std.logging"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"profile_id": "local_explicit",
|
||||
"documents": [
|
||||
{
|
||||
"component_id": "logging",
|
||||
"file_id": "cfg.std.logging",
|
||||
"profile_id": "local_dev"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
62
config/schemas/composite.schema.json
Normal file
62
config/schemas/composite.schema.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "KSP composite configuration",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["format_version", "default_profile", "profiles"],
|
||||
"properties": {
|
||||
"format_version": {
|
||||
"const": 1
|
||||
},
|
||||
"default_profile": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"profiles": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/composite_profile"
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"composite_profile": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["profile_id", "documents"],
|
||||
"properties": {
|
||||
"profile_id": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"documents": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/document_reference"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"document_reference": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["component_id", "file_id"],
|
||||
"properties": {
|
||||
"component_id": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"file_id": {
|
||||
"type": "string",
|
||||
"pattern": "^cfg\\.std\\.[a-z0-9][a-z0-9._-]*$"
|
||||
},
|
||||
"profile_id": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
309
crates/ksp-config-lib/src/composite.rs
Normal file
309
crates/ksp-config-lib/src/composite.rs
Normal 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;
|
||||
@@ -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(());
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
247
crates/ksp-config-lib/unit_tests/composite.rs
Normal file
247
crates/ksp-config-lib/unit_tests/composite.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
307
deltas/0.1.3/pre.009.md
Normal file
307
deltas/0.1.3/pre.009.md
Normal file
@@ -0,0 +1,307 @@
|
||||
<!-- file: deltas/0.1.3/pre.009.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.3-pre.009
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente validée :
|
||||
|
||||
```text
|
||||
0.1.3-pre.008
|
||||
```
|
||||
|
||||
Version technique de cette base :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.3-pre.8"
|
||||
Cargo.toml header version = 49
|
||||
```
|
||||
|
||||
Validations utilisateur exécutées le 2026-08-15 :
|
||||
|
||||
```text
|
||||
cargo fmt --all OK
|
||||
cargo check --workspace OK
|
||||
cargo clippy --workspace --all-targets OK
|
||||
cargo test --workspace OK
|
||||
cargo tree -p ksp-config-lib OK
|
||||
cargo tree -p ksp-config-lib -e features OK
|
||||
```
|
||||
|
||||
`cargo test --workspace` confirme notamment 33 tests unitaires + 6 tests publics pour `ksp-config-lib`. `cargo tree -p ksp-config-lib -d` ne signale que la coexistence transitive déjà connue `syn 2.0.119` / `syn 3.0.3` portée par l'écosystème `jsonschema`; aucune correction de dépendance KSP n'est justifiée dans cette tranche.
|
||||
|
||||
## Objet de pre.009
|
||||
|
||||
Introduire le contrat de composition générique avant la résolution environnement :
|
||||
|
||||
```text
|
||||
composite file_id
|
||||
-> schema.composite
|
||||
-> composite profile
|
||||
-> document references by file_id
|
||||
-> default or composite-selected document profile
|
||||
-> ResolvedConfigProfile per component
|
||||
-> preserve Global/Profile provenance
|
||||
```
|
||||
|
||||
Aucun composite runtime d'application fictif n'est créé.
|
||||
|
||||
## `schema.composite`
|
||||
|
||||
Le registre Config connaît désormais :
|
||||
|
||||
```text
|
||||
schema.composite -> composite.schema.json
|
||||
```
|
||||
|
||||
Le mapping est résolu sous `schemapath` et peut être remplacé comme les autres fichiers connus :
|
||||
|
||||
```text
|
||||
--filemap=schema.composite=my.composite.schema.json
|
||||
```
|
||||
|
||||
En revanche, le registre par défaut n'introduit aucun :
|
||||
|
||||
```text
|
||||
cfg.composite.<consumer>
|
||||
```
|
||||
|
||||
Un tel descriptor sera ajouté seulement lorsqu'un consumer concret existera.
|
||||
|
||||
## Contrat composite
|
||||
|
||||
Le schema générique versionné est :
|
||||
|
||||
```text
|
||||
config/schemas/composite.schema.json
|
||||
```
|
||||
|
||||
Un exemple non runtime est fourni sous :
|
||||
|
||||
```text
|
||||
config/examples/composite.example.json
|
||||
```
|
||||
|
||||
Structure :
|
||||
|
||||
```text
|
||||
format_version
|
||||
default_profile
|
||||
profiles[]
|
||||
profile_id
|
||||
documents[]
|
||||
component_id
|
||||
file_id
|
||||
profile_id? # optionnel
|
||||
```
|
||||
|
||||
Règles :
|
||||
|
||||
- le composite utilise le même modèle `default_profile` / `profiles` que les documents standards ;
|
||||
- `component_id` est unique à l'intérieur d'un profil composite ;
|
||||
- une référence cible exclusivement un `cfg.std.*` connu du registre ;
|
||||
- une référence ne contient jamais de filename physique ;
|
||||
- l'imbrication de composites n'est pas ouverte dans cette première surface ;
|
||||
- `profile_id` absent conserve le `default_profile` autonome du document référencé ;
|
||||
- `profile_id` présent impose ce profil depuis la composition.
|
||||
|
||||
## Résolution publique
|
||||
|
||||
`ConfigDocumentEngine` expose maintenant :
|
||||
|
||||
```text
|
||||
load_resolved_composite(file_id, requested_profile)
|
||||
```
|
||||
|
||||
Le caller peut sélectionner :
|
||||
|
||||
```text
|
||||
None -> default_profile du composite
|
||||
Some(profile_id) -> profil composite explicite
|
||||
```
|
||||
|
||||
Le résultat `ResolvedConfigComposite` conserve :
|
||||
|
||||
```text
|
||||
file_id du composite
|
||||
path source
|
||||
profile_id composite sélectionné
|
||||
selection_source
|
||||
components par component_id
|
||||
```
|
||||
|
||||
Chaque `ResolvedCompositeComponent` conserve le `ResolvedConfigProfile` du document standard référencé.
|
||||
|
||||
Ainsi les informations déjà stabilisées en `pre.008` restent disponibles :
|
||||
|
||||
```text
|
||||
globals
|
||||
profile
|
||||
effective
|
||||
origin(key) = Global | Profile
|
||||
```
|
||||
|
||||
## Provenance de sélection
|
||||
|
||||
`ConfigProfileSelectionSource` possède désormais :
|
||||
|
||||
```text
|
||||
DefaultProfile
|
||||
Explicit
|
||||
Composite
|
||||
```
|
||||
|
||||
Lorsqu'un composite déclare explicitement :
|
||||
|
||||
```json
|
||||
{
|
||||
"file_id": "cfg.std.logging",
|
||||
"profile_id": "local_dev"
|
||||
}
|
||||
```
|
||||
|
||||
le `ResolvedConfigProfile` correspondant porte :
|
||||
|
||||
```text
|
||||
selection_source = Composite
|
||||
```
|
||||
|
||||
Si `profile_id` est absent, le document référencé conserve :
|
||||
|
||||
```text
|
||||
selection_source = DefaultProfile
|
||||
```
|
||||
|
||||
La provenance `Global/Profile` de chaque valeur top-level n'est pas remplacée par cette provenance de sélection.
|
||||
|
||||
## Validation sémantique
|
||||
|
||||
Un composite enregistré est soumis à :
|
||||
|
||||
1. JSON syntax ;
|
||||
2. `schema.composite` ;
|
||||
3. contrat générique `default_profile` / `profiles` ;
|
||||
4. unicité des `component_id` par profil ;
|
||||
5. validité et enregistrement des `file_id` référencés ;
|
||||
6. restriction aux documents standards `cfg.std.*` ;
|
||||
7. validation/résolution du profil référencé, y compris le `default_profile` lorsqu'aucun override n'est fourni.
|
||||
|
||||
Un document référencé invalide ou inconnu utilise le nouveau diagnostic :
|
||||
|
||||
```text
|
||||
config.composite_reference_invalid
|
||||
```
|
||||
|
||||
Un profil document ou composite demandé mais absent conserve :
|
||||
|
||||
```text
|
||||
config.profile_not_found
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
Les nouveaux tests couvrent notamment :
|
||||
|
||||
- enregistrement de `schema.composite` sans composite runtime fictif ;
|
||||
- validation du schema/example composite versionné ;
|
||||
- résolution du profil composite par défaut ;
|
||||
- utilisation du `default_profile` du document référencé ;
|
||||
- sélection explicite d'un profil composite ;
|
||||
- provenance `Composite` lorsqu'un profil documentaire est imposé par le composite ;
|
||||
- profil composite inconnu ;
|
||||
- référence vers un `file_id` standard inconnu ;
|
||||
- duplication de `component_id` dans un profil composite ;
|
||||
- disponibilité des constantes/schema/provenance depuis le crate-root.
|
||||
|
||||
Les tests de résolution utilisent un descriptor composite privé à la crate pointant vers l'exemple versionné ; il ne fait pas partie de `ConfigFileRegistry::defaults()`.
|
||||
|
||||
## Hors scope
|
||||
|
||||
Cette tranche n'introduit pas encore :
|
||||
|
||||
- composite runtime d'une application réelle ;
|
||||
- `.env` ;
|
||||
- lecture de l'environnement du processus ;
|
||||
- interpolation `${NAME}` / `${NAME:-fallback}` ;
|
||||
- `Public/Internal/Secret` ;
|
||||
- redaction ;
|
||||
- adaptation vers `ksp_logging_lib::LoggingSettings` ;
|
||||
- mutation/persistence.
|
||||
|
||||
## Dépendances
|
||||
|
||||
Aucune dépendance externe ou feature Cargo supplémentaire n'est ajoutée.
|
||||
|
||||
`ksp-config-lib` ne dépend toujours pas de `ksp-logging-lib` dans cette tranche.
|
||||
|
||||
## Version technique
|
||||
|
||||
La prerelease devient :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.3-pre.9"
|
||||
Cargo.toml header version = 50
|
||||
```
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
```text
|
||||
config/examples/composite.example.json
|
||||
config/schemas/composite.schema.json
|
||||
crates/ksp-config-lib/src/composite.rs
|
||||
crates/ksp-config-lib/unit_tests/composite.rs
|
||||
deltas/0.1.3/pre.009.md
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-config-lib/src/document.rs
|
||||
crates/ksp-config-lib/src/error.rs
|
||||
crates/ksp-config-lib/src/lib.rs
|
||||
crates/ksp-config-lib/src/profile.rs
|
||||
crates/ksp-config-lib/src/registry.rs
|
||||
crates/ksp-config-lib/tests/public_api.rs
|
||||
crates/ksp-config-lib/unit_tests/registry.rs
|
||||
docs/plans/000-README.md
|
||||
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||
docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md
|
||||
docs/rules/FILE_CONTRACTS.md
|
||||
```
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Contrôles exécutés dans l'environnement de génération
|
||||
|
||||
- parsing JSON du schema et de l'exemple composite ;
|
||||
- validation du schema Draft 2020-12 et de l'exemple avec l'implémentation JSON Schema disponible dans l'environnement ;
|
||||
- comparaison du delta avec la base `pre.008` ;
|
||||
- contrôle des headers/version modifiés ;
|
||||
- contrôle de l'absence de nouvelle dépendance Cargo ;
|
||||
- contrôle statique des interdictions `unsafe`, `unwrap`, `expect`, `panic`, opérateur `?` et accès environnement applicatif dans les nouveaux sources de production ;
|
||||
- contrôle de la composition exacte de l'archive delta ;
|
||||
- réapplication du delta sur une copie de `pre.008` pour vérifier la reproduction de l'arbre livré.
|
||||
|
||||
`cargo`, `rustc` et `rustfmt` ne sont pas disponibles dans l'environnement de génération. Aucune validation Rust n'est déclarée réussie ici.
|
||||
|
||||
## Validations utilisateur à exécuter
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test --workspace
|
||||
cargo tree -p ksp-config-lib
|
||||
cargo tree -p ksp-config-lib -d
|
||||
cargo tree -p ksp-config-lib -e features
|
||||
```
|
||||
|
||||
Après validation de `pre.009`, la prochaine tranche planifiée est :
|
||||
|
||||
```text
|
||||
0.1.3-pre.010 — .env + process env + resolver ${...}
|
||||
```
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/000-README.md -->
|
||||
<!-- version: 15 -->
|
||||
<!-- version: 16 -->
|
||||
|
||||
# Plans KSP
|
||||
|
||||
@@ -13,7 +13,7 @@ Un plan décrit le périmètre, les décisions déjà acquises, les questions ou
|
||||
- [`002-FUNCTIONAL_RELEASE_SEQUENCE.md`](002-FUNCTIONAL_RELEASE_SEQUENCE.md) — séquence active de référence des premières releases fonctionnelles ;
|
||||
- [`003-V0_1_1_CORE_FOUNDATION_PLAN.md`](003-V0_1_1_CORE_FOUNDATION_PLAN.md) — plan historique clôturé de la release stable `0.1.1`, établi par `0.1.1-pre.001` puis consolidé jusqu'à `0.1.1-rel.001`.
|
||||
- [`004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`](004-V0_1_2_LOGGING_FOUNDATION_PLAN.md) — plan historique clôturé de la release stable `0.1.2`, établi par `0.1.2-pre.001` puis consolidé jusqu'à `0.1.2-rel.001`.
|
||||
- [`005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](005-V0_1_3_CONFIG_FOUNDATION_PLAN.md) — plan actif de `0.1.3 — Configuration foundation`, établi par `0.1.3-pre.001`, corrigé par `pre.001-fix.001`, complété par `pre.001-fix.002` pour le registre `file_id`/bootstrap/non-régression Logging, regranularisé par `pre.001-fix.003`, puis rescindé pendant `pre.005`; `pre.006` a fermé le routing structuré `domain`, `pre.007` le moteur JSON/JSON Schema et le premier `std.logging.json`, puis `pre.008` la résolution globals/profils/`default_profile`; `pre.009` poursuivra avec les compositions génériques par `file_id`.
|
||||
- [`005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](005-V0_1_3_CONFIG_FOUNDATION_PLAN.md) — plan actif de `0.1.3 — Configuration foundation`, établi par `0.1.3-pre.001`, corrigé par `pre.001-fix.001`, complété par `pre.001-fix.002` pour le registre `file_id`/bootstrap/non-régression Logging, regranularisé par `pre.001-fix.003`, puis rescindé pendant `pre.005`; `pre.006` a fermé le routing structuré `domain`, `pre.007` le moteur JSON/JSON Schema et le premier `std.logging.json`, `pre.008` la résolution globals/profils/`default_profile`, puis `pre.009` les compositions génériques par `file_id`; `pre.010` poursuivra avec `.env`, process env et le resolver `${...}`.
|
||||
|
||||
Le `pre.001` de chaque release fonctionnelle peut introduire son propre plan détaillé lorsque la release s'ouvre.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
|
||||
<!-- version: 13 -->
|
||||
<!-- version: 14 -->
|
||||
|
||||
# Séquence des releases fonctionnelles KSP
|
||||
|
||||
@@ -219,7 +219,7 @@ pre.015 clôture
|
||||
|
||||
Cette prévision n'est pas un plafond : chaque prerelease doit rester une petite tranche, avec scission explicite si l'objectif dépasse environ 15–20 minutes de travail effectif.
|
||||
|
||||
`pre.006` a fermé le routing Logging structuré `domain`; `pre.007` a livré le moteur JSON/JSON Schema et `std.logging.json`; `pre.008` ajoute la résolution générique globals/profils/`default_profile` avec provenance top-level. Après validation utilisateur, `pre.009` introduira les compositions génériques par `file_id`.
|
||||
`pre.006` a fermé le routing Logging structuré `domain`; `pre.007` a livré le moteur JSON/JSON Schema et `std.logging.json`; `pre.008` a ajouté la résolution générique globals/profils/`default_profile`; `pre.009` ajoute les compositions génériques par `file_id`, avec `schema.composite` mais sans composite runtime fictif. Après validation utilisateur, `pre.010` introduira `.env`, process env et le resolver `${...}`.
|
||||
|
||||
## `0.1.4` — Config desktop par défaut
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<!-- file: docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md -->
|
||||
<!-- version: 11 -->
|
||||
<!-- version: 12 -->
|
||||
|
||||
# Plan `0.1.3` — Configuration foundation
|
||||
|
||||
## 1. Statut et objectif
|
||||
|
||||
Ce plan a été établi par `0.1.3-pre.001`, corrigé par `0.1.3-pre.001-fix.001/.002/.003`, puis exécuté par petites tranches. `pre.002` a livré le bootstrap Config, `pre.003` le registre `file_id`, `pre.004` les contrats publics multi-output de Logging, `pre.005` le runtime multi-sink sur niveau/target/formats, `pre.006` le routing structuré `domain`, `pre.007` le moteur JSON/JSON Schema et `pre.008` la résolution des globals/profils/`default_profile`. La prochaine tranche est `pre.009` pour les compositions génériques par `file_id`.
|
||||
Ce plan a été établi par `0.1.3-pre.001`, corrigé par `0.1.3-pre.001-fix.001/.002/.003`, puis exécuté par petites tranches. `pre.002` a livré le bootstrap Config, `pre.003` le registre `file_id`, `pre.004` les contrats publics multi-output de Logging, `pre.005` le runtime multi-sink sur niveau/target/formats, `pre.006` le routing structuré `domain`, `pre.007` le moteur JSON/JSON Schema, `pre.008` la résolution des globals/profils/`default_profile` et `pre.009` les compositions génériques par `file_id`. La prochaine tranche est `pre.010` pour `.env`, process env et le resolver `${...}`.
|
||||
|
||||
La base auditée reste la release stable `v0.1.2`.
|
||||
|
||||
@@ -409,7 +409,30 @@ config/composite.ksp-app-wallet-desk.json
|
||||
|
||||
Leur identité logique reste `cfg.composite.<consumer>`; le filename peut être remplacé par `--filemap` sans modifier leur contenu logique.
|
||||
|
||||
Aucun composite runtime fictif n'est créé avant l'existence d'un consumer concret. Le moteur et le schéma génériques peuvent néanmoins être testés dans `0.1.3` par fixtures/examples.
|
||||
Aucun composite runtime fictif n'est créé avant l'existence d'un consumer concret. Le moteur et le schéma génériques sont exercés par `config/examples/composite.example.json` et par un descriptor de test privé à `ksp-config-lib`.
|
||||
|
||||
Contrat générique livré en `pre.009` :
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": 1,
|
||||
"default_profile": "local",
|
||||
"profiles": [
|
||||
{
|
||||
"profile_id": "local",
|
||||
"documents": [
|
||||
{
|
||||
"component_id": "logging",
|
||||
"file_id": "cfg.std.logging",
|
||||
"profile_id": "local_dev"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`component_id` est local au profil composite et unique dans celui-ci. `file_id` référence exclusivement un document standard `cfg.std.*` connu du registre. `profile_id` est optionnel : absent, il conserve le `default_profile` autonome du document référencé ; présent, il sélectionne explicitement ce profil au nom du composite. Les composites ne référencent jamais les filenames physiques et ne s'imbriquent pas dans cette première surface.
|
||||
|
||||
### 6.6 Schémas
|
||||
|
||||
@@ -419,7 +442,7 @@ Les schémas appartiennent par défaut sous :
|
||||
config/schemas/
|
||||
```
|
||||
|
||||
Première surface candidate :
|
||||
Surface actuelle :
|
||||
|
||||
```text
|
||||
config/schemas/std.logging.schema.json
|
||||
@@ -1740,12 +1763,21 @@ La validation utilisateur de `pre.007` est acquise : `fmt/check/clippy/test` pas
|
||||
|
||||
### `0.1.3-pre.009` — compositions génériques par `file_id`
|
||||
|
||||
- contrat composite générique ;
|
||||
- schema/example composite ;
|
||||
- références de documents exclusivement par `file_id` ;
|
||||
- sélection/remplacement de profils documentaires ;
|
||||
- résolution composition -> document -> profil/global ;
|
||||
- aucun composite runtime fictif obligatoire.
|
||||
Tranche livrée :
|
||||
|
||||
- `schema.composite` est enregistré par défaut vers `config/schemas/composite.schema.json` et reste surchargeable par `--filemap` ;
|
||||
- aucun `cfg.composite.<consumer>` n’est enregistré tant qu’un consumer concret n’existe pas ;
|
||||
- `config/examples/composite.example.json` exerce le contrat sans devenir une source runtime implicite ;
|
||||
- un composite utilise `format_version`, `default_profile` et `profiles[]` comme les documents standards profilés ;
|
||||
- chaque profil composite contient `documents[]` avec `component_id`, `file_id` et `profile_id` optionnel ;
|
||||
- `component_id` est unique dans un profil composite ;
|
||||
- les références ciblent exclusivement des `cfg.std.*` déjà enregistrés, jamais des filenames ni d’autres composites ;
|
||||
- `profile_id` absent laisse le document référencé choisir son `default_profile` autonome ;
|
||||
- `profile_id` présent impose ce profil et marque sa source de sélection `ConfigProfileSelectionSource::Composite` ;
|
||||
- `ResolvedConfigComposite` conserve le composite sélectionné et ses `ResolvedCompositeComponent`, chacun portant le `ResolvedConfigProfile` complet du document référencé ;
|
||||
- les provenances `Global` / `Profile` du document spécialisé sont donc conservées sous la composition ;
|
||||
- `ConfigDocumentEngine::load_resolved_composite(file_id, requested_profile)` est prêt pour les futurs descriptors `cfg.composite.<consumer>` ;
|
||||
- les tests utilisent un descriptor composite privé à la crate pointant vers l’exemple versionné afin de valider la résolution complète sans créer un composite runtime fictif.
|
||||
|
||||
### `0.1.3-pre.010` — `.env` + process env + resolver `${...}`
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/rules/FILE_CONTRACTS.md -->
|
||||
<!-- version: 10 -->
|
||||
<!-- version: 11 -->
|
||||
|
||||
# Contrats des fichiers
|
||||
|
||||
@@ -38,11 +38,12 @@ Les règles `FILE-*` définissent la responsabilité et le mode de modification
|
||||
|
||||
## Répertoire `config/`
|
||||
|
||||
| Fichier/famille | Responsabilité | Règle de modification |
|
||||
|--------------------------------|------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `config/std.<domain>.json` | Document runtime spécialisé Config possédé par `ksp-config-lib`. | Identifié par un `file_id` stable, validé par son schema enregistré et lu/modifié uniquement via Config. Comme JSON ne porte pas de commentaires de header, la version du format appartient au champ JSON `format_version`. |
|
||||
| `config/schemas/*.schema.json` | JSON Schema des documents Config gérés. | Identifié par un `file_id` `schema.*`; le schema doit être valide pour le draft déclaré avant validation d'une instance. Aucun secret/runtime local ne doit y apparaître. |
|
||||
| `config/examples/*.json` | Exemples versionnés séparés des vrais fichiers runtime. | Doivent rester schema-valides et illustratifs ; ils ne constituent jamais une source runtime implicite. |
|
||||
| Fichier/famille | Responsabilité | Règle de modification |
|
||||
|------------------------------------|------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `config/std.<domain>.json` | Document runtime spécialisé Config possédé par `ksp-config-lib`. | Identifié par un `file_id` stable, validé par son schema enregistré et lu/modifié uniquement via Config. Comme JSON ne porte pas de commentaires de header, la version du format appartient au champ JSON `format_version`. |
|
||||
| `config/composite.<consumer>.json` | Composition runtime propre à un consumer concret. | Référence uniquement des documents standards par `file_id`; son descriptor référence `schema.composite` tant qu’un schema spécialisé n’est pas requis. Aucun composite runtime fictif n’est créé avant l’existence du consumer. |
|
||||
| `config/schemas/*.schema.json` | JSON Schema des documents Config gérés. | Identifié par un `file_id` `schema.*`; le schema doit être valide pour le draft déclaré avant validation d'une instance. Aucun secret/runtime local ne doit y apparaître. |
|
||||
| `config/examples/*.json` | Exemples versionnés séparés des vrais fichiers runtime. | Doivent rester schema-valides et illustratifs ; ils ne constituent jamais une source runtime implicite. |
|
||||
|
||||
Les noms physiques sont remplaçables via le registre Config lorsque le contrat le permet ; les consumers référencent les documents par `file_id`, pas par filename.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user