v0.1.3-pre.003

This commit is contained in:
2026-08-15 19:17:09 +02:00
parent 9207919d47
commit 0629a48e97
8 changed files with 898 additions and 11 deletions

View File

@@ -1,8 +1,20 @@
// file: crates/ksp-config-lib/src/error.rs
// version: 1
// version: 2
/// 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");
/// Error code used when a logical Config file identifier is malformed.
pub const ERROR_CODE_FILE_ID_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_id_invalid");
/// Error code used when a requested logical Config file identifier is not registered.
pub const ERROR_CODE_FILE_ID_UNKNOWN: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_id_unknown");
/// Error code used when the same logical Config file identifier is registered more than once.
pub const ERROR_CODE_FILE_ID_DUPLICATE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_id_duplicate");
/// 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");

View File

@@ -1,17 +1,18 @@
// file: crates/ksp-config-lib/src/lib.rs
// version: 1
// version: 2
#![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.
//! `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.
mod bootstrap;
mod error;
mod registry;
/// Bootstrap argument used to replace the configuration document root.
pub use self::bootstrap::ARG_CFG_PATH;
@@ -27,3 +28,29 @@ pub use self::bootstrap::DEFAULT_SCHEMA_PATH;
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 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.
pub use self::error::ERROR_CODE_FILE_ID_INVALID;
/// Error code used when a requested logical Config file identifier is not registered.
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;
/// 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.
pub use self::registry::ConfigFileDescriptor;
/// Stable logical identifier for a Config-managed file.
pub use self::registry::ConfigFileId;
/// Physical root category used to resolve a Config-managed file.
pub use self::registry::ConfigFileKind;
/// Registry of KSP-known logical Config files and their replaceable physical filenames.
pub use self::registry::ConfigFileRegistry;
/// Default physical filename for the standard Logging configuration document.
pub use self::registry::DEFAULT_STD_LOGGING_FILENAME;
/// Default physical filename for the standard Logging JSON Schema document.
pub use self::registry::DEFAULT_STD_LOGGING_SCHEMA_FILENAME;
/// Logical file identifier for the standard Logging JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_STD_LOGGING;
/// Logical file identifier for the standard Logging configuration document.
pub use self::registry::FILE_ID_STD_LOGGING;

View File

