v0.1.3-pre.007
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 47
|
||||
# version: 48
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.3-pre.6"
|
||||
version = "0.1.3-pre.7"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
@@ -14,6 +14,9 @@ authors = ["SinuS von SifriduS <sinus@sasedev.net>"]
|
||||
publish = false
|
||||
|
||||
[workspace.dependencies]
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
serde_json = { version = "^1.0" }
|
||||
jsonschema = { version = "^0.49", default-features = false }
|
||||
solana-pubkey = { version = "^4.3", default-features = false }
|
||||
tracing = { version = "^0.1", default-features = false, features = ["std"] }
|
||||
tracing-subscriber = { version = "^0.3", default-features = false, features = ["fmt", "json", "ansi"] }
|
||||
|
||||
64
config/examples/std.logging.example.json
Normal file
64
config/examples/std.logging.example.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"logs_directory": "${KSP_LOGS_DIRECTORY:-logs}",
|
||||
"default_profile": "example",
|
||||
"profiles": [
|
||||
{
|
||||
"profile_id": "example",
|
||||
"default_filter": "info",
|
||||
"span_events": "new_and_close",
|
||||
"console": {
|
||||
"enabled": true,
|
||||
"output": "stdout",
|
||||
"ansi": true,
|
||||
"format": "pretty",
|
||||
"filter": {
|
||||
"level": "debug",
|
||||
"targets": [
|
||||
"*"
|
||||
],
|
||||
"domains": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"output_id": "file.all.info",
|
||||
"enabled": true,
|
||||
"path": "ksp-info.log",
|
||||
"rotation": "daily",
|
||||
"format": "compact",
|
||||
"ansi": false,
|
||||
"filter": {
|
||||
"level": "info",
|
||||
"targets": [
|
||||
"*"
|
||||
],
|
||||
"domains": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"output_id": "file.store.trace",
|
||||
"enabled": false,
|
||||
"path": "store/store-trace.jsonl",
|
||||
"rotation": "hourly",
|
||||
"format": "json",
|
||||
"ansi": false,
|
||||
"filter": {
|
||||
"level": "trace",
|
||||
"targets": [
|
||||
"*"
|
||||
],
|
||||
"domains": [
|
||||
"store"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"target_filters": []
|
||||
}
|
||||
]
|
||||
}
|
||||
266
config/schemas/std.logging.schema.json
Normal file
266
config/schemas/std.logging.schema.json
Normal file
@@ -0,0 +1,266 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "urn:ksp:schema:std.logging:v1",
|
||||
"title": "KSP standard Logging configuration",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"format_version",
|
||||
"logs_directory",
|
||||
"default_profile",
|
||||
"profiles"
|
||||
],
|
||||
"properties": {
|
||||
"format_version": {
|
||||
"const": 1
|
||||
},
|
||||
"logs_directory": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"default_profile": {
|
||||
"$ref": "#/$defs/profileId"
|
||||
},
|
||||
"profiles": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/profile"
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"profileId": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9][a-z0-9._-]*$"
|
||||
},
|
||||
"level": {
|
||||
"enum": [
|
||||
"off",
|
||||
"error",
|
||||
"warn",
|
||||
"info",
|
||||
"debug",
|
||||
"trace"
|
||||
]
|
||||
},
|
||||
"format": {
|
||||
"enum": [
|
||||
"human",
|
||||
"compact",
|
||||
"pretty",
|
||||
"json"
|
||||
]
|
||||
},
|
||||
"selectorList": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"contains": {
|
||||
"const": "*"
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"maxItems": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"targetSelectorList": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/selectorList"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"const": "*"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^ksp-"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"outputFilter": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"level",
|
||||
"targets",
|
||||
"domains"
|
||||
],
|
||||
"properties": {
|
||||
"level": {
|
||||
"$ref": "#/$defs/level"
|
||||
},
|
||||
"targets": {
|
||||
"$ref": "#/$defs/targetSelectorList"
|
||||
},
|
||||
"domains": {
|
||||
"$ref": "#/$defs/selectorList"
|
||||
}
|
||||
}
|
||||
},
|
||||
"console": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"enabled",
|
||||
"output",
|
||||
"ansi",
|
||||
"format",
|
||||
"filter"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"output": {
|
||||
"enum": [
|
||||
"stdout",
|
||||
"stderr"
|
||||
]
|
||||
},
|
||||
"ansi": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"format": {
|
||||
"$ref": "#/$defs/format"
|
||||
},
|
||||
"filter": {
|
||||
"$ref": "#/$defs/outputFilter"
|
||||
}
|
||||
},
|
||||
"not": {
|
||||
"properties": {
|
||||
"ansi": {
|
||||
"const": true
|
||||
},
|
||||
"format": {
|
||||
"const": "json"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"ansi",
|
||||
"format"
|
||||
]
|
||||
}
|
||||
},
|
||||
"outputId": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9][a-z0-9_-]*(\\.[a-z0-9][a-z0-9_-]*)*$"
|
||||
},
|
||||
"file": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"output_id",
|
||||
"enabled",
|
||||
"path",
|
||||
"rotation",
|
||||
"format",
|
||||
"ansi",
|
||||
"filter"
|
||||
],
|
||||
"properties": {
|
||||
"output_id": {
|
||||
"$ref": "#/$defs/outputId"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"rotation": {
|
||||
"enum": [
|
||||
"never",
|
||||
"hourly",
|
||||
"daily"
|
||||
]
|
||||
},
|
||||
"format": {
|
||||
"$ref": "#/$defs/format"
|
||||
},
|
||||
"ansi": {
|
||||
"const": false
|
||||
},
|
||||
"filter": {
|
||||
"$ref": "#/$defs/outputFilter"
|
||||
}
|
||||
}
|
||||
},
|
||||
"targetFilter": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"target_prefix",
|
||||
"level"
|
||||
],
|
||||
"properties": {
|
||||
"target_prefix": {
|
||||
"type": "string",
|
||||
"pattern": "^ksp-"
|
||||
},
|
||||
"level": {
|
||||
"$ref": "#/$defs/level"
|
||||
}
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"profile_id",
|
||||
"default_filter",
|
||||
"span_events",
|
||||
"console",
|
||||
"files",
|
||||
"target_filters"
|
||||
],
|
||||
"properties": {
|
||||
"profile_id": {
|
||||
"$ref": "#/$defs/profileId"
|
||||
},
|
||||
"default_filter": {
|
||||
"$ref": "#/$defs/level"
|
||||
},
|
||||
"span_events": {
|
||||
"enum": [
|
||||
"off",
|
||||
"new_and_close",
|
||||
"full"
|
||||
]
|
||||
},
|
||||
"console": {
|
||||
"$ref": "#/$defs/console"
|
||||
},
|
||||
"files": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/file"
|
||||
}
|
||||
},
|
||||
"target_filters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/targetFilter"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
73
config/std.logging.json
Normal file
73
config/std.logging.json
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"logs_directory": "${KSP_LOGS_DIRECTORY:-logs}",
|
||||
"default_profile": "local_dev",
|
||||
"profiles": [
|
||||
{
|
||||
"profile_id": "local_dev",
|
||||
"default_filter": "warn",
|
||||
"span_events": "new_and_close",
|
||||
"console": {
|
||||
"enabled": true,
|
||||
"output": "stderr",
|
||||
"ansi": true,
|
||||
"format": "compact",
|
||||
"filter": {
|
||||
"level": "debug",
|
||||
"targets": [
|
||||
"*"
|
||||
],
|
||||
"domains": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"output_id": "file.all.debug",
|
||||
"enabled": true,
|
||||
"path": "debug/ksp-debug.log",
|
||||
"rotation": "daily",
|
||||
"format": "human",
|
||||
"ansi": false,
|
||||
"filter": {
|
||||
"level": "debug",
|
||||
"targets": [
|
||||
"*"
|
||||
],
|
||||
"domains": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"output_id": "file.config.error",
|
||||
"enabled": true,
|
||||
"path": "config/ksp-config-errors.jsonl",
|
||||
"rotation": "daily",
|
||||
"format": "json",
|
||||
"ansi": false,
|
||||
"filter": {
|
||||
"level": "error",
|
||||
"targets": [
|
||||
"ksp-config-lib"
|
||||
],
|
||||
"domains": [
|
||||
"config"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"target_filters": [
|
||||
{
|
||||
"target_prefix": "ksp-config-lib",
|
||||
"level": "trace"
|
||||
},
|
||||
{
|
||||
"target_prefix": "ksp-logging-lib",
|
||||
"level": "debug"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
# file: crates/ksp-config-lib/Cargo.toml
|
||||
# version: 1
|
||||
# version: 2
|
||||
|
||||
[package]
|
||||
name = "ksp-config-lib"
|
||||
@@ -9,6 +9,9 @@ repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
jsonschema.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
451
crates/ksp-config-lib/src/document.rs
Normal file
451
crates/ksp-config-lib/src/document.rs
Normal file
@@ -0,0 +1,451 @@
|
||||
// file: crates/ksp-config-lib/src/document.rs
|
||||
// version: 1
|
||||
|
||||
/// A Config-managed JSON document that has passed syntax, schema and current semantic validation.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ConfigJsonDocument {
|
||||
file_id: crate::ConfigFileId,
|
||||
path: std::path::PathBuf,
|
||||
value: serde_json::Value,
|
||||
}
|
||||
|
||||
impl ConfigJsonDocument {
|
||||
/// Returns the logical Config file identifier used to load this document.
|
||||
#[must_use]
|
||||
pub fn file_id(&self) -> &crate::ConfigFileId {
|
||||
return &self.file_id;
|
||||
}
|
||||
|
||||
/// Returns the resolved physical path from which this document was loaded.
|
||||
#[must_use]
|
||||
pub fn path(&self) -> &std::path::Path {
|
||||
return self.path.as_path();
|
||||
}
|
||||
|
||||
/// Returns the validated JSON value without transferring Config ownership of file I/O or validation.
|
||||
#[must_use]
|
||||
pub fn value(&self) -> &serde_json::Value {
|
||||
return &self.value;
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic JSON/JSON Schema engine owned by `ksp-config-lib`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ConfigDocumentEngine {
|
||||
bootstrap: crate::ConfigBootstrapOptions,
|
||||
registry: crate::ConfigFileRegistry,
|
||||
}
|
||||
|
||||
impl ConfigDocumentEngine {
|
||||
/// Creates a document engine from already validated bootstrap options and a logical file registry.
|
||||
#[must_use]
|
||||
pub fn new(bootstrap: crate::ConfigBootstrapOptions, registry: crate::ConfigFileRegistry) -> Self {
|
||||
return Self { bootstrap, registry };
|
||||
}
|
||||
|
||||
/// Returns the bootstrap roots used by this engine.
|
||||
#[must_use]
|
||||
pub const fn bootstrap(&self) -> &crate::ConfigBootstrapOptions {
|
||||
return &self.bootstrap;
|
||||
}
|
||||
|
||||
/// Returns the logical file registry used by this engine.
|
||||
#[must_use]
|
||||
pub const fn registry(&self) -> &crate::ConfigFileRegistry {
|
||||
return &self.registry;
|
||||
}
|
||||
|
||||
/// Loads one registered Config document and validates it against its registered JSON Schema and current KSP semantic invariants.
|
||||
pub fn load_validated_document(&self, file_id: &crate::ConfigFileId) -> ksp_core_lib::Result<ConfigJsonDocument> {
|
||||
let descriptor = self.registry.descriptor(file_id);
|
||||
let descriptor = match descriptor {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if descriptor.kind() != crate::ConfigFileKind::Config {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_FILE_MAPPING_INVALID, "requested file_id does not identify a Config document")
|
||||
.with_context("file_id", file_id.as_str()),
|
||||
);
|
||||
}
|
||||
let schema_file_id = match descriptor.schema_file_id() {
|
||||
std::option::Option::Some(value) => value.clone(),
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_FILE_MAPPING_INVALID, "Config document has no registered validation schema")
|
||||
.with_context("file_id", file_id.as_str()),
|
||||
);
|
||||
},
|
||||
};
|
||||
let document = self.load_json(file_id);
|
||||
let document = match document {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let schema = self.load_json(&schema_file_id);
|
||||
let schema = match schema {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let schema_validation = validate_schema_document(&schema);
|
||||
match schema_validation {
|
||||
std::result::Result::Ok(()) => {},
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
let instance_validation = validate_instance(&document, &schema);
|
||||
match instance_validation {
|
||||
std::result::Result::Ok(()) => {},
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
let semantic_validation = validate_document_semantics(&document);
|
||||
return match semantic_validation {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(document),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
fn load_json(&self, file_id: &crate::ConfigFileId) -> ksp_core_lib::Result<ConfigJsonDocument> {
|
||||
let path = self.registry.resolve_path(&self.bootstrap, file_id);
|
||||
let path = match path {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let content = std::fs::read_to_string(path.as_path());
|
||||
let content = match content {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(json_read_error(file_id, &path, error)),
|
||||
};
|
||||
let value = serde_json::from_str::<serde_json::Value>(content.as_str());
|
||||
return match value {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(ConfigJsonDocument { file_id: file_id.clone(), path, value }),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(json_syntax_error(file_id, &path, error)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct LoggingDocumentSource {
|
||||
format_version: u32,
|
||||
logs_directory: String,
|
||||
default_profile: String,
|
||||
profiles: std::vec::Vec<LoggingProfileSource>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct LoggingProfileSource {
|
||||
profile_id: String,
|
||||
default_filter: String,
|
||||
span_events: String,
|
||||
console: LoggingConsoleSource,
|
||||
files: std::vec::Vec<LoggingFileSource>,
|
||||
target_filters: std::vec::Vec<LoggingTargetFilterSource>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct LoggingConsoleSource {
|
||||
enabled: bool,
|
||||
output: String,
|
||||
ansi: bool,
|
||||
format: String,
|
||||
filter: LoggingOutputFilterSource,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct LoggingFileSource {
|
||||
output_id: String,
|
||||
enabled: bool,
|
||||
path: String,
|
||||
rotation: String,
|
||||
format: String,
|
||||
ansi: bool,
|
||||
filter: LoggingOutputFilterSource,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct LoggingOutputFilterSource {
|
||||
level: String,
|
||||
targets: std::vec::Vec<String>,
|
||||
domains: std::vec::Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct LoggingTargetFilterSource {
|
||||
target_prefix: String,
|
||||
level: String,
|
||||
}
|
||||
|
||||
fn validate_schema_document(schema: &ConfigJsonDocument) -> ksp_core_lib::Result<()> {
|
||||
let validation = jsonschema::meta::validate(schema.value());
|
||||
return match validation {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_SCHEMA_INVALID, "Config JSON Schema document is invalid")
|
||||
.with_context("file_id", schema.file_id().as_str())
|
||||
.with_context("path", schema.path().to_string_lossy().into_owned())
|
||||
.with_context("detail", error.to_string()),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn validate_instance(document: &ConfigJsonDocument, schema: &ConfigJsonDocument) -> ksp_core_lib::Result<()> {
|
||||
let validation = jsonschema::draft202012::validate(schema.value(), document.value());
|
||||
return match validation {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_SCHEMA_VALIDATION_FAILED, "Config document does not satisfy its registered JSON Schema")
|
||||
.with_context("file_id", document.file_id().as_str())
|
||||
.with_context("schema_file_id", schema.file_id().as_str())
|
||||
.with_context("path", document.path().to_string_lossy().into_owned())
|
||||
.with_context("detail", error.to_string()),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn validate_document_semantics(document: &ConfigJsonDocument) -> ksp_core_lib::Result<()> {
|
||||
if document.file_id().as_str() != crate::FILE_ID_STD_LOGGING {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
let parsed = serde_json::from_value::<LoggingDocumentSource>(document.value().clone());
|
||||
let parsed = match parsed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
semantic_error(document, "schema-valid Logging document cannot be decoded into the KSP source contract").with_source(error),
|
||||
);
|
||||
},
|
||||
};
|
||||
return validate_logging_document(document, &parsed);
|
||||
}
|
||||
|
||||
fn validate_logging_document(document: &ConfigJsonDocument, source: &LoggingDocumentSource) -> ksp_core_lib::Result<()> {
|
||||
if source.format_version != 1 {
|
||||
return std::result::Result::Err(semantic_error(document, "unsupported Logging document format_version"));
|
||||
}
|
||||
if source.logs_directory.trim().is_empty() {
|
||||
return std::result::Result::Err(semantic_error(document, "logs_directory must not be empty"));
|
||||
}
|
||||
if source.default_profile.trim().is_empty() {
|
||||
return std::result::Result::Err(semantic_error(document, "default_profile must not be empty"));
|
||||
}
|
||||
for (profile_index, profile) in source.profiles.iter().enumerate() {
|
||||
let validation = validate_logging_profile(document, profile, profile_index);
|
||||
if let std::result::Result::Err(error) = validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn validate_logging_profile(document: &ConfigJsonDocument, profile: &LoggingProfileSource, profile_index: usize) -> ksp_core_lib::Result<()> {
|
||||
if profile.profile_id.trim().is_empty() || profile.default_filter.trim().is_empty() || profile.span_events.trim().is_empty() {
|
||||
return std::result::Result::Err(
|
||||
semantic_error(document, "Logging profile identity and base settings must not be empty").with_context("profile_index", profile_index.to_string()),
|
||||
);
|
||||
}
|
||||
let console_validation = validate_logging_console(document, &profile.console, profile_index);
|
||||
if let std::result::Result::Err(error) = console_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
for (file_index, file) in profile.files.iter().enumerate() {
|
||||
let file_validation = validate_logging_file(document, file, profile_index, file_index);
|
||||
if let std::result::Result::Err(error) = file_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
for previous in &profile.files[..file_index] {
|
||||
if previous.output_id == file.output_id {
|
||||
return std::result::Result::Err(
|
||||
semantic_error(document, "Logging file output identifiers must be unique within a profile")
|
||||
.with_context("profile_index", profile_index.to_string())
|
||||
.with_context("output_id", file.output_id.as_str()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (target_index, target_filter) in profile.target_filters.iter().enumerate() {
|
||||
if target_filter.target_prefix.trim().is_empty() || !target_filter.target_prefix.starts_with("ksp-") || target_filter.level.trim().is_empty() {
|
||||
return std::result::Result::Err(
|
||||
semantic_error(document, "Logging global target filter must identify a KSP-owned target")
|
||||
.with_context("profile_index", profile_index.to_string())
|
||||
.with_context("target_filter_index", target_index.to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn validate_logging_console(document: &ConfigJsonDocument, console: &LoggingConsoleSource, profile_index: usize) -> ksp_core_lib::Result<()> {
|
||||
let _enabled = console.enabled;
|
||||
if console.output.trim().is_empty() || console.format.trim().is_empty() {
|
||||
return std::result::Result::Err(
|
||||
semantic_error(document, "Logging console output and format must not be empty").with_context("profile_index", profile_index.to_string()),
|
||||
);
|
||||
}
|
||||
if console.ansi && console.format == "json" {
|
||||
return std::result::Result::Err(
|
||||
semantic_error(document, "ANSI formatting is not compatible with JSON console output").with_context("profile_index", profile_index.to_string()),
|
||||
);
|
||||
}
|
||||
return validate_logging_output_filter(document, &console.filter, profile_index, "console");
|
||||
}
|
||||
|
||||
fn validate_logging_file(document: &ConfigJsonDocument, file: &LoggingFileSource, profile_index: usize, file_index: usize) -> ksp_core_lib::Result<()> {
|
||||
let _enabled = file.enabled;
|
||||
if !valid_output_id(file.output_id.as_str()) {
|
||||
return std::result::Result::Err(
|
||||
semantic_error(document, "Logging file output_id is invalid")
|
||||
.with_context("profile_index", profile_index.to_string())
|
||||
.with_context("file_index", file_index.to_string())
|
||||
.with_context("output_id", file.output_id.as_str()),
|
||||
);
|
||||
}
|
||||
if file.path.trim().is_empty() || !relative_log_path_is_valid(file.path.as_str()) {
|
||||
return std::result::Result::Err(
|
||||
semantic_error(document, "Logging file path must stay relative to logs_directory without traversal")
|
||||
.with_context("profile_index", profile_index.to_string())
|
||||
.with_context("file_index", file_index.to_string()),
|
||||
);
|
||||
}
|
||||
if file.rotation.trim().is_empty() || file.format.trim().is_empty() {
|
||||
return std::result::Result::Err(
|
||||
semantic_error(document, "Logging file rotation and format must not be empty").with_context("profile_index", profile_index.to_string()),
|
||||
);
|
||||
}
|
||||
if file.ansi {
|
||||
return std::result::Result::Err(
|
||||
semantic_error(document, "ANSI sequences are not allowed in persistent Logging outputs").with_context("profile_index", profile_index.to_string()),
|
||||
);
|
||||
}
|
||||
return validate_logging_output_filter(document, &file.filter, profile_index, file.output_id.as_str());
|
||||
}
|
||||
|
||||
fn validate_logging_output_filter(
|
||||
document: &ConfigJsonDocument,
|
||||
filter: &LoggingOutputFilterSource,
|
||||
profile_index: usize,
|
||||
output: &str,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
if filter.level.trim().is_empty() {
|
||||
return std::result::Result::Err(
|
||||
semantic_error(document, "Logging output filter level must not be empty").with_context("profile_index", profile_index.to_string()),
|
||||
);
|
||||
}
|
||||
let targets = validate_selectors(document, &filter.targets, true, profile_index, output, "targets");
|
||||
if let std::result::Result::Err(error) = targets {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return validate_selectors(document, &filter.domains, false, profile_index, output, "domains");
|
||||
}
|
||||
|
||||
fn validate_selectors(
|
||||
document: &ConfigJsonDocument,
|
||||
selectors: &[String],
|
||||
target_dimension: bool,
|
||||
profile_index: usize,
|
||||
output: &str,
|
||||
dimension: &'static str,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
if selectors.is_empty() {
|
||||
return std::result::Result::Err(selector_error(document, profile_index, output, dimension, "selector list must not be empty"));
|
||||
}
|
||||
if selectors.len() > 1
|
||||
&& selectors.iter().any(|selector| -> bool {
|
||||
return selector == "*";
|
||||
})
|
||||
{
|
||||
return std::result::Result::Err(selector_error(document, profile_index, output, dimension, "wildcard selector must be used alone"));
|
||||
}
|
||||
for (index, selector) in selectors.iter().enumerate() {
|
||||
if selector.trim().is_empty() {
|
||||
return std::result::Result::Err(
|
||||
selector_error(document, profile_index, output, dimension, "selector must not be empty").with_context("selector_index", index.to_string()),
|
||||
);
|
||||
}
|
||||
if target_dimension && selector != "*" && !selector.starts_with("ksp-") {
|
||||
return std::result::Result::Err(
|
||||
selector_error(document, profile_index, output, dimension, "target selector must identify a KSP-owned target")
|
||||
.with_context("selector_index", index.to_string()),
|
||||
);
|
||||
}
|
||||
for previous in &selectors[..index] {
|
||||
if previous == selector {
|
||||
return std::result::Result::Err(
|
||||
selector_error(document, profile_index, output, dimension, "selectors must be unique").with_context("selector_index", index.to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn valid_output_id(output_id: &str) -> bool {
|
||||
let mut previous_was_separator = true;
|
||||
if output_id.is_empty() {
|
||||
return false;
|
||||
}
|
||||
for byte in output_id.bytes() {
|
||||
if byte == b'.' {
|
||||
if previous_was_separator {
|
||||
return false;
|
||||
}
|
||||
previous_was_separator = true;
|
||||
} else if byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_' || byte == b'-' {
|
||||
previous_was_separator = false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return !previous_was_separator;
|
||||
}
|
||||
|
||||
fn relative_log_path_is_valid(value: &str) -> bool {
|
||||
let path = std::path::Path::new(value);
|
||||
if path.is_absolute() {
|
||||
return false;
|
||||
}
|
||||
let mut has_normal_component = false;
|
||||
for component in path.components() {
|
||||
match component {
|
||||
std::path::Component::Normal(_) => has_normal_component = true,
|
||||
std::path::Component::CurDir | std::path::Component::ParentDir | std::path::Component::RootDir | std::path::Component::Prefix(_) => return false,
|
||||
}
|
||||
}
|
||||
return has_normal_component;
|
||||
}
|
||||
|
||||
fn selector_error(document: &ConfigJsonDocument, profile_index: usize, output: &str, dimension: &'static str, reason: &'static str) -> ksp_core_lib::Error {
|
||||
return semantic_error(document, reason)
|
||||
.with_context("profile_index", profile_index.to_string())
|
||||
.with_context("output", output)
|
||||
.with_context("dimension", dimension);
|
||||
}
|
||||
|
||||
fn semantic_error(document: &ConfigJsonDocument, reason: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID, "Config document 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 json_read_error(file_id: &crate::ConfigFileId, path: &std::path::Path, source: std::io::Error) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_FILE_READ_FAILED, "Config-managed JSON file cannot be read")
|
||||
.with_context("file_id", file_id.as_str())
|
||||
.with_context("path", path.to_string_lossy().into_owned())
|
||||
.with_source(source);
|
||||
}
|
||||
|
||||
fn json_syntax_error(file_id: &crate::ConfigFileId, path: &std::path::Path, source: serde_json::Error) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_SYNTAX_INVALID, "Config-managed file contains invalid JSON syntax")
|
||||
.with_context("file_id", file_id.as_str())
|
||||
.with_context("path", path.to_string_lossy().into_owned())
|
||||
.with_source(source);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/document.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/error.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
/// 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");
|
||||
@@ -18,3 +18,18 @@ pub const ERROR_CODE_FILE_ID_DUPLICATE: ksp_core_lib::ErrorCode = ksp_core_lib::
|
||||
|
||||
/// Error code used when a Config filename mapping or descriptor relation is invalid.
|
||||
pub const ERROR_CODE_FILE_MAPPING_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_mapping_invalid");
|
||||
|
||||
/// Error code used when a Config-managed JSON document or schema cannot be read from its resolved path.
|
||||
pub const ERROR_CODE_JSON_FILE_READ_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "json_file_read_failed");
|
||||
|
||||
/// Error code used when a Config-managed file contains invalid JSON syntax.
|
||||
pub const ERROR_CODE_JSON_SYNTAX_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "json_syntax_invalid");
|
||||
|
||||
/// Error code used when a JSON Schema document is itself invalid for the selected JSON Schema draft.
|
||||
pub const ERROR_CODE_SCHEMA_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "schema_invalid");
|
||||
|
||||
/// Error code used when a Config document does not satisfy its registered JSON Schema.
|
||||
pub const ERROR_CODE_SCHEMA_VALIDATION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "schema_validation_failed");
|
||||
|
||||
/// 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");
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
// file: crates/ksp-config-lib/src/lib.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! KSP-owned application configuration facade.
|
||||
//!
|
||||
//! `0.1.3-pre.003` provides the non-recursive bootstrap roots plus a stable logical file registry. Consumers select Config-managed files by `file_id`;
|
||||
//! physical filenames can be replaced at bootstrap without changing those logical identities. JSON/schema loading, profiles, environment resolution and
|
||||
//! persistence are introduced by later bounded prereleases.
|
||||
//! `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
|
||||
//! prereleases.
|
||||
|
||||
mod bootstrap;
|
||||
mod document;
|
||||
mod error;
|
||||
mod registry;
|
||||
|
||||
@@ -24,10 +25,16 @@ 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;
|
||||
/// 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.
|
||||
pub use self::document::ConfigJsonDocument;
|
||||
/// Error code used when a Config bootstrap argument is missing its value.
|
||||
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 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.
|
||||
pub use self::error::ERROR_CODE_FILE_ID_DUPLICATE;
|
||||
/// Error code used when a logical Config file identifier is malformed.
|
||||
@@ -36,9 +43,17 @@ pub use self::error::ERROR_CODE_FILE_ID_INVALID;
|
||||
pub use self::error::ERROR_CODE_FILE_ID_UNKNOWN;
|
||||
/// Error code used when a Config filename mapping or descriptor relation is invalid.
|
||||
pub use self::error::ERROR_CODE_FILE_MAPPING_INVALID;
|
||||
/// Error code used when a Config-managed JSON document or schema cannot be read.
|
||||
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 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;
|
||||
/// 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 root category.
|
||||
/// Logical descriptor associating a stable file identifier with its physical filename and validation schema.
|
||||
pub use self::registry::ConfigFileDescriptor;
|
||||
/// Stable logical identifier for a Config-managed file.
|
||||
pub use self::registry::ConfigFileId;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/registry.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
/// Bootstrap argument used to replace a known Config filename mapping.
|
||||
pub const ARG_FILE_MAP: &str = "--filemap";
|
||||
@@ -43,12 +43,13 @@ pub enum ConfigFileKind {
|
||||
Schema,
|
||||
}
|
||||
|
||||
/// Logical descriptor associating a stable file identifier with its physical filename and root category.
|
||||
/// Logical descriptor associating a stable file identifier with its physical filename, root category and optional validation schema.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ConfigFileDescriptor {
|
||||
file_id: ConfigFileId,
|
||||
kind: ConfigFileKind,
|
||||
filename: std::path::PathBuf,
|
||||
schema_file_id: std::option::Option<ConfigFileId>,
|
||||
}
|
||||
|
||||
impl ConfigFileDescriptor {
|
||||
@@ -70,7 +71,18 @@ impl ConfigFileDescriptor {
|
||||
return self.filename.as_path();
|
||||
}
|
||||
|
||||
fn new(file_id: &'static str, kind: ConfigFileKind, filename: &'static str) -> ksp_core_lib::Result<Self> {
|
||||
/// Returns the logical schema identifier associated with this Config document when one is declared.
|
||||
#[must_use]
|
||||
pub fn schema_file_id(&self) -> std::option::Option<&ConfigFileId> {
|
||||
return self.schema_file_id.as_ref();
|
||||
}
|
||||
|
||||
fn new(
|
||||
file_id: &'static str,
|
||||
kind: ConfigFileKind,
|
||||
filename: &'static str,
|
||||
schema_file_id: std::option::Option<&'static str>,
|
||||
) -> ksp_core_lib::Result<Self> {
|
||||
let file_id = ConfigFileId::new(file_id);
|
||||
let file_id = match file_id {
|
||||
std::result::Result::Ok(value) => value,
|
||||
@@ -82,8 +94,13 @@ impl ConfigFileDescriptor {
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
let filename = validate_relative_filename(&file_id, std::path::PathBuf::from(filename));
|
||||
return match filename {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(Self { file_id, kind, filename: value }),
|
||||
let filename = match filename {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let schema_file_id = parse_schema_file_id(&file_id, kind, schema_file_id);
|
||||
return match schema_file_id {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(Self { file_id, kind, filename, schema_file_id: value }),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
@@ -98,12 +115,18 @@ pub struct ConfigFileRegistry {
|
||||
impl ConfigFileRegistry {
|
||||
/// Creates the registry containing the KSP default file mappings known in the current release.
|
||||
pub fn defaults() -> ksp_core_lib::Result<Self> {
|
||||
let logging = ConfigFileDescriptor::new(FILE_ID_STD_LOGGING, ConfigFileKind::Config, DEFAULT_STD_LOGGING_FILENAME);
|
||||
let logging = ConfigFileDescriptor::new(
|
||||
FILE_ID_STD_LOGGING,
|
||||
ConfigFileKind::Config,
|
||||
DEFAULT_STD_LOGGING_FILENAME,
|
||||
std::option::Option::Some(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 = ConfigFileDescriptor::new(FILE_ID_SCHEMA_STD_LOGGING, ConfigFileKind::Schema, DEFAULT_STD_LOGGING_SCHEMA_FILENAME);
|
||||
let logging_schema =
|
||||
ConfigFileDescriptor::new(FILE_ID_SCHEMA_STD_LOGGING, ConfigFileKind::Schema, 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),
|
||||
@@ -154,7 +177,7 @@ impl ConfigFileRegistry {
|
||||
return std::result::Result::Ok(root.join(descriptor.filename()));
|
||||
}
|
||||
|
||||
/// Replaces the physical filename of one known logical identifier while preserving its kind and logical identity.
|
||||
/// Replaces the physical filename of one known logical identifier while preserving its kind, schema association and logical identity.
|
||||
pub fn with_filename_override(mut self, file_id: &ConfigFileId, filename: impl std::convert::Into<std::path::PathBuf>) -> ksp_core_lib::Result<Self> {
|
||||
let update = self.set_filename_override(file_id, filename.into());
|
||||
return match update {
|
||||
@@ -174,7 +197,12 @@ impl ConfigFileRegistry {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let updated = ConfigFileDescriptor { file_id: descriptor.file_id.clone(), kind: descriptor.kind, filename };
|
||||
let updated = ConfigFileDescriptor {
|
||||
file_id: descriptor.file_id.clone(),
|
||||
kind: descriptor.kind,
|
||||
filename,
|
||||
schema_file_id: descriptor.schema_file_id.clone(),
|
||||
};
|
||||
self.descriptors.insert(file_id.clone(), updated);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
@@ -190,7 +218,68 @@ fn build_registry<const N: usize>(descriptors: [ConfigFileDescriptor; N]) -> ksp
|
||||
return std::result::Result::Err(duplicate_file_id_error(duplicate_id.as_str()));
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(registry);
|
||||
let associations = validate_schema_associations(®istry);
|
||||
return match associations {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(registry),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
fn parse_schema_file_id(
|
||||
file_id: &ConfigFileId,
|
||||
kind: ConfigFileKind,
|
||||
schema_file_id: std::option::Option<&'static str>,
|
||||
) -> ksp_core_lib::Result<std::option::Option<ConfigFileId>> {
|
||||
return match (kind, schema_file_id) {
|
||||
(ConfigFileKind::Schema, std::option::Option::Some(_)) => {
|
||||
std::result::Result::Err(invalid_file_mapping_with_id_error(file_id.as_str(), "schema descriptors cannot declare another validation schema"))
|
||||
},
|
||||
(ConfigFileKind::Schema, std::option::Option::None) | (ConfigFileKind::Config, std::option::Option::None) => {
|
||||
std::result::Result::Ok(std::option::Option::None)
|
||||
},
|
||||
(ConfigFileKind::Config, std::option::Option::Some(value)) => {
|
||||
let schema_id = ConfigFileId::new(value);
|
||||
match schema_id {
|
||||
std::result::Result::Ok(schema_id) => {
|
||||
if schema_id.as_str().starts_with("schema.") {
|
||||
std::result::Result::Ok(std::option::Option::Some(schema_id))
|
||||
} else {
|
||||
std::result::Result::Err(invalid_file_mapping_with_id_error(
|
||||
file_id.as_str(),
|
||||
"validation schema file_id must use the schema namespace",
|
||||
))
|
||||
}
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn validate_schema_associations(registry: &ConfigFileRegistry) -> ksp_core_lib::Result<()> {
|
||||
for descriptor in registry.descriptors.values() {
|
||||
let schema_file_id = match descriptor.schema_file_id() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
let schema = registry.descriptors.get(schema_file_id);
|
||||
let schema = match schema {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(invalid_file_mapping_with_id_error(
|
||||
descriptor.file_id().as_str(),
|
||||
"validation schema file_id is not registered",
|
||||
));
|
||||
},
|
||||
};
|
||||
if schema.kind() != ConfigFileKind::Schema {
|
||||
return std::result::Result::Err(invalid_file_mapping_with_id_error(
|
||||
descriptor.file_id().as_str(),
|
||||
"validation schema descriptor must have schema kind",
|
||||
));
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn apply_file_map_argument(registry: &mut ConfigFileRegistry, argument: &std::ffi::OsStr) -> ksp_core_lib::Result<()> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// file: crates/ksp-config-lib/tests/public_api.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Integration tests for the public `ksp-config-lib` bootstrap and logical file registry contracts.
|
||||
//! Integration tests for the public `ksp-config-lib` bootstrap, logical file registry and validated JSON document contracts.
|
||||
|
||||
#[test]
|
||||
fn bootstrap_contract_is_available_from_crate_root() {
|
||||
@@ -72,3 +72,23 @@ fn logical_file_registry_is_available_from_crate_root() {
|
||||
assert_eq!(ksp_config_lib::ARG_FILE_MAP, "--filemap");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validated_json_document_engine_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 document = engine.load_validated_document(&file_id);
|
||||
assert!(document.is_ok(), "public document engine should validate committed Logging configuration: {document:?}");
|
||||
if let std::result::Result::Ok(document) = document {
|
||||
assert_eq!(document.file_id(), &file_id);
|
||||
assert_eq!(document.value().get("format_version"), std::option::Option::Some(&serde_json::Value::from(1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
224
crates/ksp-config-lib/unit_tests/document.rs
Normal file
224
crates/ksp-config-lib/unit_tests/document.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/document.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn committed_logging_document_passes_registered_schema_and_semantic_validation() {
|
||||
let workspace = workspace_root();
|
||||
let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
|
||||
let registry = crate::ConfigFileRegistry::defaults();
|
||||
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
|
||||
assert!(bootstrap.is_ok(), "workspace Config paths should be valid: {bootstrap:?}");
|
||||
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
|
||||
assert!(file_id.is_ok(), "logging file_id should be valid: {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 = super::ConfigDocumentEngine::new(bootstrap, registry);
|
||||
let document = engine.load_validated_document(&file_id);
|
||||
assert!(document.is_ok(), "committed std.logging.json should validate: {document:?}");
|
||||
if let std::result::Result::Ok(document) = document {
|
||||
assert_eq!(document.file_id(), &file_id);
|
||||
assert_eq!(document.path(), workspace.join("config/std.logging.json").as_path());
|
||||
let default_profile = document.value().get("default_profile");
|
||||
assert!(default_profile.is_some(), "validated Logging document should retain default_profile");
|
||||
if let std::option::Option::Some(default_profile) = default_profile {
|
||||
assert_eq!(default_profile.as_str(), std::option::Option::Some("local_dev"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_document_is_reported_with_file_read_error() {
|
||||
let fixture = fixture_roots("missing-document");
|
||||
let prepared = prepare_fixture(&fixture, std::option::Option::None, valid_minimal_logging_schema());
|
||||
assert!(prepared.is_ok(), "fixture should be writable: {prepared:?}");
|
||||
if prepared.is_ok() {
|
||||
let result = load_fixture(&fixture);
|
||||
assert_error_code(result, crate::ERROR_CODE_JSON_FILE_READ_FAILED);
|
||||
}
|
||||
cleanup_fixture(&fixture);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_json_is_reported_before_schema_validation() {
|
||||
let fixture = fixture_roots("malformed-json");
|
||||
let prepared = prepare_fixture(&fixture, std::option::Option::Some("{ invalid-json"), valid_minimal_logging_schema());
|
||||
assert!(prepared.is_ok(), "fixture should be writable: {prepared:?}");
|
||||
if prepared.is_ok() {
|
||||
let result = load_fixture(&fixture);
|
||||
assert_error_code(result, crate::ERROR_CODE_JSON_SYNTAX_INVALID);
|
||||
}
|
||||
cleanup_fixture(&fixture);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_schema_document_is_reported_before_instance_validation() {
|
||||
let fixture = fixture_roots("invalid-schema");
|
||||
let schema = r#"{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "definitely-not-a-json-schema-type"
|
||||
}"#;
|
||||
let prepared = prepare_fixture(&fixture, std::option::Option::Some("{}"), schema);
|
||||
assert!(prepared.is_ok(), "fixture should be writable: {prepared:?}");
|
||||
if prepared.is_ok() {
|
||||
let result = load_fixture(&fixture);
|
||||
assert_error_code(result, crate::ERROR_CODE_SCHEMA_INVALID);
|
||||
}
|
||||
cleanup_fixture(&fixture);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_violation_is_distinct_from_json_syntax_failure() {
|
||||
let fixture = fixture_roots("schema-violation");
|
||||
let schema = r#"{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"required": ["required_field"],
|
||||
"properties": {
|
||||
"required_field": {"type": "string"}
|
||||
}
|
||||
}"#;
|
||||
let prepared = prepare_fixture(&fixture, std::option::Option::Some("{}"), schema);
|
||||
assert!(prepared.is_ok(), "fixture should be writable: {prepared:?}");
|
||||
if prepared.is_ok() {
|
||||
let result = load_fixture(&fixture);
|
||||
assert_error_code(result, crate::ERROR_CODE_SCHEMA_VALIDATION_FAILED);
|
||||
}
|
||||
cleanup_fixture(&fixture);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_valid_logging_document_can_still_fail_ksp_semantics() {
|
||||
let fixture = fixture_roots("semantic-invalid");
|
||||
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 document = r#"{
|
||||
"format_version": 1,
|
||||
"logs_directory": "logs",
|
||||
"default_profile": "duplicate-output",
|
||||
"profiles": [
|
||||
{
|
||||
"profile_id": "duplicate-output",
|
||||
"default_filter": "info",
|
||||
"span_events": "off",
|
||||
"console": {
|
||||
"enabled": false,
|
||||
"output": "stderr",
|
||||
"ansi": false,
|
||||
"format": "human",
|
||||
"filter": {"level": "trace", "targets": ["*"], "domains": ["*"]}
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"output_id": "file.same",
|
||||
"enabled": true,
|
||||
"path": "first.log",
|
||||
"rotation": "daily",
|
||||
"format": "human",
|
||||
"ansi": false,
|
||||
"filter": {"level": "info", "targets": ["*"], "domains": ["*"]}
|
||||
},
|
||||
{
|
||||
"output_id": "file.same",
|
||||
"enabled": true,
|
||||
"path": "second.log",
|
||||
"rotation": "daily",
|
||||
"format": "human",
|
||||
"ansi": false,
|
||||
"filter": {"level": "info", "targets": ["*"], "domains": ["*"]}
|
||||
}
|
||||
],
|
||||
"target_filters": []
|
||||
}
|
||||
]
|
||||
}"#;
|
||||
let prepared = prepare_fixture(&fixture, std::option::Option::Some(document), schema_source.as_str());
|
||||
assert!(prepared.is_ok(), "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 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 {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let registry = crate::ConfigFileRegistry::defaults();
|
||||
let registry = match registry {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
|
||||
let file_id = match file_id {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let engine = super::ConfigDocumentEngine::new(bootstrap, registry);
|
||||
return engine.load_validated_document(&file_id);
|
||||
}
|
||||
|
||||
fn prepare_fixture(fixture: &FixtureRoots, document: std::option::Option<&str>, schema: &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 schema_write = std::fs::write(fixture.schemas.join(crate::DEFAULT_STD_LOGGING_SCHEMA_FILENAME), schema);
|
||||
if let std::result::Result::Err(error) = schema_write {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if let std::option::Option::Some(document) = document {
|
||||
let document_write = std::fs::write(fixture.config.join(crate::DEFAULT_STD_LOGGING_FILENAME), document);
|
||||
if let std::result::Result::Err(error) = document_write {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn valid_minimal_logging_schema() -> &'static str {
|
||||
return r#"{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object"
|
||||
}"#;
|
||||
}
|
||||
|
||||
fn assert_error_code(result: ksp_core_lib::Result<super::ConfigJsonDocument>, expected: ksp_core_lib::ErrorCode) {
|
||||
assert!(result.is_err(), "fixture should fail with {expected:?}: {result:?}");
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), expected);
|
||||
}
|
||||
}
|
||||
|
||||
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 mut root = std::env::temp_dir();
|
||||
root.push(format!("ksp-config-lib-pre007-{name}-{}", std::process::id()));
|
||||
return FixtureRoots { config: root.join("config"), schemas: root.join("schemas"), root };
|
||||
}
|
||||
|
||||
fn cleanup_fixture(fixture: &FixtureRoots) {
|
||||
let result = std::fs::remove_dir_all(fixture.root.as_path());
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.kind(), std::io::ErrorKind::NotFound, "fixture cleanup should only ignore missing directories: {error}");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/registry.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn defaults_register_logging_document_and_schema_with_distinct_roots() {
|
||||
@@ -18,6 +18,11 @@ fn defaults_register_logging_document_and_schema_with_distinct_roots() {
|
||||
if let (std::result::Result::Ok(logging), std::result::Result::Ok(schema)) = (logging, schema) {
|
||||
assert_eq!(logging.kind(), super::ConfigFileKind::Config);
|
||||
assert_eq!(logging.filename(), std::path::Path::new(super::DEFAULT_STD_LOGGING_FILENAME));
|
||||
let logging_schema = logging.schema_file_id();
|
||||
assert!(logging_schema.is_some(), "logging document should declare its validation schema");
|
||||
if let std::option::Option::Some(logging_schema) = logging_schema {
|
||||
assert_eq!(logging_schema, &schema_id);
|
||||
}
|
||||
assert_eq!(schema.kind(), super::ConfigFileKind::Schema);
|
||||
assert_eq!(schema.filename(), std::path::Path::new(super::DEFAULT_STD_LOGGING_SCHEMA_FILENAME));
|
||||
}
|
||||
@@ -143,8 +148,8 @@ fn malformed_filemap_arguments_are_rejected() {
|
||||
|
||||
#[test]
|
||||
fn duplicate_registry_ids_are_rejected() {
|
||||
let first = super::ConfigFileDescriptor::new("cfg.duplicate", super::ConfigFileKind::Config, "first.json");
|
||||
let second = super::ConfigFileDescriptor::new("cfg.duplicate", super::ConfigFileKind::Config, "second.json");
|
||||
let first = super::ConfigFileDescriptor::new("cfg.duplicate", super::ConfigFileKind::Config, "first.json", std::option::Option::None);
|
||||
let second = super::ConfigFileDescriptor::new("cfg.duplicate", super::ConfigFileKind::Config, "second.json", std::option::Option::None);
|
||||
assert!(first.is_ok(), "first descriptor should be valid: {first:?}");
|
||||
assert!(second.is_ok(), "second descriptor should be valid: {second:?}");
|
||||
if let (std::result::Result::Ok(first), std::result::Result::Ok(second)) = (first, second) {
|
||||
@@ -158,13 +163,26 @@ fn duplicate_registry_ids_are_rejected() {
|
||||
|
||||
#[test]
|
||||
fn descriptor_kind_must_match_file_id_namespace() {
|
||||
let result = super::ConfigFileDescriptor::new("schema.invalid-kind", super::ConfigFileKind::Config, "invalid.json");
|
||||
let result = super::ConfigFileDescriptor::new("schema.invalid-kind", super::ConfigFileKind::Config, "invalid.json", std::option::Option::None);
|
||||
assert!(result.is_err(), "descriptor kind mismatch must be rejected");
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FILE_MAPPING_INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_schema_association_must_reference_registered_schema_descriptor() {
|
||||
let config = super::ConfigFileDescriptor::new("cfg.test", super::ConfigFileKind::Config, "test.json", std::option::Option::Some("schema.test"));
|
||||
assert!(config.is_ok(), "config descriptor should be valid before registry association validation: {config:?}");
|
||||
if let std::result::Result::Ok(config) = config {
|
||||
let result = super::build_registry([config]);
|
||||
assert!(result.is_err(), "registry must reject a missing schema association");
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FILE_MAPPING_INVALID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_mapping_invalid<T: std::fmt::Debug>(result: ksp_core_lib::Result<T>) {
|
||||
assert!(result.is_err(), "invalid filename must be rejected: {result:?}");
|
||||
if let std::result::Result::Err(error) = result {
|
||||
|
||||
347
deltas/0.1.3/pre.007.md
Normal file
347
deltas/0.1.3/pre.007.md
Normal file
@@ -0,0 +1,347 @@
|
||||
<!-- file: deltas/0.1.3/pre.007.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.3-pre.007
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente validée :
|
||||
|
||||
```text
|
||||
0.1.3-pre.006
|
||||
```
|
||||
|
||||
Version technique de cette base :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.3-pre.6"
|
||||
Cargo.toml header version = 47
|
||||
```
|
||||
|
||||
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-logging-lib OK
|
||||
cargo tree -p ksp-logging-lib -d OK — aucune duplication
|
||||
cargo tree -p ksp-logging-lib -e features OK
|
||||
```
|
||||
|
||||
Les tranches Logging préalables au premier schema Config sont donc closes.
|
||||
|
||||
## Objet de pre.007
|
||||
|
||||
Introduire la première surface JSON/JSON Schema réelle de `ksp-config-lib` sans ouvrir encore la résolution de profils, les composites ou l'environnement :
|
||||
|
||||
```text
|
||||
registry file_id
|
||||
-> resolved Config path
|
||||
-> JSON parse
|
||||
-> registered schema file_id
|
||||
-> schema parse + meta-schema validation
|
||||
-> document validation
|
||||
-> bounded document semantics
|
||||
```
|
||||
|
||||
Le premier document concret est :
|
||||
|
||||
```text
|
||||
cfg.std.logging -> config/std.logging.json
|
||||
```
|
||||
|
||||
validé par :
|
||||
|
||||
```text
|
||||
schema.std.logging -> config/schemas/std.logging.schema.json
|
||||
```
|
||||
|
||||
## Dépendances
|
||||
|
||||
Versions vérifiées au moment de l'ajout depuis les sources officielles `crates.io` / `docs.rs` :
|
||||
|
||||
```text
|
||||
serde 1.0.229 -> workspace constraint ^1.0
|
||||
serde_json 1.0.151 -> workspace constraint ^1.0
|
||||
jsonschema 0.49.9 -> workspace constraint ^0.49
|
||||
```
|
||||
|
||||
`jsonschema` est déclaré avec :
|
||||
|
||||
```text
|
||||
default-features = false
|
||||
```
|
||||
|
||||
La première surface KSP utilise un schema autonome avec références JSON Pointer internes uniquement ; aucune récupération HTTP/file de schemas distants n'est nécessaire.
|
||||
|
||||
Les trois dépendances sont déclarées uniquement sous `[workspace.dependencies]`, puis consommées par `ksp-config-lib` avec `.workspace = true`.
|
||||
|
||||
## Association document -> schema
|
||||
|
||||
`ConfigFileDescriptor` possède maintenant :
|
||||
|
||||
```text
|
||||
schema_file_id: Option<ConfigFileId>
|
||||
```
|
||||
|
||||
Le mapping par défaut devient conceptuellement :
|
||||
|
||||
```text
|
||||
cfg.std.logging
|
||||
kind = Config
|
||||
filename = std.logging.json
|
||||
schema_file_id = schema.std.logging
|
||||
|
||||
schema.std.logging
|
||||
kind = Schema
|
||||
filename = std.logging.schema.json
|
||||
schema_file_id = none
|
||||
```
|
||||
|
||||
Le registre valide :
|
||||
|
||||
- qu'un schema référencé utilise le namespace `schema.*` ;
|
||||
- que son descriptor existe réellement ;
|
||||
- qu'il possède `ConfigFileKind::Schema` ;
|
||||
- qu'un descriptor Schema ne référence pas lui-même un schema Config.
|
||||
|
||||
Un `--filemap` change uniquement le filename physique et conserve cette association logique.
|
||||
|
||||
Aucun `schema.composite` n'est encore créé : il sera ajouté avec la tranche composite qui en a réellement besoin.
|
||||
|
||||
## Moteur JSON générique
|
||||
|
||||
Nouvelle façade :
|
||||
|
||||
```text
|
||||
ConfigDocumentEngine
|
||||
ConfigJsonDocument
|
||||
```
|
||||
|
||||
`ConfigDocumentEngine::load_validated_document(file_id)` :
|
||||
|
||||
1. vérifie que le `file_id` désigne un document Config ;
|
||||
2. résout son path via `ConfigBootstrapOptions` + `ConfigFileRegistry` ;
|
||||
3. lit et parse le JSON ;
|
||||
4. charge le schema associé par son propre `file_id` ;
|
||||
5. valide le schema contre son meta-schema ;
|
||||
6. valide le document contre le schema Draft 2020-12 ;
|
||||
7. exécute les invariants sémantiques actuellement connus pour ce type de document ;
|
||||
8. retourne le JSON validé sans transférer aux consumers la responsabilité de lecture/validation filesystem.
|
||||
|
||||
`ConfigJsonDocument` expose uniquement :
|
||||
|
||||
```text
|
||||
file_id
|
||||
resolved path
|
||||
validated serde_json::Value
|
||||
```
|
||||
|
||||
Les autres crates restent interdites de lecture directe des fichiers Config.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
Les nouveaux codes Config distinguent :
|
||||
|
||||
```text
|
||||
config.json_file_read_failed
|
||||
config.json_syntax_invalid
|
||||
config.schema_invalid
|
||||
config.schema_validation_failed
|
||||
config.document_semantic_invalid
|
||||
```
|
||||
|
||||
Les erreurs enregistrent le `file_id` et le path concernés, mais jamais le contenu JSON complet.
|
||||
|
||||
## Premier schema Logging
|
||||
|
||||
`config/schemas/std.logging.schema.json` utilise JSON Schema Draft 2020-12 et couvre la surface Logging stabilisée en `pre.004/.005/.006` :
|
||||
|
||||
```text
|
||||
format_version
|
||||
logs_directory
|
||||
default_profile
|
||||
profiles[]
|
||||
profile_id
|
||||
default_filter
|
||||
span_events
|
||||
console
|
||||
enabled
|
||||
output
|
||||
ansi
|
||||
format
|
||||
filter.level
|
||||
filter.targets[]
|
||||
filter.domains[]
|
||||
files[]
|
||||
output_id
|
||||
enabled
|
||||
path
|
||||
rotation
|
||||
format
|
||||
ansi = false
|
||||
filter.level
|
||||
filter.targets[]
|
||||
filter.domains[]
|
||||
target_filters[]
|
||||
```
|
||||
|
||||
Le schema encode notamment :
|
||||
|
||||
- niveaux `off/error/warn/info/debug/trace` ;
|
||||
- lifecycle `off/new_and_close/full` ;
|
||||
- formats `human/compact/pretty/json` ;
|
||||
- rotation `never/hourly/daily` ;
|
||||
- console `stdout/stderr` ;
|
||||
- targets KSP ou wildcard `*` ;
|
||||
- selectors uniques, wildcard utilisé seul ;
|
||||
- ANSI interdit pour les fichiers ;
|
||||
- ANSI + JSON console interdit.
|
||||
|
||||
## Document runtime et exemple
|
||||
|
||||
Ajouts :
|
||||
|
||||
```text
|
||||
config/std.logging.json
|
||||
config/schemas/std.logging.schema.json
|
||||
config/examples/std.logging.example.json
|
||||
```
|
||||
|
||||
Le runtime utilise déjà :
|
||||
|
||||
```text
|
||||
"logs_directory": "${KSP_LOGS_DIRECTORY:-logs}"
|
||||
```
|
||||
|
||||
mais `pre.007` ne résout encore aucune variable. Le placeholder reste une valeur source string valide jusqu'au resolver de `pre.010`.
|
||||
|
||||
Le runtime démontre :
|
||||
|
||||
- console configurable ;
|
||||
- plusieurs fichiers ;
|
||||
- formats humain et JSON ;
|
||||
- filtres par level/target/domain ;
|
||||
- target overrides globaux ;
|
||||
- `output_id` distinct du `file_id` Config.
|
||||
|
||||
## Validation sémantique de base
|
||||
|
||||
Après JSON Schema, Config vérifie déjà les invariants directement liés à la surface Logging :
|
||||
|
||||
- `format_version = 1` ;
|
||||
- chaînes globales requises non vides ;
|
||||
- `output_id` fichier conforme ;
|
||||
- `output_id` uniques dans un même profil ;
|
||||
- path fichier relatif à `logs_directory`, sans traversal ;
|
||||
- ANSI fichier interdit ;
|
||||
- ANSI + JSON console interdit ;
|
||||
- selectors non vides/uniques ;
|
||||
- wildcard seul ;
|
||||
- target selector et global `target_prefix` limités aux targets `ksp-*`.
|
||||
|
||||
Ne sont volontairement **pas encore** traités dans cette tranche :
|
||||
|
||||
- unicité des `profile_id` ;
|
||||
- résolution de `default_profile` ;
|
||||
- sélection explicite d'un profil ;
|
||||
- construction d'une configuration effective globals + profil.
|
||||
|
||||
Ces responsabilités appartiennent à `pre.008`.
|
||||
|
||||
## Tests ajoutés
|
||||
|
||||
Les tests unitaires Config couvrent :
|
||||
|
||||
- document Logging runtime commité valide ;
|
||||
- fichier Config absent ;
|
||||
- syntaxe JSON invalide ;
|
||||
- schema lui-même invalide ;
|
||||
- document ne satisfaisant pas son schema ;
|
||||
- document schema-valide mais sémantiquement invalide ;
|
||||
- association document -> schema présente dans le registre ;
|
||||
- association vers schema absent refusée.
|
||||
|
||||
Le test d'API publique vérifie que `ConfigDocumentEngine` charge le vrai `std.logging.json` depuis les racines Config du workspace.
|
||||
|
||||
## Hors scope
|
||||
|
||||
Cette tranche ne fait pas encore :
|
||||
|
||||
- résolution globals/profils/`default_profile` ;
|
||||
- composite ;
|
||||
- `.env` ;
|
||||
- `std::env` ;
|
||||
- interpolation `${...}` ;
|
||||
- classification secret/public/internal ;
|
||||
- redaction ;
|
||||
- adaptation vers `ksp_logging_lib::LoggingSettings` ;
|
||||
- mutation/persistence.
|
||||
|
||||
`ksp-config-lib` ne dépend donc toujours pas de `ksp-logging-lib` dans `pre.007`.
|
||||
|
||||
## Version technique
|
||||
|
||||
La prerelease devient :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.3-pre.7"
|
||||
Cargo.toml header version = 48
|
||||
```
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
```text
|
||||
config/std.logging.json
|
||||
config/schemas/std.logging.schema.json
|
||||
config/examples/std.logging.example.json
|
||||
crates/ksp-config-lib/src/document.rs
|
||||
crates/ksp-config-lib/unit_tests/document.rs
|
||||
deltas/0.1.3/pre.007.md
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-config-lib/Cargo.toml
|
||||
crates/ksp-config-lib/src/error.rs
|
||||
crates/ksp-config-lib/src/lib.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
|
||||
```
|
||||
|
||||
## Contrôles exécutés dans l'environnement de génération
|
||||
|
||||
Les fichiers JSON ont été parsés avec `python -m json.tool`.
|
||||
|
||||
Le schema Draft 2020-12 et le document runtime ont également été validés avec l'implémentation Python `jsonschema` disponible dans l'environnement de génération.
|
||||
|
||||
Aucune commande Cargo n'est disponible dans cet environnement ; aucune validation Rust n'est donc 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
|
||||
```
|
||||
|
||||
Une attention particulière doit être portée au graphe `jsonschema` avec `default-features = false` afin de confirmer qu'aucun stack HTTP/TLS de résolution distante n'est introduit inutilement.
|
||||
|
||||
Après validation de `pre.007`, la prochaine tranche est :
|
||||
|
||||
```text
|
||||
0.1.3-pre.008 — globals + profils + default_profile
|
||||
```
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/000-README.md -->
|
||||
<!-- version: 13 -->
|
||||
<!-- version: 14 -->
|
||||
|
||||
# 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` ferme maintenant le routing structuré `domain` avant le premier schema Logging de `pre.007`.
|
||||
- [`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` et `pre.007` livre maintenant le moteur JSON/JSON Schema et le premier `std.logging.json`; `pre.008` poursuit avec globals/profils/`default_profile`.
|
||||
|
||||
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: 11 -->
|
||||
<!-- version: 12 -->
|
||||
|
||||
# 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.
|
||||
|
||||
Après validation de `pre.005-fix.001`, `pre.006` ferme le routing Logging structuré `domain`; `pre.007` redevient donc la prochaine tranche Config avec JSON/JSON Schema et le premier document `std.logging.json`.
|
||||
`pre.006` a fermé le routing Logging structuré `domain`; `pre.007` livre le moteur JSON/JSON Schema et le premier document `std.logging.json`. Après validation utilisateur de `pre.007`, la prochaine tranche est `pre.008` pour globals, profils et `default_profile`.
|
||||
|
||||
## `0.1.4` — Config desktop par défaut
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<!-- file: docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md -->
|
||||
<!-- version: 9 -->
|
||||
<!-- version: 10 -->
|
||||
|
||||
# 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 et `pre.006` ferme le routing structuré `domain`. Le premier schema Logging peut donc commencer en `pre.007` après validation utilisateur de cette tranche.
|
||||
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` et `pre.007` introduit le moteur JSON/JSON Schema ainsi que le premier document runtime `std.logging.json`. La prochaine tranche est `pre.008` pour la résolution des globals/profils/`default_profile`.
|
||||
|
||||
La base auditée reste la release stable `v0.1.2`.
|
||||
|
||||
@@ -280,10 +280,9 @@ Premier mapping codé dans `ksp-config-lib` :
|
||||
```text
|
||||
cfg.std.logging -> std.logging.json
|
||||
schema.std.logging -> std.logging.schema.json
|
||||
schema.composite -> composite.schema.json
|
||||
```
|
||||
|
||||
Le descriptor `cfg.std.logging` référence logiquement `schema.std.logging`; il ne contient pas le nom physique du schema.
|
||||
Le descriptor `cfg.std.logging` référence logiquement `schema.std.logging`; il ne contient pas le nom physique du schema. Le futur `schema.composite` n'est ajouté au registre qu'avec la tranche composite qui en a réellement besoin.
|
||||
|
||||
Un futur consumer pourra introduire par le registre KSP :
|
||||
|
||||
@@ -1701,19 +1700,25 @@ Implémentation candidate livrée par `pre.006` :
|
||||
|
||||
Aucune dépendance externe ou feature Cargo supplémentaire n'est nécessaire pour cette tranche. `pre.007` peut donc construire `std.logging.schema.json` sur une surface Logging qui représente et exécute réellement les trois dimensions de routing retenues : level, target et domain.
|
||||
|
||||
La validation utilisateur de `pre.006` reste requise avant l'ouverture de `pre.007`.
|
||||
La validation utilisateur de `pre.006` est acquise : `fmt/check/clippy/test` et les trois graphes Cargo demandés sont propres.
|
||||
|
||||
### `0.1.3-pre.007` — JSON/JSON Schema + premier document Logging
|
||||
|
||||
- `serde`/`serde_json`/`jsonschema` au workspace avec versions revérifiées ;
|
||||
- infrastructure générique de lecture JSON ;
|
||||
- association descriptor -> schema par `file_id` ;
|
||||
- validation JSON Schema ;
|
||||
- `config/`, `config/schemas/`, `config/examples/` ;
|
||||
- `std.logging.schema.json` aligné sur les contrats Logging réellement stabilisés en `pre.004/.005/.006` ;
|
||||
- runtime `config/std.logging.json` ;
|
||||
- exemple Logging ;
|
||||
- parse/schema/validation sémantique de base.
|
||||
Tranche livrée :
|
||||
|
||||
- `serde ^1.0`, `serde_json ^1.0` et `jsonschema ^0.49` sont déclarés au workspace puis consommés avec `.workspace = true` par `ksp-config-lib` ; les versions observées au moment de l'ajout sont respectivement `1.0.229`, `1.0.151` et `0.49.9` ;
|
||||
- `jsonschema` est consommé avec `default-features = false` : `0.1.3` n'a besoin d'aucune récupération HTTP/file de références externes pour le schema Logging autonome ;
|
||||
- `ConfigFileDescriptor` possède désormais l'association logique optionnelle `schema_file_id`; `cfg.std.logging` référence `schema.std.logging` ;
|
||||
- le registre vérifie qu'un schema référencé existe réellement et possède `ConfigFileKind::Schema` ;
|
||||
- `ConfigDocumentEngine` centralise la lecture des JSON enregistrés, la validation du document schema contre son meta-schema, la validation de l'instance puis les invariants sémantiques propres au document ;
|
||||
- les diagnostics distinguent lecture impossible, syntaxe JSON invalide, schema invalide, échec de validation schema et invalidité sémantique ;
|
||||
- `config/std.logging.json`, `config/schemas/std.logging.schema.json` et `config/examples/std.logging.example.json` constituent la première surface Config réelle ;
|
||||
- le schema est JSON Schema Draft 2020-12 et reflète la surface Logging stabilisée : filtre global, lifecycle spans, console, multi-fichiers, `output_id`, rotation, formats, ANSI et routing `level/target/domain` ;
|
||||
- le document source contient déjà `logs_directory`, `default_profile` et `profiles[]`, mais `pre.007` ne résout encore aucun profil et ne vérifie pas encore l'unicité des `profile_id` ni que `default_profile` référence un profil existant ; ces responsabilités restent explicitement à `pre.008` ;
|
||||
- la validation sémantique de base couvre notamment les `output_id` fichier, chemins relatifs sous `logs_directory`, ANSI fichier interdit, incompatibilité ANSI+JSON console, selectors wildcard/target KSP et unicité des `output_id` dans un profil ;
|
||||
- aucune interpolation `${...}` n'est encore exécutée : `${KSP_LOGS_DIRECTORY:-logs}` reste une string source schema-valide jusqu'au resolver de `pre.010`.
|
||||
|
||||
Aucune dépendance à `ksp-logging-lib` n'est encore nécessaire dans Config : le mapping vers `LoggingSettings` reste réservé à `pre.012`.
|
||||
|
||||
### `0.1.3-pre.008` — globals + profils + `default_profile`
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/rules/FILE_CONTRACTS.md -->
|
||||
<!-- version: 8 -->
|
||||
<!-- version: 9 -->
|
||||
|
||||
# Contrats des fichiers
|
||||
|
||||
@@ -36,6 +36,16 @@ Les règles `FILE-*` définissent la responsabilité et le mode de modification
|
||||
| futurs documents de référence | Définir vocabulaire, identifiants et références canoniques. | Mettre à jour quand la référence canonique évolue. |
|
||||
| futures validations | Conserver des résultats réellement exécutés. | Ne jamais enregistrer une validation supposée comme réussie. |
|
||||
|
||||
## 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. |
|
||||
|
||||
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.
|
||||
|
||||
## Répertoire `prompts/`
|
||||
|
||||
| Fichier/famille | Responsabilité | Règle de modification |
|
||||
|
||||
Reference in New Issue
Block a user