v0.1.3-pre.008

This commit is contained in:
2026-08-15 21:20:56 +02:00
parent 96753e4ba1
commit d1196c03e5
13 changed files with 728 additions and 25 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/document.rs
// version: 1
// version: 2
/// A Config-managed JSON document that has passed syntax, schema and current semantic validation.
#[derive(Clone, Debug, PartialEq)]
@@ -208,6 +208,10 @@ fn validate_instance(document: &ConfigJsonDocument, schema: &ConfigJsonDocument)
}
fn validate_document_semantics(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);
}
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: 3
// version: 4
/// 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");
@@ -33,3 +33,6 @@ pub const ERROR_CODE_SCHEMA_VALIDATION_FAILED: ksp_core_lib::ErrorCode = ksp_cor
/// Error code used when a schema-valid Config document violates KSP semantic invariants for its document type.
pub const ERROR_CODE_DOCUMENT_SEMANTIC_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "document_semantic_invalid");
/// 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");

View File

@@ -1,18 +1,19 @@
// file: crates/ksp-config-lib/src/lib.rs
// version: 3
// version: 4
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! KSP-owned application configuration facade.
//!
//! `0.1.3-pre.007` owns the non-recursive bootstrap roots, stable logical file registry and first generic JSON/JSON Schema loading pipeline. The standard
//! Logging document is the first registered runtime document. Profile resolution, environment substitution, sensitivity and persistence remain in later bounded
//! `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.
mod bootstrap;
mod document;
mod error;
mod profile;
mod registry;
/// Bootstrap argument used to replace the configuration document root.
@@ -47,10 +48,18 @@ pub use self::error::ERROR_CODE_FILE_MAPPING_INVALID;
pub use self::error::ERROR_CODE_JSON_FILE_READ_FAILED;
/// Error code used when a Config-managed file contains invalid JSON syntax.
pub use self::error::ERROR_CODE_JSON_SYNTAX_INVALID;
/// Error code used when an explicitly requested Config profile does not exist.
pub use self::error::ERROR_CODE_PROFILE_NOT_FOUND;
/// Error code used when a JSON Schema document is itself invalid.
pub use self::error::ERROR_CODE_SCHEMA_INVALID;
/// Error code used when a Config document fails its registered JSON Schema validation.
pub use self::error::ERROR_CODE_SCHEMA_VALIDATION_FAILED;
/// Source that selected an effective standard Config profile.
pub use self::profile::ConfigProfileSelectionSource;
/// Origin of one top-level value in a resolved standard Config profile.
pub use self::profile::ConfigValueOrigin;
/// Validated standard Config document resolved to one profile with global/profile provenance.
pub use self::profile::ResolvedConfigProfile;
/// Bootstrap argument used to replace a known Config filename mapping.
pub use self::registry::ARG_FILE_MAP;
/// Logical descriptor associating a stable file identifier with its physical filename and validation schema.

View File