@@ -0,0 +1,309 @@
// file: crates/ksp-config-lib/src/registry.rs
// version: 1
/// Bootstrap argument used to replace a known Config filename mapping.
pub const ARG_FILE_MAP: &str = "--filemap";
/// Logical file identifier for the standard Logging configuration document.
pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging";
/// Logical file identifier for the standard Logging JSON Schema document.
pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging";
/// Default physical filename for the standard Logging configuration document.
pub const DEFAULT_STD_LOGGING_FILENAME: &str = "std.logging.json";
/// Default physical filename for the standard Logging JSON Schema document.
pub const DEFAULT_STD_LOGGING_SCHEMA_FILENAME: &str = "std.logging.schema.json";
/// Stable logical identifier for a Config-managed file.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct ConfigFileId(String);
impl ConfigFileId {
/// Creates and validates a logical Config file identifier.
pub fn new(value: impl std::convert::Into<String>) -> ksp_core_lib::Result<Self> {
let value = value.into();
let validated = validate_file_id(value.as_str());
return match validated {
std::result::Result::Ok(()) => std::result::Result::Ok(Self(value)),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
/// Returns the logical identifier as text.
#[must_use]
pub fn as_str(&self) -> &str {
return self.0.as_str();
}
}
/// Physical root category used to resolve a Config-managed file.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ConfigFileKind {
/// Runtime configuration document resolved below `cfgpath`.
Config,
/// JSON Schema document resolved below `schemapath`.
Schema,
}
/// Logical descriptor associating a stable file identifier with its physical filename and root category.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConfigFileDescriptor {
file_id: ConfigFileId,
kind: ConfigFileKind,
filename: std::path::PathBuf,
}
impl ConfigFileDescriptor {
/// Returns the stable logical identifier.
#[must_use]
pub fn file_id(&self) -> &ConfigFileId {
return &self.file_id;
}
/// Returns the root category used when resolving the file.
#[must_use]
pub fn kind(&self) -> ConfigFileKind {
return self.kind;
}
/// Returns the relative physical filename currently mapped to the identifier.
#[must_use]
pub fn filename(&self) -> &std::path::Path {
return self.filename.as_path();
}
fn new(file_id: &'static str, kind: ConfigFileKind, filename: &'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,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let kind_validation = validate_kind_prefix(&file_id, kind);
match kind_validation {
std::result::Result::Ok(()) => {},
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 }),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
}
/// Registry of KSP-known logical Config files and their replaceable physical filenames.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConfigFileRegistry {
descriptors: std::collections::BTreeMap<ConfigFileId, ConfigFileDescriptor>,
}
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 = 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 = match logging_schema {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return build_registry([logging, logging_schema]);
}
/// Creates the default registry and applies repeatable `--filemap=<file_id>=<filename>` overrides from raw process arguments.
///
/// Unrelated arguments are ignored. A repeated mapping for the same known `file_id` is accepted and the last mapping wins.
pub fn from_args(args: &[std::ffi::OsString]) -> ksp_core_lib::Result<Self> {
let registry = Self::defaults();
let mut registry = match registry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut index: usize = 0;
while index < args.len() {
let application = apply_file_map_argument(&mut registry, &args[index]);
match application {
std::result::Result::Ok(()) => {},
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
index += 1;
}
return std::result::Result::Ok(registry);
}
/// Returns the descriptor associated with a known logical file identifier.
pub fn descriptor(&self, file_id: &ConfigFileId) -> ksp_core_lib::Result<&ConfigFileDescriptor> {
return match self.descriptors.get(file_id) {
std::option::Option::Some(descriptor) => std::result::Result::Ok(descriptor),
std::option::Option::None => std::result::Result::Err(unknown_file_id_error(file_id.as_str())),
};
}
/// Resolves a known logical file identifier below the bootstrap root selected by its descriptor kind.
pub fn resolve_path(&self, bootstrap: &crate::ConfigBootstrapOptions, file_id: &ConfigFileId) -> ksp_core_lib::Result<std::path::PathBuf> {
let descriptor = self.descriptor(file_id);
let descriptor = match descriptor {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let root = match descriptor.kind() {
ConfigFileKind::Config => bootstrap.cfg_path(),
ConfigFileKind::Schema => bootstrap.schema_path(),
};
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.
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 {
std::result::Result::Ok(()) => std::result::Result::Ok(self),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
fn set_filename_override(&mut self, file_id: &ConfigFileId, filename: std::path::PathBuf) -> ksp_core_lib::Result<()> {
let descriptor = self.descriptors.get(file_id);
let descriptor = match descriptor {
std::option::Option::Some(value) => value.clone(),
std::option::Option::None => return std::result::Result::Err(unknown_file_id_error(file_id.as_str())),
};
let filename = validate_relative_filename(file_id, filename);
let filename = match filename {
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 };
self.descriptors.insert(file_id.clone(), updated);
return std::result::Result::Ok(());
}
}
fn build_registry<const N: usize>(descriptors: [ConfigFileDescriptor; N]) -> ksp_core_lib::Result<ConfigFileRegistry> {
let mut registry = ConfigFileRegistry { descriptors: std::collections::BTreeMap::new() };
for descriptor in descriptors {
let file_id = descriptor.file_id.clone();
let duplicate_id = file_id.clone();
let previous = registry.descriptors.insert(file_id, descriptor);
if previous.is_some() {
return std::result::Result::Err(duplicate_file_id_error(duplicate_id.as_str()));
}
}
return std::result::Result::Ok(registry);
}
fn apply_file_map_argument(registry: &mut ConfigFileRegistry, argument: &std::ffi::OsStr) -> ksp_core_lib::Result<()> {
let text = match argument.to_str() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(()),
};
if text == ARG_FILE_MAP {
return std::result::Result::Err(invalid_file_mapping_error("--filemap requires the inline form --filemap=<file_id>=<filename>"));
}
let prefix = "--filemap=";
let mapping = match text.strip_prefix(prefix) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(()),
};
let separator = mapping.find('=');
let separator = match separator {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(invalid_file_mapping_error("file mapping must contain a file_id and filename separated by '='"));
},
};
let file_id_text = &mapping[..separator];
let filename_text = &mapping[separator + 1..];
if file_id_text.is_empty() || filename_text.is_empty() {
return std::result::Result::Err(invalid_file_mapping_error("file mapping requires non-empty file_id and filename values"));
}
let file_id = ConfigFileId::new(file_id_text);
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return registry.set_filename_override(&file_id, std::path::PathBuf::from(filename_text));
}
fn validate_file_id(value: &str) -> ksp_core_lib::Result<()> {
if value.is_empty() || value.starts_with('.') || value.ends_with('.') || value.contains("..") {
return std::result::Result::Err(invalid_file_id_error(value));
}
let mut valid = true;
for byte in value.bytes() {
let allowed = byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'.' || byte == b'_' || byte == b'-';
if !allowed {
valid = false;
}
}
if !valid {
return std::result::Result::Err(invalid_file_id_error(value));
}
return std::result::Result::Ok(());
}
fn validate_kind_prefix(file_id: &ConfigFileId, kind: ConfigFileKind) -> ksp_core_lib::Result<()> {
let valid = match kind {
ConfigFileKind::Config => file_id.as_str().starts_with("cfg."),
ConfigFileKind::Schema => file_id.as_str().starts_with("schema."),
};
if !valid {
return std::result::Result::Err(invalid_file_mapping_with_id_error(file_id.as_str(), "file_id prefix does not match descriptor kind"));
}
return std::result::Result::Ok(());
}
fn validate_relative_filename(file_id: &ConfigFileId, filename: std::path::PathBuf) -> ksp_core_lib::Result<std::path::PathBuf> {
if filename.as_os_str().is_empty() || filename.is_absolute() {
return std::result::Result::Err(invalid_filename_error(file_id.as_str(), &filename));
}
let mut has_normal_component = false;
for component in filename.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 std::result::Result::Err(invalid_filename_error(file_id.as_str(), &filename));
},
}
}
if !has_normal_component {
return std::result::Result::Err(invalid_filename_error(file_id.as_str(), &filename));
}
return std::result::Result::Ok(filename);
}
fn invalid_file_id_error(file_id: &str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_FILE_ID_INVALID, "Config file_id is invalid")
.with_context("file_id", file_id)
.with_context("reason", "expected lowercase ASCII segments separated by single dots");
}
fn unknown_file_id_error(file_id: &str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_FILE_ID_UNKNOWN, "Config file_id is not registered").with_context("file_id", file_id);
}
fn duplicate_file_id_error(file_id: &str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_FILE_ID_DUPLICATE, "Config file_id is registered more than once").with_context("file_id", file_id);
}
fn invalid_file_mapping_error(reason: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_FILE_MAPPING_INVALID, "Config file mapping is invalid").with_context("reason", reason);
}
fn invalid_file_mapping_with_id_error(file_id: &str, reason: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_FILE_MAPPING_INVALID, "Config file mapping is invalid")
.with_context("file_id", file_id)
.with_context("reason", reason);
}
fn invalid_filename_error(file_id: &str, filename: &std::path::Path) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_FILE_MAPPING_INVALID, "Config mapped filename is invalid")
.with_context("file_id", file_id)
.with_context("filename", filename.to_string_lossy().into_owned())
.with_context("reason", "filename must stay relative to its Config-owned root without traversal components");
}
#[cfg(test)]
#[path = "../unit_tests/registry.rs"]
mod tests;

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-config-lib/tests/public_api.rs
// version: 2
// version: 3
//! Integration tests for the public `ksp-config-lib` bootstrap contract.
//! Integration tests for the public `ksp-config-lib` bootstrap and logical file registry contracts.
#[test]
fn bootstrap_contract_is_available_from_crate_root() {
@@ -40,3 +40,35 @@ fn cli_bootstrap_parser_is_available_from_crate_root() {
assert_eq!(options.schema_path(), std::path::Path::new("consumer-schemas"));
}
}
#[test]
fn logical_file_registry_is_available_from_crate_root() {
let args = [
std::ffi::OsString::from("consumer"),
std::ffi::OsString::from("--filemap=cfg.std.logging=consumer.logging.json"),
std::ffi::OsString::from("--filemap=schema.std.logging=consumer.logging.schema.json"),
];
let registry = ksp_config_lib::ConfigFileRegistry::from_args(&args);
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_paths("consumer-config", "consumer-schemas");
let logging_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_STD_LOGGING);
let schema_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING);
assert!(registry.is_ok(), "public registry should accept known filemap overrides: {registry:?}");
assert!(bootstrap.is_ok(), "bootstrap paths should remain available: {bootstrap:?}");
assert!(logging_id.is_ok(), "public logging file_id should be valid: {logging_id:?}");
assert!(schema_id.is_ok(), "public schema file_id should be valid: {schema_id:?}");
if let (std::result::Result::Ok(registry), std::result::Result::Ok(bootstrap), std::result::Result::Ok(logging_id), std::result::Result::Ok(schema_id)) =
(registry, bootstrap, logging_id, schema_id)
{
let logging = registry.resolve_path(&bootstrap, &logging_id);
let schema = registry.resolve_path(&bootstrap, &schema_id);
assert!(logging.is_ok(), "public logging path should resolve: {logging:?}");
assert!(schema.is_ok(), "public schema path should resolve: {schema:?}");
if let std::result::Result::Ok(logging) = logging {
assert_eq!(logging, std::path::PathBuf::from("consumer-config/consumer.logging.json"));
}
if let std::result::Result::Ok(schema) = schema {
assert_eq!(schema, std::path::PathBuf::from("consumer-schemas/consumer.logging.schema.json"));
}
assert_eq!(ksp_config_lib::ARG_FILE_MAP, "--filemap");
}
}

