v0.1.3-pre.002
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 40
|
||||
# version: 41
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-core-lib", "crates/ksp-logging-lib"]
|
||||
members = ["crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.3-pre.1"
|
||||
version = "0.1.3-pre.2"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
14
crates/ksp-config-lib/Cargo.toml
Normal file
14
crates/ksp-config-lib/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
# file: crates/ksp-config-lib/Cargo.toml
|
||||
# version: 1
|
||||
|
||||
[package]
|
||||
name = "ksp-config-lib"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
205
crates/ksp-config-lib/src/bootstrap.rs
Normal file
205
crates/ksp-config-lib/src/bootstrap.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
// file: crates/ksp-config-lib/src/bootstrap.rs
|
||||
// version: 1
|
||||
|
||||
/// Default root containing KSP runtime configuration documents.
|
||||
pub const DEFAULT_CFG_PATH: &str = "config";
|
||||
/// Default root containing KSP JSON schemas.
|
||||
pub const DEFAULT_SCHEMA_PATH: &str = "config/schemas";
|
||||
/// Bootstrap argument used to replace the configuration document root.
|
||||
pub const ARG_CFG_PATH: &str = "--cfgpath";
|
||||
/// Bootstrap argument used to replace the schema root.
|
||||
pub const ARG_SCHEMA_PATH: &str = "--schemapath";
|
||||
|
||||
/// Non-recursive bootstrap options required before Config can resolve any managed document.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ConfigBootstrapOptions {
|
||||
cfg_path: std::path::PathBuf,
|
||||
schema_path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl ConfigBootstrapOptions {
|
||||
/// Creates bootstrap options using the KSP hardcoded configuration and schema roots.
|
||||
pub fn defaults() -> ksp_core_lib::Result<Self> {
|
||||
return Self::from_paths(crate::DEFAULT_CFG_PATH, crate::DEFAULT_SCHEMA_PATH);
|
||||
}
|
||||
|
||||
/// Creates bootstrap options from explicit programmatic configuration and schema roots.
|
||||
pub fn from_paths(
|
||||
cfg_path: impl std::convert::Into<std::path::PathBuf>,
|
||||
schema_path: impl std::convert::Into<std::path::PathBuf>,
|
||||
) -> ksp_core_lib::Result<Self> {
|
||||
let cfg_path = validate_bootstrap_path(crate::ARG_CFG_PATH, cfg_path.into());
|
||||
let cfg_path = match cfg_path {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let schema_path = validate_bootstrap_path(crate::ARG_SCHEMA_PATH, schema_path.into());
|
||||
return match schema_path {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(Self { cfg_path, schema_path: value }),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Parses the KSP-owned bootstrap path arguments from a raw process argument slice.
|
||||
///
|
||||
/// Both `--cfgpath=value` / `--cfgpath value` and `--schemapath=value` / `--schemapath value` are accepted. Unrelated arguments are ignored so an
|
||||
/// application can pass its complete argument vector. When the same bootstrap path is specified more than once, the last explicit value wins.
|
||||
pub fn from_args(args: &[std::ffi::OsString]) -> ksp_core_lib::Result<Self> {
|
||||
let mut options = Self::defaults_unchecked();
|
||||
let mut index: usize = 0;
|
||||
while index < args.len() {
|
||||
let argument = &args[index];
|
||||
if argument.as_os_str() == std::ffi::OsStr::new(crate::ARG_CFG_PATH) {
|
||||
let parsed = parse_separate_path_argument(args, index, crate::ARG_CFG_PATH);
|
||||
match parsed {
|
||||
std::result::Result::Ok((path, next_index)) => {
|
||||
options.cfg_path = path;
|
||||
index = next_index;
|
||||
},
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
} else if argument.as_os_str() == std::ffi::OsStr::new(crate::ARG_SCHEMA_PATH) {
|
||||
let parsed = parse_separate_path_argument(args, index, crate::ARG_SCHEMA_PATH);
|
||||
match parsed {
|
||||
std::result::Result::Ok((path, next_index)) => {
|
||||
options.schema_path = path;
|
||||
index = next_index;
|
||||
},
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
} else {
|
||||
let inline = parse_inline_path_argument(argument);
|
||||
match inline {
|
||||
std::option::Option::Some((kind, path)) => match kind {
|
||||
BootstrapPathKind::Config => options.cfg_path = path,
|
||||
BootstrapPathKind::Schema => options.schema_path = path,
|
||||
},
|
||||
std::option::Option::None => {},
|
||||
}
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
return Self::from_paths(options.cfg_path, options.schema_path);
|
||||
}
|
||||
|
||||
/// Returns the root used for managed runtime configuration documents.
|
||||
#[must_use]
|
||||
pub fn cfg_path(&self) -> &std::path::Path {
|
||||
return self.cfg_path.as_path();
|
||||
}
|
||||
|
||||
/// Returns the root used for managed JSON schemas.
|
||||
#[must_use]
|
||||
pub fn schema_path(&self) -> &std::path::Path {
|
||||
return self.schema_path.as_path();
|
||||
}
|
||||
|
||||
/// Replaces the configuration document root after applying bootstrap path validation.
|
||||
pub fn with_cfg_path(self, path: impl std::convert::Into<std::path::PathBuf>) -> ksp_core_lib::Result<Self> {
|
||||
let validated = validate_bootstrap_path(crate::ARG_CFG_PATH, path.into());
|
||||
return match validated {
|
||||
std::result::Result::Ok(cfg_path) => std::result::Result::Ok(Self { cfg_path, schema_path: self.schema_path }),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Replaces the schema root after applying bootstrap path validation.
|
||||
pub fn with_schema_path(self, path: impl std::convert::Into<std::path::PathBuf>) -> ksp_core_lib::Result<Self> {
|
||||
let validated = validate_bootstrap_path(crate::ARG_SCHEMA_PATH, path.into());
|
||||
return match validated {
|
||||
std::result::Result::Ok(schema_path) => std::result::Result::Ok(Self { cfg_path: self.cfg_path, schema_path }),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
fn defaults_unchecked() -> Self {
|
||||
return Self {
|
||||
cfg_path: std::path::PathBuf::from(crate::DEFAULT_CFG_PATH),
|
||||
schema_path: std::path::PathBuf::from(crate::DEFAULT_SCHEMA_PATH),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum BootstrapPathKind {
|
||||
Config,
|
||||
Schema,
|
||||
}
|
||||
|
||||
fn parse_inline_path_argument(argument: &std::ffi::OsStr) -> std::option::Option<(BootstrapPathKind, std::path::PathBuf)> {
|
||||
let text = match argument.to_str() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
};
|
||||
let cfg_prefix = "--cfgpath=";
|
||||
let schema_prefix = "--schemapath=";
|
||||
if let std::option::Option::Some(value) = text.strip_prefix(cfg_prefix) {
|
||||
return std::option::Option::Some((BootstrapPathKind::Config, std::path::PathBuf::from(value)));
|
||||
}
|
||||
if let std::option::Option::Some(value) = text.strip_prefix(schema_prefix) {
|
||||
return std::option::Option::Some((BootstrapPathKind::Schema, std::path::PathBuf::from(value)));
|
||||
}
|
||||
return std::option::Option::None;
|
||||
}
|
||||
|
||||
fn parse_separate_path_argument(args: &[std::ffi::OsString], index: usize, argument_name: &'static str) -> ksp_core_lib::Result<(std::path::PathBuf, usize)> {
|
||||
let value_index = index + 1;
|
||||
if value_index >= args.len() {
|
||||
return std::result::Result::Err(missing_argument_value_error(argument_name));
|
||||
}
|
||||
let value = &args[value_index];
|
||||
let option_like = match value.to_str() {
|
||||
std::option::Option::Some(text) => text.starts_with("--"),
|
||||
std::option::Option::None => false,
|
||||
};
|
||||
if option_like {
|
||||
return std::result::Result::Err(missing_argument_value_error(argument_name));
|
||||
}
|
||||
return std::result::Result::Ok((std::path::PathBuf::from(value.as_os_str()), value_index));
|
||||
}
|
||||
|
||||
fn validate_bootstrap_path(argument_name: &'static str, path: std::path::PathBuf) -> ksp_core_lib::Result<std::path::PathBuf> {
|
||||
if path.as_os_str().is_empty() {
|
||||
return std::result::Result::Err(invalid_path_error(argument_name, &path, "path is empty"));
|
||||
}
|
||||
let metadata = std::fs::metadata(path.as_path());
|
||||
return match metadata {
|
||||
std::result::Result::Ok(value) => {
|
||||
if value.is_dir() {
|
||||
std::result::Result::Ok(path)
|
||||
} else {
|
||||
std::result::Result::Err(invalid_path_error(argument_name, &path, "existing path is not a directory"))
|
||||
}
|
||||
},
|
||||
std::result::Result::Err(error) => {
|
||||
if error.kind() == std::io::ErrorKind::NotFound {
|
||||
std::result::Result::Ok(path)
|
||||
} else {
|
||||
std::result::Result::Err(invalid_path_source_error(argument_name, &path, error))
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn missing_argument_value_error(argument_name: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE, "Config bootstrap argument requires a path value")
|
||||
.with_context("argument", argument_name);
|
||||
}
|
||||
|
||||
fn invalid_path_error(argument_name: &'static str, path: &std::path::Path, reason: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_BOOTSTRAP_INVALID_PATH, "Config bootstrap path is invalid")
|
||||
.with_context("argument", argument_name)
|
||||
.with_context("path", path.to_string_lossy().into_owned())
|
||||
.with_context("reason", reason);
|
||||
}
|
||||
|
||||
fn invalid_path_source_error(argument_name: &'static str, path: &std::path::Path, source: std::io::Error) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_BOOTSTRAP_INVALID_PATH, "Config bootstrap path cannot be inspected")
|
||||
.with_context("argument", argument_name)
|
||||
.with_context("path", path.to_string_lossy().into_owned())
|
||||
.with_source(source);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/bootstrap.rs"]
|
||||
mod tests;
|
||||
8
crates/ksp-config-lib/src/error.rs
Normal file
8
crates/ksp-config-lib/src/error.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
// file: crates/ksp-config-lib/src/error.rs
|
||||
// version: 1
|
||||
|
||||
/// 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");
|
||||
|
||||
/// Error code used when a Config bootstrap path is empty, inaccessible, or resolves to an existing non-directory path.
|
||||
pub const ERROR_CODE_BOOTSTRAP_INVALID_PATH: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "bootstrap_invalid_path");
|
||||
29
crates/ksp-config-lib/src/lib.rs
Normal file
29
crates/ksp-config-lib/src/lib.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
// file: crates/ksp-config-lib/src/lib.rs
|
||||
// version: 1
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! KSP-owned application configuration facade.
|
||||
//!
|
||||
//! `0.1.3-pre.002` establishes the non-recursive bootstrap boundary used before any managed configuration document can be read. Configuration and schema
|
||||
//! roots have hardcoded KSP defaults and can only be replaced through explicit bootstrap arguments or the programmatic [`ConfigBootstrapOptions`] API.
|
||||
//! Document registries, JSON/schema loading, profiles, environment resolution and persistence are introduced by later bounded prereleases.
|
||||
|
||||
mod bootstrap;
|
||||
mod error;
|
||||
|
||||
/// Bootstrap argument used to replace the configuration document root.
|
||||
pub use self::bootstrap::ARG_CFG_PATH;
|
||||
/// Bootstrap argument used to replace the schema root.
|
||||
pub use self::bootstrap::ARG_SCHEMA_PATH;
|
||||
/// Non-recursive bootstrap options required before Config can resolve any managed document.
|
||||
pub use self::bootstrap::ConfigBootstrapOptions;
|
||||
/// Default root containing KSP runtime configuration documents.
|
||||
pub use self::bootstrap::DEFAULT_CFG_PATH;
|
||||
/// Default root containing KSP JSON schemas.
|
||||
pub use self::bootstrap::DEFAULT_SCHEMA_PATH;
|
||||
/// 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;
|
||||
40
crates/ksp-config-lib/tests/public_api.rs
Normal file
40
crates/ksp-config-lib/tests/public_api.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
// file: crates/ksp-config-lib/tests/public_api.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn bootstrap_contract_is_available_from_crate_root() {
|
||||
let result = ksp_config_lib::ConfigBootstrapOptions::defaults();
|
||||
assert!(result.is_ok(), "default bootstrap options should be available: {result:?}");
|
||||
if let std::result::Result::Ok(options) = result {
|
||||
assert_eq!(options.cfg_path(), std::path::Path::new(ksp_config_lib::DEFAULT_CFG_PATH));
|
||||
assert_eq!(options.schema_path(), std::path::Path::new(ksp_config_lib::DEFAULT_SCHEMA_PATH));
|
||||
assert_eq!(ksp_config_lib::ARG_CFG_PATH, "--cfgpath");
|
||||
assert_eq!(ksp_config_lib::ARG_SCHEMA_PATH, "--schemapath");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn programmatic_bootstrap_paths_are_independent() {
|
||||
let result = ksp_config_lib::ConfigBootstrapOptions::from_paths("runtime-config", "runtime-schemas");
|
||||
assert!(result.is_ok(), "programmatic bootstrap paths should be valid: {result:?}");
|
||||
if let std::result::Result::Ok(options) = result {
|
||||
assert_eq!(options.cfg_path(), std::path::Path::new("runtime-config"));
|
||||
assert_eq!(options.schema_path(), std::path::Path::new("runtime-schemas"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_bootstrap_parser_is_available_from_crate_root() {
|
||||
let args = [
|
||||
std::ffi::OsString::from("consumer"),
|
||||
std::ffi::OsString::from("--cfgpath=consumer-config"),
|
||||
std::ffi::OsString::from("--schemapath"),
|
||||
std::ffi::OsString::from("consumer-schemas"),
|
||||
];
|
||||
let result = ksp_config_lib::ConfigBootstrapOptions::from_args(&args);
|
||||
assert!(result.is_ok(), "public bootstrap parser should accept KSP path arguments: {result:?}");
|
||||
if let std::result::Result::Ok(options) = result {
|
||||
assert_eq!(options.cfg_path(), std::path::Path::new("consumer-config"));
|
||||
assert_eq!(options.schema_path(), std::path::Path::new("consumer-schemas"));
|
||||
}
|
||||
}
|
||||
142
crates/ksp-config-lib/unit_tests/bootstrap.rs
Normal file
142
crates/ksp-config-lib/unit_tests/bootstrap.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/bootstrap.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn defaults_use_hardcoded_ksp_roots() {
|
||||
let result = super::ConfigBootstrapOptions::defaults();
|
||||
assert!(result.is_ok(), "default bootstrap paths should be valid: {result:?}");
|
||||
if let std::result::Result::Ok(options) = result {
|
||||
assert_eq!(options.cfg_path(), std::path::Path::new(super::DEFAULT_CFG_PATH));
|
||||
assert_eq!(options.schema_path(), std::path::Path::new(super::DEFAULT_SCHEMA_PATH));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cfg_path_override_keeps_schema_default() {
|
||||
let defaults = super::ConfigBootstrapOptions::defaults();
|
||||
assert!(defaults.is_ok(), "default bootstrap paths should be valid: {defaults:?}");
|
||||
if let std::result::Result::Ok(options) = defaults {
|
||||
let result = options.with_cfg_path("custom-config");
|
||||
assert!(result.is_ok(), "cfg path override should be valid: {result:?}");
|
||||
if let std::result::Result::Ok(options) = result {
|
||||
assert_eq!(options.cfg_path(), std::path::Path::new("custom-config"));
|
||||
assert_eq!(options.schema_path(), std::path::Path::new(super::DEFAULT_SCHEMA_PATH));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_path_override_keeps_cfg_default() {
|
||||
let defaults = super::ConfigBootstrapOptions::defaults();
|
||||
assert!(defaults.is_ok(), "default bootstrap paths should be valid: {defaults:?}");
|
||||
if let std::result::Result::Ok(options) = defaults {
|
||||
let result = options.with_schema_path("custom-schemas");
|
||||
assert!(result.is_ok(), "schema path override should be valid: {result:?}");
|
||||
if let std::result::Result::Ok(options) = result {
|
||||
assert_eq!(options.cfg_path(), std::path::Path::new(super::DEFAULT_CFG_PATH));
|
||||
assert_eq!(options.schema_path(), std::path::Path::new("custom-schemas"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cfg_cli_override_keeps_schema_default() {
|
||||
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--cfgpath=cli-config")];
|
||||
let result = super::ConfigBootstrapOptions::from_args(&args);
|
||||
assert!(result.is_ok(), "cfg CLI override should parse: {result:?}");
|
||||
if let std::result::Result::Ok(options) = result {
|
||||
assert_eq!(options.cfg_path(), std::path::Path::new("cli-config"));
|
||||
assert_eq!(options.schema_path(), std::path::Path::new(super::DEFAULT_SCHEMA_PATH));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_cli_override_keeps_cfg_default() {
|
||||
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--schemapath=cli-schemas")];
|
||||
let result = super::ConfigBootstrapOptions::from_args(&args);
|
||||
assert!(result.is_ok(), "schema CLI override should parse: {result:?}");
|
||||
if let std::result::Result::Ok(options) = result {
|
||||
assert_eq!(options.cfg_path(), std::path::Path::new(super::DEFAULT_CFG_PATH));
|
||||
assert_eq!(options.schema_path(), std::path::Path::new("cli-schemas"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parser_accepts_inline_and_separate_forms_and_last_value_wins() {
|
||||
let args = [
|
||||
std::ffi::OsString::from("ksp-app"),
|
||||
std::ffi::OsString::from("--cfgpath=first-config"),
|
||||
std::ffi::OsString::from("--unrelated"),
|
||||
std::ffi::OsString::from("--cfgpath"),
|
||||
std::ffi::OsString::from("second-config"),
|
||||
std::ffi::OsString::from("--schemapath=first-schemas"),
|
||||
std::ffi::OsString::from("--schemapath"),
|
||||
std::ffi::OsString::from("second-schemas"),
|
||||
];
|
||||
let result = super::ConfigBootstrapOptions::from_args(&args);
|
||||
assert!(result.is_ok(), "bootstrap arguments should parse: {result:?}");
|
||||
if let std::result::Result::Ok(options) = result {
|
||||
assert_eq!(options.cfg_path(), std::path::Path::new("second-config"));
|
||||
assert_eq!(options.schema_path(), std::path::Path::new("second-schemas"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parser_reports_missing_separate_value() {
|
||||
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--cfgpath")];
|
||||
let result = super::ConfigBootstrapOptions::from_args(&args);
|
||||
assert!(result.is_err(), "missing value must be rejected");
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parser_reports_another_option_as_missing_separate_value() {
|
||||
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--cfgpath"), std::ffi::OsString::from("--other-option")];
|
||||
let result = super::ConfigBootstrapOptions::from_args(&args);
|
||||
assert!(result.is_err(), "another option must not become a bootstrap path value");
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_inline_path_is_rejected() {
|
||||
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--schemapath=")];
|
||||
let result = super::ConfigBootstrapOptions::from_args(&args);
|
||||
assert!(result.is_err(), "empty path must be rejected");
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_BOOTSTRAP_INVALID_PATH);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_programmatic_paths_do_not_depend_on_default_roots() {
|
||||
let result = super::ConfigBootstrapOptions::from_paths("programmatic-config", "programmatic-schemas");
|
||||
assert!(result.is_ok(), "explicit programmatic paths should be valid: {result:?}");
|
||||
if let std::result::Result::Ok(options) = result {
|
||||
assert_eq!(options.cfg_path(), std::path::Path::new("programmatic-config"));
|
||||
assert_eq!(options.schema_path(), std::path::Path::new("programmatic-schemas"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_non_directory_path_is_rejected() {
|
||||
let fixture = unique_fixture_path("existing-file");
|
||||
let create = std::fs::write(fixture.as_path(), b"fixture");
|
||||
assert!(create.is_ok(), "fixture file should be creatable: {create:?}");
|
||||
let result = super::ConfigBootstrapOptions::from_paths(fixture.as_path(), "programmatic-schemas");
|
||||
let remove = std::fs::remove_file(fixture.as_path());
|
||||
assert!(remove.is_ok(), "fixture file should be removable: {remove:?}");
|
||||
assert!(result.is_err(), "existing file must not be accepted as a bootstrap directory");
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_BOOTSTRAP_INVALID_PATH);
|
||||
}
|
||||
}
|
||||
|
||||
fn unique_fixture_path(name: &str) -> std::path::PathBuf {
|
||||
let mut path = std::env::temp_dir();
|
||||
path.push(format!("ksp-config-lib-{name}-{}", std::process::id()));
|
||||
return path;
|
||||
}
|
||||
233
deltas/0.1.3/pre.002.md
Normal file
233
deltas/0.1.3/pre.002.md
Normal file
@@ -0,0 +1,233 @@
|
||||
<!-- file: deltas/0.1.3/pre.002.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.3-pre.002
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison documentaire précédente validée :
|
||||
|
||||
```text
|
||||
0.1.3-pre.001-fix.003
|
||||
```
|
||||
|
||||
La base porte :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.3-pre.1"
|
||||
Cargo.toml header version = 40
|
||||
```
|
||||
|
||||
Le plan `docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md` est en version `4` et borne `pre.002` à la création de `ksp-config-lib` et au bootstrap `cfgpath` / `schemapath` uniquement.
|
||||
|
||||
## Objet de pre.002
|
||||
|
||||
Cette tranche ouvre le développement fonctionnel de Config sans anticiper les tranches suivantes.
|
||||
|
||||
Elle :
|
||||
|
||||
- crée `ksp-config-lib` ;
|
||||
- ajoute la crate au workspace ;
|
||||
- fixe les deux racines bootstrap non récursives ;
|
||||
- expose leur équivalent programmatique ;
|
||||
- possède le parsing des deux arguments CLI correspondants ;
|
||||
- valide les chemins bootstrap ;
|
||||
- introduit uniquement les erreurs Config nécessaires à cette surface ;
|
||||
- ajoute les tests unitaires et d'intégration de cette API publique.
|
||||
|
||||
Elle n'introduit pas encore :
|
||||
|
||||
- le registre `file_id -> filename` ;
|
||||
- `--filemap` ;
|
||||
- `serde`, `serde_json` ou `jsonschema` ;
|
||||
- les documents JSON runtime ;
|
||||
- les profils/composites ;
|
||||
- `.env` ou `std::env::var` ;
|
||||
- l'interpolation `${...}` ;
|
||||
- les secrets ;
|
||||
- la persistence ;
|
||||
- une dépendance directe à `ksp-logging-lib` tant qu'aucun événement Config ne l'utilise réellement.
|
||||
|
||||
## Crate `ksp-config-lib`
|
||||
|
||||
La nouvelle crate hérite de la version, de l'édition, du repository et des lints du workspace.
|
||||
|
||||
Sa seule dépendance est actuellement :
|
||||
|
||||
```toml
|
||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||
```
|
||||
|
||||
Cela respecte la règle d'ajout des dépendances uniquement lorsqu'elles sont réellement utilisées. La direction architecturale future reste `ksp-config-lib -> ksp-logging-lib`, mais cette dépendance n'est pas ajoutée prématurément dans `pre.002`.
|
||||
|
||||
## Bootstrap non récursif
|
||||
|
||||
Les défauts KSP sont codés dans Config :
|
||||
|
||||
```text
|
||||
DEFAULT_CFG_PATH = "config"
|
||||
DEFAULT_SCHEMA_PATH = "config/schemas"
|
||||
```
|
||||
|
||||
Ils ne dépendent d'aucun document Config, `.env` ou variable applicative.
|
||||
|
||||
La surface publique introduite est :
|
||||
|
||||
```text
|
||||
ConfigBootstrapOptions::defaults()
|
||||
ConfigBootstrapOptions::from_paths(...)
|
||||
ConfigBootstrapOptions::from_args(...)
|
||||
ConfigBootstrapOptions::cfg_path()
|
||||
ConfigBootstrapOptions::schema_path()
|
||||
ConfigBootstrapOptions::with_cfg_path(...)
|
||||
ConfigBootstrapOptions::with_schema_path(...)
|
||||
```
|
||||
|
||||
Les arguments possédés par Config sont :
|
||||
|
||||
```text
|
||||
--cfgpath
|
||||
--schemapath
|
||||
```
|
||||
|
||||
Le parser accepte les deux formes :
|
||||
|
||||
```text
|
||||
--cfgpath=/path/to/configs
|
||||
--cfgpath /path/to/configs
|
||||
|
||||
--schemapath=/path/to/schemas
|
||||
--schemapath /path/to/schemas
|
||||
```
|
||||
|
||||
Les arguments étrangers sont ignorés afin qu'une application puisse transmettre son vecteur d'arguments complet à Config. Si un même path est fourni plusieurs fois, le dernier override explicite gagne.
|
||||
|
||||
Les deux roots restent indépendants : remplacer `cfgpath` ne modifie pas `schemapath`, et inversement.
|
||||
|
||||
## Validation des chemins
|
||||
|
||||
`pre.002` applique seulement les garanties qui sont valides avant la création des premiers documents runtime :
|
||||
|
||||
- path vide : refusé ;
|
||||
- path existant et répertoire : accepté ;
|
||||
- path existant mais non répertoire : refusé ;
|
||||
- path inexistant : accepté, car `config/` et `config/schemas/` ne sont créés que dans une tranche ultérieure ;
|
||||
- path relatif ou absolu : accepté.
|
||||
|
||||
Un argument séparé sans valeur, ou immédiatement suivi d'une autre option `--...`, produit une erreur dédiée.
|
||||
|
||||
## Erreurs Config initiales
|
||||
|
||||
Les codes restent possédés par `ksp-config-lib` avec le domaine `config` :
|
||||
|
||||
```text
|
||||
config.bootstrap_argument_missing_value
|
||||
config.bootstrap_invalid_path
|
||||
```
|
||||
|
||||
Ils utilisent les contrats existants de Core :
|
||||
|
||||
```text
|
||||
ksp_core_lib::Error
|
||||
ksp_core_lib::ErrorCode
|
||||
ksp_core_lib::Result<T>
|
||||
```
|
||||
|
||||
Core ne reçoit aucune connaissance métier Config.
|
||||
|
||||
## Tests ajoutés
|
||||
|
||||
Les tests unitaires couvrent notamment :
|
||||
|
||||
- les deux défauts hardcodés ;
|
||||
- l'indépendance des overrides cfg/schema ;
|
||||
- les formes CLI inline et séparées ;
|
||||
- la règle du dernier override ;
|
||||
- l'ignorance des arguments étrangers ;
|
||||
- l'absence de valeur ;
|
||||
- le refus d'un path vide ;
|
||||
- l'acceptation d'un path programmatique inexistant ;
|
||||
- le refus d'un path existant qui est un fichier.
|
||||
|
||||
Les tests d'intégration vérifient la façade publique au crate-root et le parsing consommable par une crate externe.
|
||||
|
||||
## Version technique
|
||||
|
||||
La prerelease devient :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.3-pre.2"
|
||||
```
|
||||
|
||||
Le manifest racine devient :
|
||||
|
||||
```text
|
||||
# version: 41
|
||||
```
|
||||
|
||||
Le plan Config devient :
|
||||
|
||||
```text
|
||||
<!-- version: 5 -->
|
||||
```
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
```text
|
||||
crates/ksp-config-lib/Cargo.toml
|
||||
crates/ksp-config-lib/src/bootstrap.rs
|
||||
crates/ksp-config-lib/src/error.rs
|
||||
crates/ksp-config-lib/src/lib.rs
|
||||
crates/ksp-config-lib/unit_tests/bootstrap.rs
|
||||
crates/ksp-config-lib/tests/public_api.rs
|
||||
deltas/0.1.3/pre.002.md
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md
|
||||
```
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Validations non exécutées à faire dans l'environnement utilisateur
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Aucun script d'audit Rust/KSP exécutable n'est présent dans la base reconstruite de cette tranche.
|
||||
|
||||
L'environnement de génération du delta ne fournit pas `cargo`, `rustc` ni `rustfmt`. Aucune commande Cargo ci-dessus n'est donc déclarée réussie avant la validation dans l'environnement utilisateur.
|
||||
|
||||
## Validations exécutées avant livraison
|
||||
|
||||
Des contrôles statiques ont vérifié avant livraison :
|
||||
|
||||
- headers `file:` / `version:` présents sur les nouveaux fichiers ;
|
||||
- aucune ligne Rust supérieure à 160 colonnes avant formatage ;
|
||||
- aucun `unsafe`, `unwrap`, `expect`, `panic!` ou opérateur `?` dans `src/` ;
|
||||
- aucun `use` de non-trait ;
|
||||
- aucune lecture de variable applicative par `std::env` dans `src/` ;
|
||||
- aucune dépendance externe nouvelle ;
|
||||
- aucun `Cargo.lock` ajouté au delta.
|
||||
|
||||
## Suite après validation
|
||||
|
||||
Si `pre.002` est validée, la prochaine tranche prévue est :
|
||||
|
||||
```text
|
||||
0.1.3-pre.003 — registre logique file_id -> filename + --filemap
|
||||
```
|
||||
|
||||
Elle ne doit pas encore lire les documents JSON ou leurs schemas.
|
||||
@@ -1,11 +1,11 @@
|
||||
<!-- file: docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md -->
|
||||
<!-- version: 4 -->
|
||||
<!-- version: 5 -->
|
||||
|
||||
# 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`, complété par `0.1.3-pre.001-fix.002`, puis regranularisé par `0.1.3-pre.001-fix.003` avant tout développement fonctionnel de Config.
|
||||
Ce plan a été établi par `0.1.3-pre.001`, corrigé par `0.1.3-pre.001-fix.001`, complété par `0.1.3-pre.001-fix.002`, puis regranularisé par `0.1.3-pre.001-fix.003`. `0.1.3-pre.002` ouvre maintenant le développement fonctionnel avec la seule fondation bootstrap de `ksp-config-lib`.
|
||||
|
||||
La base auditée reste la release stable `v0.1.2`.
|
||||
|
||||
@@ -230,25 +230,25 @@ La complétion Logging n'ouvre ni une nouvelle façade ni une dépendance invers
|
||||
|
||||
## 5. Matrice de responsabilités
|
||||
|
||||
| Responsabilité | Propriétaire | Consommateurs | Interdit |
|
||||
|---|---|---|---|
|
||||
| Définir les `file_id` connus et leur mapping par défaut | `ksp-config-lib` | bootstrap Config | noms physiques codés dans les consumers |
|
||||
| Résoudre `file_id -> filename` | `ksp-config-lib` | session Config | référence inter-document par filename |
|
||||
| Interpréter `--cfgpath`, `--schemapath` et overrides de mapping KSP | `ksp-config-lib` | applications qui transmettent argv/options | parser ces options différemment dans chaque binaire |
|
||||
| Lire un document Config JSON | `ksp-config-lib` | crates/apps via API Config | lecture directe par consumer |
|
||||
| Valider JSON Schema | `ksp-config-lib` | orchestration/management | validation divergente dans chaque crate |
|
||||
| Résoudre globals/profils/compositions | `ksp-config-lib` | orchestration | résolution locale dans un binaire |
|
||||
| Lire `KSP_*` / `KSPB_*` du processus | `ksp-config-lib` | crates/apps via API Config | `std::env::var*` applicatif hors Config |
|
||||
| Lire `.env` | `ksp-config-lib` | crates/apps via API Config | loader dotenv direct hors Config |
|
||||
| Résoudre `${...}` et fallback | `ksp-config-lib` | tous les consumers | interpolation locale dans les consumers |
|
||||
| Classer sensibilité et construire une valeur sûre | `ksp-config-lib` | runtime/logging/diagnostics | redaction ad hoc dans chaque crate |
|
||||
| Modifier/sauvegarder JSON Config | `ksp-config-lib` | management explicite | écriture directe par application |
|
||||
| Créer/modifier/supprimer une entrée `.env` | `ksp-config-lib` | management explicite | édition directe par application |
|
||||
| Modifier l'environnement externe du shell/systemd/parent | propriétaire externe de ce processus | Config le lit seulement | prétendre qu'un child process peut administrer son parent |
|
||||
| Posséder les settings/sinks/routing Logging | `ksp-logging-lib` | Config construit les contrats publics | logique de routing dupliquée dans Config |
|
||||
| Posséder `LoggingGuard` | orchestration/application | lifecycle Logging | singleton Config global |
|
||||
| Initialiser/recharger Logging | orchestration | Config fournit settings/changement | Logging lisant Config |
|
||||
| DTO/bindings Tauri | application Tauri | frontend | TS-RS automatique dans Config |
|
||||
| Responsabilité | Propriétaire | Consommateurs | Interdit |
|
||||
|---------------------------------------------------------------------|--------------------------------------|--------------------------------------------|-----------------------------------------------------------|
|
||||
| Définir les `file_id` connus et leur mapping par défaut | `ksp-config-lib` | bootstrap Config | noms physiques codés dans les consumers |
|
||||
| Résoudre `file_id -> filename` | `ksp-config-lib` | session Config | référence inter-document par filename |
|
||||
| Interpréter `--cfgpath`, `--schemapath` et overrides de mapping KSP | `ksp-config-lib` | applications qui transmettent argv/options | parser ces options différemment dans chaque binaire |
|
||||
| Lire un document Config JSON | `ksp-config-lib` | crates/apps via API Config | lecture directe par consumer |
|
||||
| Valider JSON Schema | `ksp-config-lib` | orchestration/management | validation divergente dans chaque crate |
|
||||
| Résoudre globals/profils/compositions | `ksp-config-lib` | orchestration | résolution locale dans un binaire |
|
||||
| Lire `KSP_*` / `KSPB_*` du processus | `ksp-config-lib` | crates/apps via API Config | `std::env::var*` applicatif hors Config |
|
||||
| Lire `.env` | `ksp-config-lib` | crates/apps via API Config | loader dotenv direct hors Config |
|
||||
| Résoudre `${...}` et fallback | `ksp-config-lib` | tous les consumers | interpolation locale dans les consumers |
|
||||
| Classer sensibilité et construire une valeur sûre | `ksp-config-lib` | runtime/logging/diagnostics | redaction ad hoc dans chaque crate |
|
||||
| Modifier/sauvegarder JSON Config | `ksp-config-lib` | management explicite | écriture directe par application |
|
||||
| Créer/modifier/supprimer une entrée `.env` | `ksp-config-lib` | management explicite | édition directe par application |
|
||||
| Modifier l'environnement externe du shell/systemd/parent | propriétaire externe de ce processus | Config le lit seulement | prétendre qu'un child process peut administrer son parent |
|
||||
| Posséder les settings/sinks/routing Logging | `ksp-logging-lib` | Config construit les contrats publics | logique de routing dupliquée dans Config |
|
||||
| Posséder `LoggingGuard` | orchestration/application | lifecycle Logging | singleton Config global |
|
||||
| Initialiser/recharger Logging | orchestration | Config fournit settings/changement | Logging lisant Config |
|
||||
| DTO/bindings Tauri | application Tauri | frontend | TS-RS automatique dans Config |
|
||||
|
||||
## 6. Registre de fichiers, bootstrap, arborescence et nomenclature
|
||||
|
||||
@@ -580,23 +580,23 @@ Décisions :
|
||||
|
||||
### 8.1 Écart `ksp-logging-lib 0.1.2` à fermer
|
||||
|
||||
| Capacité | `0.1.2` | Requise par `std.logging.json` |
|
||||
|---|---:|---:|
|
||||
| filtre global | oui | oui |
|
||||
| overrides par target | oui | oui |
|
||||
| lifecycle spans | oui | oui |
|
||||
| console stdout/stderr | oui | oui |
|
||||
| console enabled | via `Option` | oui explicite |
|
||||
| console ANSI configurable | non | oui |
|
||||
| format console configurable | non | oui |
|
||||
| plusieurs fichiers | non | oui |
|
||||
| rotation par fichier | un seul fichier | oui par sink |
|
||||
| format par fichier | non | oui |
|
||||
| filtre par sink/target | non | oui |
|
||||
| filtre par sink/domain | non | oui |
|
||||
| filtre par sink/niveau | non indépendant | oui |
|
||||
| hot reload transactionnel | oui | à conserver |
|
||||
| non-blocking/guards/drop counters | oui | à conserver et généraliser par sinks |
|
||||
| Capacité | `0.1.2` | Requise par `std.logging.json` |
|
||||
|-----------------------------------|----------------:|-------------------------------------:|
|
||||
| filtre global | oui | oui |
|
||||
| overrides par target | oui | oui |
|
||||
| lifecycle spans | oui | oui |
|
||||
| console stdout/stderr | oui | oui |
|
||||
| console enabled | via `Option` | oui explicite |
|
||||
| console ANSI configurable | non | oui |
|
||||
| format console configurable | non | oui |
|
||||
| plusieurs fichiers | non | oui |
|
||||
| rotation par fichier | un seul fichier | oui par sink |
|
||||
| format par fichier | non | oui |
|
||||
| filtre par sink/target | non | oui |
|
||||
| filtre par sink/domain | non | oui |
|
||||
| filtre par sink/niveau | non indépendant | oui |
|
||||
| hot reload transactionnel | oui | à conserver |
|
||||
| non-blocking/guards/drop counters | oui | à conserver et généraliser par sinks |
|
||||
|
||||
Ce tableau est un **gap identifié**, pas une invitation à déplacer Logging dans Config. La tranche qui le ferme modifie `ksp-logging-lib` uniquement dans son domaine propriétaire.
|
||||
|
||||
@@ -1569,6 +1569,19 @@ Objectif unique : créer la crate et fixer comment Config trouve ses deux racine
|
||||
- tests unitaires du bootstrap ;
|
||||
- **aucun registre `file_id` dans cette tranche**.
|
||||
|
||||
Implémentation candidate livrée par `pre.002` :
|
||||
|
||||
- `ksp-config-lib` entre dans le workspace avec `ksp-core-lib` comme seule dépendance actuellement utilisée ;
|
||||
- `ksp-logging-lib` reste une dépendance architecturale future de Config mais n'est pas ajoutée tant qu'aucun événement Config ne l'utilise réellement ;
|
||||
- `DEFAULT_CFG_PATH = "config"` et `DEFAULT_SCHEMA_PATH = "config/schemas"` sont codés dans Config ;
|
||||
- `ConfigBootstrapOptions::defaults()` valide les deux défauts ;
|
||||
- `ConfigBootstrapOptions::from_paths(...)` fournit l'équivalent programmatique explicite sans dépendre des chemins par défaut ;
|
||||
- `ConfigBootstrapOptions::from_args(...)` accepte `--cfgpath=value`, `--cfgpath value`, `--schemapath=value` et `--schemapath value`, ignore les arguments étrangers et applique le dernier override explicite d'un même path ;
|
||||
- un path inexistant est accepté à ce stade, un path vide ou un path existant non répertoire est refusé ;
|
||||
- le registre `file_id`, `--filemap`, JSON, schemas, profils et environnement restent strictement hors de `pre.002`.
|
||||
|
||||
La validation utilisateur de cette implémentation reste requise avant l'ouverture de `pre.003`.
|
||||
|
||||
### `0.1.3-pre.003` — registre logique `file_id -> filename`
|
||||
|
||||
Objectif unique : rendre les noms physiques remplaçables sans modifier l'identité logique des fichiers.
|
||||
|
||||
Reference in New Issue
Block a user