v0.1.3-pre.013
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/document.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
/// A Config-managed JSON document that has passed syntax, schema and current semantic validation.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -82,7 +82,41 @@ impl ConfigDocumentEngine {
|
||||
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);
|
||||
return self.validate_document(document, &schema_file_id);
|
||||
}
|
||||
|
||||
pub(crate) fn validate_candidate(&self, file_id: &crate::ConfigFileId, value: serde_json::Value) -> 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 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 document = ConfigJsonDocument { file_id: file_id.clone(), path, value };
|
||||
return self.validate_document(document, &schema_file_id);
|
||||
}
|
||||
|
||||
fn validate_document(&self, document: ConfigJsonDocument, schema_file_id: &crate::ConfigFileId) -> ksp_core_lib::Result<ConfigJsonDocument> {
|
||||
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),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/environment.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
/// Default local environment file read by Config from the process launch directory.
|
||||
pub const DEFAULT_DOTENV_PATH: &str = ".env";
|
||||
@@ -231,7 +231,7 @@ impl ConfigEnvironment {
|
||||
};
|
||||
}
|
||||
|
||||
fn load_from_dotenv_path(dotenv_path: &std::path::Path) -> ksp_core_lib::Result<Self> {
|
||||
pub(crate) fn load_from_dotenv_path(dotenv_path: &std::path::Path) -> ksp_core_lib::Result<Self> {
|
||||
let process = collect_process_environment(std::env::vars_os());
|
||||
let process = match process {
|
||||
std::result::Result::Ok(value) => value,
|
||||
@@ -245,6 +245,14 @@ impl ConfigEnvironment {
|
||||
return std::result::Result::Ok(Self { process, dotenv, dotenv_path: dotenv_path.to_path_buf() });
|
||||
}
|
||||
|
||||
pub(crate) const fn process_values(&self) -> &std::collections::BTreeMap<String, String> {
|
||||
return &self.process;
|
||||
}
|
||||
|
||||
pub(crate) const fn dotenv_values(&self) -> &std::collections::BTreeMap<String, String> {
|
||||
return &self.dotenv;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn from_maps(process: std::collections::BTreeMap<String, String>, dotenv: std::collections::BTreeMap<String, String>) -> Self {
|
||||
return Self { process, dotenv, dotenv_path: std::path::PathBuf::from(DEFAULT_DOTENV_PATH) };
|
||||
@@ -288,7 +296,7 @@ fn load_dotenv_file(path: &std::path::Path) -> ksp_core_lib::Result<std::collect
|
||||
return parse_dotenv_content(path, content.as_str());
|
||||
}
|
||||
|
||||
fn parse_dotenv_content(path: &std::path::Path, content: &str) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>> {
|
||||
pub(crate) fn parse_dotenv_content(path: &std::path::Path, content: &str) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>> {
|
||||
let mut output = std::collections::BTreeMap::<String, String>::new();
|
||||
for (line_index, raw_line) in content.lines().enumerate() {
|
||||
let raw_line = if line_index == 0 { raw_line.trim_start_matches('\u{feff}') } else { raw_line };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/error.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
/// 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");
|
||||
@@ -60,3 +60,9 @@ pub const ERROR_CODE_ENVIRONMENT_PLACEHOLDER_INVALID: ksp_core_lib::ErrorCode =
|
||||
|
||||
/// Error code used when an environment-resolved Config cannot be mapped safely to a runtime consumer contract.
|
||||
pub const ERROR_CODE_EFFECTIVE_CONFIG_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "effective_config_invalid");
|
||||
|
||||
/// Error code used when an explicit Config management operation is unsupported or targets the wrong managed resource kind.
|
||||
pub const ERROR_CODE_MANAGEMENT_OPERATION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "management_operation_invalid");
|
||||
|
||||
/// Error code used when an atomic managed Config or `.env` persistence operation fails before commit.
|
||||
pub const ERROR_CODE_PERSISTENCE_WRITE_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "persistence_write_failed");
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
// file: crates/ksp-config-lib/src/lib.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! KSP-owned application configuration facade.
|
||||
//!
|
||||
//! `0.1.3-pre.012` owns bootstrap roots, the logical file registry, generic JSON/JSON Schema loading, standard-document profile resolution, generic composite
|
||||
//! `0.1.3-pre.013` owns bootstrap roots, the logical file registry, generic JSON/JSON Schema loading, standard-document profile resolution, generic composite
|
||||
//! resolution and KSP/KSPB environment resolution through process + `.env` + fallback precedence. The standard Logging document remains the first registered
|
||||
//! runtime document. Environment-derived values preserve real/safe representations, sensitivity and provenance; the standard Logging profile can now be
|
||||
//! mapped explicitly to `ksp_logging_lib::LoggingSettings`. Persistence remains in a later bounded prerelease.
|
||||
//! mapped explicitly to `ksp_logging_lib::LoggingSettings`. Explicit management now owns typed Logging mutation, safe environment reports, privileged reveal calls and atomic JSON/`.env` persistence.
|
||||
|
||||
mod bootstrap;
|
||||
mod composite;
|
||||
@@ -17,6 +17,8 @@ mod document;
|
||||
mod environment;
|
||||
mod error;
|
||||
mod logging;
|
||||
mod management;
|
||||
mod persistence;
|
||||
mod profile;
|
||||
mod registry;
|
||||
mod sensitivity;
|
||||
@@ -83,6 +85,10 @@ pub use self::error::ERROR_CODE_FILE_MAPPING_INVALID;
|
||||
pub use self::error::ERROR_CODE_JSON_FILE_READ_FAILED;
|
||||
/// Error code used when a Config-managed file contains invalid JSON syntax.
|
||||
pub use self::error::ERROR_CODE_JSON_SYNTAX_INVALID;
|
||||
/// Error code used when an explicit management operation is unsupported or targets the wrong managed resource kind.
|
||||
pub use self::error::ERROR_CODE_MANAGEMENT_OPERATION_INVALID;
|
||||
/// Error code used when atomic managed Config or `.env` persistence fails before commit.
|
||||
pub use self::error::ERROR_CODE_PERSISTENCE_WRITE_FAILED;
|
||||
/// Error code used when an explicitly requested Config profile does not exist.
|
||||
pub use self::error::ERROR_CODE_PROFILE_NOT_FOUND;
|
||||
/// Error code used when a JSON Schema document is itself invalid.
|
||||
@@ -91,6 +97,28 @@ pub use self::error::ERROR_CODE_SCHEMA_INVALID;
|
||||
pub use self::error::ERROR_CODE_SCHEMA_VALIDATION_FAILED;
|
||||
/// Effective standard Logging configuration mapped to `ksp_logging_lib::LoggingSettings`.
|
||||
pub use self::logging::ResolvedLoggingConfig;
|
||||
/// Result of one validated Config document persistence operation.
|
||||
pub use self::management::ConfigDocumentChangeReport;
|
||||
/// Result of one persistent `.env` mutation.
|
||||
pub use self::management::ConfigEnvironmentChangeReport;
|
||||
/// Safe desired/effective/shadow view of one KSP/KSPB environment variable.
|
||||
pub use self::management::ConfigEnvironmentReport;
|
||||
/// Raw source of one registered Config document read through the explicit management surface.
|
||||
pub use self::management::ConfigManagedSource;
|
||||
/// Explicit Config management facade for source inspection and validated persistent mutations.
|
||||
pub use self::management::ConfigManagement;
|
||||
/// Typed source contract for `config/std.logging.json`.
|
||||
pub use self::management::LoggingConfigDocument;
|
||||
/// Typed source contract for the standard Logging console output.
|
||||
pub use self::management::LoggingConsoleConfig;
|
||||
/// Typed source contract for one persistent Logging file output.
|
||||
pub use self::management::LoggingFileConfig;
|
||||
/// Typed source contract for one Logging sink selector/filter.
|
||||
pub use self::management::LoggingOutputFilterConfig;
|
||||
/// Typed source contract for one profile in `std.logging.json`.
|
||||
pub use self::management::LoggingProfileConfig;
|
||||
/// Typed source contract for one global Logging target override.
|
||||
pub use self::management::LoggingTargetFilterConfig;
|
||||
/// Source that selected an effective standard Config profile.
|
||||
pub use self::profile::ConfigProfileSelectionSource;
|
||||
/// Origin of one top-level value in a resolved standard Config profile.
|
||||
|
||||
1008
crates/ksp-config-lib/src/management.rs
Normal file
1008
crates/ksp-config-lib/src/management.rs
Normal file
File diff suppressed because it is too large
Load Diff
113
crates/ksp-config-lib/src/persistence.rs
Normal file
113
crates/ksp-config-lib/src/persistence.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
// file: crates/ksp-config-lib/src/persistence.rs
|
||||
// version: 1
|
||||
|
||||
static NEXT_TEMPORARY_FILE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||
|
||||
pub(crate) fn atomic_write(path: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
|
||||
return atomic_write_with_policy(path, content, false);
|
||||
}
|
||||
|
||||
pub(crate) fn atomic_write_private(path: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
|
||||
return atomic_write_with_policy(path, content, true);
|
||||
}
|
||||
|
||||
fn atomic_write_with_policy(path: &std::path::Path, content: &[u8], private_when_new: bool) -> ksp_core_lib::Result<()> {
|
||||
let parent = match path.parent() {
|
||||
std::option::Option::Some(value) if !value.as_os_str().is_empty() => value,
|
||||
_ => std::path::Path::new("."),
|
||||
};
|
||||
let filename = match path.file_name().and_then(std::ffi::OsStr::to_str) {
|
||||
std::option::Option::Some(value) if !value.is_empty() => value,
|
||||
_ => return std::result::Result::Err(persistence_error(path, "managed Config path has no UTF-8 file name")),
|
||||
};
|
||||
let existing_permissions = destination_permissions(path);
|
||||
let existing_permissions = match existing_permissions {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let temporary_id = NEXT_TEMPORARY_FILE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let temporary_name = format!(".{filename}.ksp-tmp-{}-{temporary_id}", std::process::id());
|
||||
let temporary_path = parent.join(temporary_name);
|
||||
let opened = std::fs::OpenOptions::new().write(true).create_new(true).open(temporary_path.as_path());
|
||||
let mut file = match opened {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(persistence_io_error(path, "temporary Config file cannot be created", error)),
|
||||
};
|
||||
let permissions = apply_temporary_permissions(&file, existing_permissions, private_when_new);
|
||||
if let std::result::Result::Err(error) = permissions {
|
||||
cleanup_temporary_file(temporary_path.as_path());
|
||||
return std::result::Result::Err(persistence_io_error(path, "temporary Config file permissions cannot be applied", error));
|
||||
}
|
||||
let write = std::io::Write::write_all(&mut file, content);
|
||||
if let std::result::Result::Err(error) = write {
|
||||
cleanup_temporary_file(temporary_path.as_path());
|
||||
return std::result::Result::Err(persistence_io_error(path, "temporary Config file cannot be written", error));
|
||||
}
|
||||
let sync = file.sync_all();
|
||||
if let std::result::Result::Err(error) = sync {
|
||||
cleanup_temporary_file(temporary_path.as_path());
|
||||
return std::result::Result::Err(persistence_io_error(path, "temporary Config file cannot be synchronized", error));
|
||||
}
|
||||
drop(file);
|
||||
let rename = std::fs::rename(temporary_path.as_path(), path);
|
||||
if let std::result::Result::Err(error) = rename {
|
||||
cleanup_temporary_file(temporary_path.as_path());
|
||||
return std::result::Result::Err(persistence_io_error(path, "atomic Config file replacement failed", error));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn destination_permissions(path: &std::path::Path) -> ksp_core_lib::Result<std::option::Option<std::fs::Permissions>> {
|
||||
let metadata = std::fs::metadata(path);
|
||||
return match metadata {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(std::option::Option::Some(value.permissions())),
|
||||
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => std::result::Result::Ok(std::option::Option::None),
|
||||
std::result::Result::Err(error) => {
|
||||
std::result::Result::Err(persistence_io_error(path, "managed Config file metadata cannot be read before replacement", error))
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn apply_temporary_permissions(
|
||||
file: &std::fs::File,
|
||||
existing_permissions: std::option::Option<std::fs::Permissions>,
|
||||
private_when_new: bool,
|
||||
) -> std::io::Result<()> {
|
||||
if let std::option::Option::Some(permissions) = existing_permissions {
|
||||
return file.set_permissions(permissions);
|
||||
}
|
||||
return apply_new_file_permissions(file, private_when_new);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn apply_new_file_permissions(file: &std::fs::File, private_when_new: bool) -> std::io::Result<()> {
|
||||
if private_when_new {
|
||||
let permissions = <std::fs::Permissions as std::os::unix::fs::PermissionsExt>::from_mode(0o600);
|
||||
return file.set_permissions(permissions);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn apply_new_file_permissions(_file: &std::fs::File, _private_when_new: bool) -> std::io::Result<()> {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn cleanup_temporary_file(path: &std::path::Path) {
|
||||
let removal = std::fs::remove_file(path);
|
||||
if let std::result::Result::Err(error) = removal
|
||||
&& error.kind() != std::io::ErrorKind::NotFound
|
||||
{
|
||||
ksp_logging_lib::warn!(target: "ksp-config-lib", domain: "config.persistence", path = %path.to_string_lossy(), error = %error, "unable to cleanup temporary Config file");
|
||||
}
|
||||
}
|
||||
|
||||
fn persistence_error(path: &std::path::Path, reason: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_PERSISTENCE_WRITE_FAILED, "Config persistence failed")
|
||||
.with_context("path", path.to_string_lossy().into_owned())
|
||||
.with_context("reason", reason);
|
||||
}
|
||||
|
||||
fn persistence_io_error(path: &std::path::Path, reason: &'static str, source: std::io::Error) -> ksp_core_lib::Error {
|
||||
return persistence_error(path, reason).with_source(source);
|
||||
}
|
||||
Reference in New Issue
Block a user