View File

@@ -0,0 +1,179 @@
// file: crates/ksp-config-lib/unit_tests/registry.rs
// version: 1
#[test]
fn defaults_register_logging_document_and_schema_with_distinct_roots() {
let registry = super::ConfigFileRegistry::defaults();
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
if let std::result::Result::Ok(registry) = registry {
let logging_id = super::ConfigFileId::new(super::FILE_ID_STD_LOGGING);
let schema_id = super::ConfigFileId::new(super::FILE_ID_SCHEMA_STD_LOGGING);
assert!(logging_id.is_ok(), "logging file_id should be valid: {logging_id:?}");
assert!(schema_id.is_ok(), "logging schema file_id should be valid: {schema_id:?}");
if let (std::result::Result::Ok(logging_id), std::result::Result::Ok(schema_id)) = (logging_id, schema_id) {
let logging = registry.descriptor(&logging_id);
let schema = registry.descriptor(&schema_id);
assert!(logging.is_ok(), "logging descriptor should exist: {logging:?}");
assert!(schema.is_ok(), "logging schema descriptor should exist: {schema:?}");
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));
assert_eq!(schema.kind(), super::ConfigFileKind::Schema);
assert_eq!(schema.filename(), std::path::Path::new(super::DEFAULT_STD_LOGGING_SCHEMA_FILENAME));
}
}
}
}
#[test]
fn resolve_path_uses_descriptor_kind_to_select_bootstrap_root() {
let registry = super::ConfigFileRegistry::defaults();
let bootstrap = crate::ConfigBootstrapOptions::from_paths("runtime-config", "runtime-schemas");
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
assert!(bootstrap.is_ok(), "bootstrap paths should be valid: {bootstrap:?}");
if let (std::result::Result::Ok(registry), std::result::Result::Ok(bootstrap)) = (registry, bootstrap) {
let logging_id = super::ConfigFileId::new(super::FILE_ID_STD_LOGGING);
let schema_id = super::ConfigFileId::new(super::FILE_ID_SCHEMA_STD_LOGGING);
if let (std::result::Result::Ok(logging_id), std::result::Result::Ok(schema_id)) = (logging_id, schema_id) {
let logging = registry.resolve_path(&bootstrap, &logging_id);
let schema = registry.resolve_path(&bootstrap, &schema_id);
assert!(logging.is_ok(), "logging path should resolve: {logging:?}");
assert!(schema.is_ok(), "schema path should resolve: {schema:?}");
if let std::result::Result::Ok(logging) = logging {
assert_eq!(logging, std::path::PathBuf::from("runtime-config/std.logging.json"));
}
if let std::result::Result::Ok(schema) = schema {
assert_eq!(schema, std::path::PathBuf::from("runtime-schemas/std.logging.schema.json"));
}
}
}
}
#[test]
fn cli_filemap_override_replaces_filename_and_last_value_wins() {
let args = [
std::ffi::OsString::from("ksp-app"),
std::ffi::OsString::from("--filemap=cfg.std.logging=first.logging.json"),
std::ffi::OsString::from("--other-option"),
std::ffi::OsString::from("--filemap=cfg.std.logging=profiles/custom.logging.json"),
];
let registry = super::ConfigFileRegistry::from_args(&args);
assert!(registry.is_ok(), "filemap overrides should parse: {registry:?}");
if let std::result::Result::Ok(registry) = registry {
let file_id = super::ConfigFileId::new(super::FILE_ID_STD_LOGGING);
if let std::result::Result::Ok(file_id) = file_id {
let descriptor = registry.descriptor(&file_id);
assert!(descriptor.is_ok(), "logging descriptor should remain registered: {descriptor:?}");
if let std::result::Result::Ok(descriptor) = descriptor {
assert_eq!(descriptor.filename(), std::path::Path::new("profiles/custom.logging.json"));
assert_eq!(descriptor.kind(), super::ConfigFileKind::Config);
}
}
}
}
#[test]
fn programmatic_override_preserves_file_id_and_kind() {
let registry = super::ConfigFileRegistry::defaults();
let file_id = super::ConfigFileId::new(super::FILE_ID_SCHEMA_STD_LOGGING);
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
assert!(file_id.is_ok(), "schema file_id should be valid: {file_id:?}");
if let (std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) = (registry, file_id) {
let overridden = registry.with_filename_override(&file_id, "alternate/logging.schema.json");
assert!(overridden.is_ok(), "programmatic override should be valid: {overridden:?}");
if let std::result::Result::Ok(overridden) = overridden {
let descriptor = overridden.descriptor(&file_id);
if let std::result::Result::Ok(descriptor) = descriptor {
assert_eq!(descriptor.file_id(), &file_id);
assert_eq!(descriptor.kind(), super::ConfigFileKind::Schema);
assert_eq!(descriptor.filename(), std::path::Path::new("alternate/logging.schema.json"));
}
}
}
}
#[test]
fn unknown_file_id_override_is_rejected() {
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--filemap=cfg.unknown=unknown.json")];
let result = super::ConfigFileRegistry::from_args(&args);
assert!(result.is_err(), "unknown logical files must not be introduced by CLI override");
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_UNKNOWN);
}
}
#[test]
fn invalid_file_ids_are_rejected() {
for value in ["", ".cfg", "cfg.", "cfg..logging", "CFG.logging", "cfg/logging"] {
let result = super::ConfigFileId::new(value);
assert!(result.is_err(), "invalid file_id must be rejected: {value}");
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_INVALID);
}
}
}
#[test]
fn absolute_and_traversing_filenames_are_rejected() {
let registry = super::ConfigFileRegistry::defaults();
let file_id = super::ConfigFileId::new(super::FILE_ID_STD_LOGGING);
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(registry), std::result::Result::Ok(file_id)) = (registry, file_id) {
let traversal = registry.clone().with_filename_override(&file_id, "../outside.json");
let current = registry.clone().with_filename_override(&file_id, "./logging.json");
let absolute = registry.with_filename_override(&file_id, absolute_fixture_path());
assert_mapping_invalid(traversal);
assert_mapping_invalid(current);
assert_mapping_invalid(absolute);
}
}
#[test]
fn malformed_filemap_arguments_are_rejected() {
for argument in ["--filemap", "--filemap=cfg.std.logging", "--filemap==logging.json", "--filemap=cfg.std.logging="] {
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from(argument)];
let result = super::ConfigFileRegistry::from_args(&args);
assert!(result.is_err(), "malformed filemap argument must be rejected: {argument}");
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_FILE_MAPPING_INVALID);
}
}
}
#[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");
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) {
let result = super::build_registry([first, second]);
assert!(result.is_err(), "duplicate file_ids must be rejected");
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_DUPLICATE);
}
}
}
#[test]
fn descriptor_kind_must_match_file_id_namespace() {
let result = super::ConfigFileDescriptor::new("schema.invalid-kind", super::ConfigFileKind::Config, "invalid.json");
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);
}
}
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 {
assert_eq!(error.code(), crate::ERROR_CODE_FILE_MAPPING_INVALID);
}
}
fn absolute_fixture_path() -> std::path::PathBuf {
let mut path = std::env::temp_dir();
path.push("ksp-config-lib-absolute-mapping.json");
return path;
}