@@ -0,0 +1,236 @@
// file: crates/ksp-config-lib/src/profile.rs
// version: 1
/// Origin of one top-level value in a resolved standard Config profile.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ConfigValueOrigin {
/// Value comes from the global section of the specialized document.
Global,
/// Value comes from the selected profile object.
Profile,
}
/// Source that selected the effective profile.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ConfigProfileSelectionSource {
/// The document's autonomous `default_profile` selected the profile.
DefaultProfile,
/// A caller explicitly requested the profile by `profile_id`.
Explicit,
}
/// Validated standard document resolved to one profile while retaining global/profile provenance.
#[derive(Clone, Debug, PartialEq)]
pub struct ResolvedConfigProfile {
file_id: crate::ConfigFileId,
path: std::path::PathBuf,
profile_id: String,
selection_source: ConfigProfileSelectionSource,
globals: serde_json::Map<String, serde_json::Value>,
profile: serde_json::Map<String, serde_json::Value>,
effective: serde_json::Map<String, serde_json::Value>,
origins: std::collections::BTreeMap<String, ConfigValueOrigin>,
}
impl ResolvedConfigProfile {
/// Returns the logical document identifier from which this profile was resolved.
#[must_use]
pub fn file_id(&self) -> &crate::ConfigFileId {
return &self.file_id;
}
/// Returns the physical path of the validated source document.
#[must_use]
pub fn path(&self) -> &std::path::Path {
return self.path.as_path();
}
/// Returns the selected unique profile identifier.
#[must_use]
pub fn profile_id(&self) -> &str {
return self.profile_id.as_str();
}
/// Returns whether selection came from `default_profile` or an explicit caller request.
#[must_use]
pub const fn selection_source(&self) -> ConfigProfileSelectionSource {
return self.selection_source;
}
/// Returns document-global values, excluding the reserved `default_profile` and `profiles` keys.
#[must_use]
pub fn globals(&self) -> &serde_json::Map<String, serde_json::Value> {
return &self.globals;
}
/// Returns the selected profile object including its `profile_id`.
#[must_use]
pub fn profile(&self) -> &serde_json::Map<String, serde_json::Value> {
return &self.profile;
}
/// Returns a deterministic top-level effective view in which selected profile keys override same-named global keys.
#[must_use]
pub fn effective(&self) -> &serde_json::Map<String, serde_json::Value> {
return &self.effective;
}
/// Returns the top-level provenance for an effective key.
#[must_use]
pub fn origin(&self, key: &str) -> std::option::Option<ConfigValueOrigin> {
return self.origins.get(key).copied();
}
}
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.
pub fn load_resolved_profile(
&self,
file_id: &crate::ConfigFileId,
requested_profile: std::option::Option<&str>,
) -> ksp_core_lib::Result<ResolvedConfigProfile> {
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),
};
return resolve_document_profile(&document, requested_profile);
}
}
pub(crate) fn validate_document_profile_contract(document: &crate::ConfigJsonDocument) -> ksp_core_lib::Result<()> {
let root = match document.value().as_object() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(()),
};
let default_profile = root.get("default_profile");
let profiles = root.get("profiles");
if default_profile.is_none() && profiles.is_none() {
return std::result::Result::Ok(());
}
let default_profile = match default_profile.and_then(serde_json::Value::as_str) {
std::option::Option::Some(value) if !value.trim().is_empty() => value,
_ => return std::result::Result::Err(profile_semantic_error(document, "default_profile must identify a non-empty profile_id")),
};
let profiles = match profiles.and_then(serde_json::Value::as_array) {
std::option::Option::Some(value) if !value.is_empty() => value,
_ => return std::result::Result::Err(profile_semantic_error(document, "profiles must contain at least one profile object")),
};
let mut ids = std::collections::BTreeSet::<String>::new();
let mut default_found = false;
for (index, profile) in profiles.iter().enumerate() {
let object = match profile.as_object() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(
profile_semantic_error(document, "profile entry must be an object").with_context("profile_index", index.to_string()),
);
},
};
let profile_id = match object.get("profile_id").and_then(serde_json::Value::as_str) {
std::option::Option::Some(value) if !value.trim().is_empty() => value,
_ => {
return std::result::Result::Err(
profile_semantic_error(document, "profile entry must declare a non-empty profile_id").with_context("profile_index", index.to_string()),
);
},
};
if ids.contains(profile_id) {
return std::result::Result::Err(
profile_semantic_error(document, "profile_id values must be unique")
.with_context("profile_index", index.to_string())
.with_context("profile_id", profile_id),
);
}
ids.insert(profile_id.to_owned());
if profile_id == default_profile {
default_found = true;
}
}
if !default_found {
return std::result::Result::Err(
profile_semantic_error(document, "default_profile must reference an existing profile_id").with_context("default_profile", default_profile),
);
}
return std::result::Result::Ok(());
}
fn resolve_document_profile(document: &crate::ConfigJsonDocument, requested_profile: std::option::Option<&str>) -> 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")),
};
let default_profile = match root.get("default_profile").and_then(serde_json::Value::as_str) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(profile_semantic_error(document, "profile resolution requires default_profile")),
};
let (selected_profile, selection_source) = match requested_profile {
std::option::Option::Some(value) => (value, ConfigProfileSelectionSource::Explicit),
std::option::Option::None => (default_profile, ConfigProfileSelectionSource::DefaultProfile),
};
let profiles = match root.get("profiles").and_then(serde_json::Value::as_array) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(profile_semantic_error(document, "profile resolution requires profiles")),
};
let mut selected: std::option::Option<&serde_json::Map<String, serde_json::Value>> = std::option::Option::None;
for profile in profiles {
let object = match profile.as_object() {
std::option::Option::Some(value) => value,
std::option::Option::None => continue,
};
if object.get("profile_id").and_then(serde_json::Value::as_str) == std::option::Option::Some(selected_profile) {
selected = std::option::Option::Some(object);
break;
}
}
let selected = match selected {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_PROFILE_NOT_FOUND, "requested Config profile does not exist")
.with_context("file_id", document.file_id().as_str())
.with_context("path", document.path().to_string_lossy().into_owned())
.with_context("profile_id", selected_profile),
);
},
};
let mut globals = serde_json::Map::<String, serde_json::Value>::new();
let mut effective = serde_json::Map::<String, serde_json::Value>::new();
let mut origins = std::collections::BTreeMap::<String, ConfigValueOrigin>::new();
for (key, value) in root {
if key != "default_profile" && key != "profiles" {
globals.insert(key.clone(), value.clone());
effective.insert(key.clone(), value.clone());
origins.insert(key.clone(), ConfigValueOrigin::Global);
}
}
let profile = (*selected).clone();
for (key, value) in &profile {
effective.insert(key.clone(), value.clone());
origins.insert(key.clone(), ConfigValueOrigin::Profile);
}
return std::result::Result::Ok(ResolvedConfigProfile {
file_id: document.file_id().clone(),
path: document.path().to_path_buf(),
profile_id: selected_profile.to_owned(),
selection_source,
globals,
profile,
effective,
origins,
});
}
fn profile_semantic_error(document: &crate::ConfigJsonDocument, reason: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID, "Config document violates KSP profile invariants")
.with_context("file_id", document.file_id().as_str())
.with_context("path", document.path().to_string_lossy().into_owned())
.with_context("reason", reason);
}
#[cfg(test)]
#[path = "../unit_tests/profile.rs"]
mod tests;

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-config-lib/tests/public_api.rs
// version: 4
// version: 5
//! Integration tests for the public `ksp-config-lib` bootstrap, logical file registry and validated JSON document contracts.
//! Integration tests for the public `ksp-config-lib` bootstrap, file registry, validated JSON document and profile-resolution contracts.
#[test]
fn bootstrap_contract_is_available_from_crate_root() {
@@ -92,3 +92,27 @@ fn validated_json_document_engine_is_available_from_crate_root() {
}
}
}
#[test]
fn resolved_profile_contract_is_available_from_crate_root() {
let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
let registry = ksp_config_lib::ConfigFileRegistry::defaults();
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_STD_LOGGING);
assert!(bootstrap.is_ok(), "public bootstrap should accept committed Config roots: {bootstrap:?}");
assert!(registry.is_ok(), "public registry should remain constructible: {registry:?}");
assert!(file_id.is_ok(), "public Logging file_id should remain constructible: {file_id:?}");
if let (std::result::Result::Ok(bootstrap), std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) = (bootstrap, registry, file_id) {
let engine = ksp_config_lib::ConfigDocumentEngine::new(bootstrap, registry);
let resolved = engine.load_resolved_profile(&file_id, std::option::Option::None);
assert!(resolved.is_ok(), "public profile resolver should resolve committed default profile: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved {
assert_eq!(resolved.profile_id(), "local_dev");
assert_eq!(resolved.selection_source(), ksp_config_lib::ConfigProfileSelectionSource::DefaultProfile);
assert_eq!(resolved.origin("logs_directory"), std::option::Option::Some(ksp_config_lib::ConfigValueOrigin::Global));
assert_eq!(resolved.origin("default_filter"), std::option::Option::Some(ksp_config_lib::ConfigValueOrigin::Profile));
assert!(!resolved.effective().contains_key("default_profile"));
assert!(!resolved.effective().contains_key("profiles"));
}
}
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/unit_tests/document.rs
// version: 1
// version: 2
#[test]
fn committed_logging_document_passes_registered_schema_and_semantic_validation() {
@@ -143,6 +143,85 @@ fn schema_valid_logging_document_can_still_fail_ksp_semantics() {
cleanup_fixture(&fixture);
}
#[test]
fn duplicate_profile_ids_are_semantically_invalid() {
let fixture = fixture_roots("duplicate-profile-id");
let workspace = workspace_root();
let schema_source = std::fs::read_to_string(workspace.join("config/schemas/std.logging.schema.json"));
assert!(schema_source.is_ok(), "committed Logging schema should be readable: {schema_source:?}");
if let std::result::Result::Ok(schema_source) = schema_source {
let source = valid_logging_source_with_profiles("first", "first", "first");
let prepared = prepare_fixture(&fixture, std::option::Option::Some(source.as_str()), schema_source.as_str());
assert!(prepared.is_ok(), "duplicate profile fixture should be writable: {prepared:?}");
if prepared.is_ok() {
let result = load_fixture(&fixture);
assert_error_code(result, crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID);
}
}
cleanup_fixture(&fixture);
}
#[test]
fn missing_default_profile_target_is_semantically_invalid() {
let fixture = fixture_roots("missing-default-profile");
let workspace = workspace_root();
let schema_source = std::fs::read_to_string(workspace.join("config/schemas/std.logging.schema.json"));
assert!(schema_source.is_ok(), "committed Logging schema should be readable: {schema_source:?}");
if let std::result::Result::Ok(schema_source) = schema_source {
let source = valid_logging_source_with_profiles("missing", "first", "second");
let prepared = prepare_fixture(&fixture, std::option::Option::Some(source.as_str()), schema_source.as_str());
assert!(prepared.is_ok(), "missing default profile fixture should be writable: {prepared:?}");
if prepared.is_ok() {
let result = load_fixture(&fixture);
assert_error_code(result, crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID);
}
}
cleanup_fixture(&fixture);
}
fn valid_logging_source_with_profiles(default_profile: &str, first_profile: &str, second_profile: &str) -> String {
return format!(
r#"{{
"format_version": 1,
"logs_directory": "logs",
"default_profile": "{default_profile}",
"profiles": [
{},
{}
]
}}"#,
valid_logging_profile(first_profile, "file.first"),
valid_logging_profile(second_profile, "file.second")
);
}
fn valid_logging_profile(profile_id: &str, output_id: &str) -> String {
return format!(
r#"{{
"profile_id": "{profile_id}",
"default_filter": "info",
"span_events": "off",
"console": {{
"enabled": false,
"output": "stderr",
"ansi": false,
"format": "human",
"filter": {{"level": "trace", "targets": ["*"], "domains": ["*"]}}
}},
"files": [{{
"output_id": "{output_id}",
"enabled": true,
"path": "output.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"filter": {{"level": "info", "targets": ["*"], "domains": ["*"]}}
}}],
"target_filters": []
}}"#
);
}
fn load_fixture(fixture: &FixtureRoots) -> ksp_core_lib::Result<super::ConfigJsonDocument> {
let bootstrap = crate::ConfigBootstrapOptions::from_paths(fixture.config.as_path(), fixture.schemas.as_path());
let bootstrap = match bootstrap {
@@ -212,7 +291,7 @@ struct FixtureRoots {
fn fixture_roots(name: &str) -> FixtureRoots {
let mut root = std::env::temp_dir();
root.push(format!("ksp-config-lib-pre007-{name}-{}", std::process::id()));
root.push(format!("ksp-config-lib-pre008-{name}-{}", std::process::id()));
return FixtureRoots { config: root.join("config"), schemas: root.join("schemas"), root };
}

View File

@@ -0,0 +1,70 @@
// file: crates/ksp-config-lib/unit_tests/profile.rs
// version: 1
#[test]
fn committed_default_profile_resolves_globals_profile_and_provenance() {
let engine = committed_engine();
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
assert!(engine.is_ok(), "committed Config engine should be constructible: {engine:?}");
assert!(file_id.is_ok(), "logging 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_profile(&file_id, std::option::Option::None);
assert!(resolved.is_ok(), "default profile should resolve: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved {
assert_eq!(resolved.profile_id(), "local_dev");
assert_eq!(resolved.selection_source(), super::ConfigProfileSelectionSource::DefaultProfile);
assert_eq!(resolved.globals().get("logs_directory").and_then(serde_json::Value::as_str), std::option::Option::Some("${KSP_LOGS_DIRECTORY:-logs}"));
assert_eq!(resolved.profile().get("default_filter").and_then(serde_json::Value::as_str), std::option::Option::Some("warn"));
assert_eq!(resolved.effective().get("default_filter").and_then(serde_json::Value::as_str), std::option::Option::Some("warn"));
assert_eq!(resolved.origin("logs_directory"), std::option::Option::Some(super::ConfigValueOrigin::Global));
assert_eq!(resolved.origin("default_filter"), std::option::Option::Some(super::ConfigValueOrigin::Profile));
assert!(!resolved.effective().contains_key("default_profile"));
assert!(!resolved.effective().contains_key("profiles"));
}
}
}
#[test]
fn explicit_profile_selection_is_distinct_from_default_selection() {
let engine = committed_engine();
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
assert!(engine.is_ok(), "committed Config engine should be constructible: {engine:?}");
assert!(file_id.is_ok(), "logging 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_profile(&file_id, std::option::Option::Some("local_dev"));
assert!(resolved.is_ok(), "explicit committed profile should resolve: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved {
assert_eq!(resolved.profile_id(), "local_dev");
assert_eq!(resolved.selection_source(), super::ConfigProfileSelectionSource::Explicit);
}
}
}
#[test]
fn unknown_explicit_profile_has_distinct_error_code() {
let engine = committed_engine();
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
assert!(engine.is_ok(), "committed Config engine should be constructible: {engine:?}");
assert!(file_id.is_ok(), "logging 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_profile(&file_id, std::option::Option::Some("does-not-exist"));
assert!(result.is_err(), "unknown explicit profile must be rejected: {result:?}");
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_PROFILE_NOT_FOUND);
}
}
}
fn committed_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
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 = crate::ConfigFileRegistry::defaults();
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),
};
}