1048 lines
40 KiB
Rust
1048 lines
40 KiB
Rust
// file: crates/ksp-config-lib/src/management.rs
|
|
// version: 6
|
|
|
|
/// Raw source of one registered Config document read for explicit management/correction.
|
|
#[derive(Clone, Eq, PartialEq)]
|
|
pub struct ConfigManagedSource {
|
|
file_id: crate::ConfigFileId,
|
|
path: std::path::PathBuf,
|
|
content: String,
|
|
}
|
|
|
|
impl ConfigManagedSource {
|
|
/// Returns the stable Config file identifier.
|
|
#[must_use]
|
|
pub const fn file_id(&self) -> &crate::ConfigFileId {
|
|
return &self.file_id;
|
|
}
|
|
|
|
/// Returns the resolved managed path.
|
|
#[must_use]
|
|
pub fn path(&self) -> &std::path::Path {
|
|
return self.path.as_path();
|
|
}
|
|
|
|
/// Returns the raw source text through the explicit management surface.
|
|
#[must_use]
|
|
pub fn content(&self) -> &str {
|
|
return self.content.as_str();
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for ConfigManagedSource {
|
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
return formatter.debug_struct("ConfigManagedSource").field("file_id", &self.file_id).field("path", &self.path).finish_non_exhaustive();
|
|
}
|
|
}
|
|
|
|
/// Safe desired/effective view of one KSP/KSPB environment variable for management diagnostics.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct ConfigEnvironmentReport {
|
|
variable_name: String,
|
|
sensitivity: crate::ConfigSensitivity,
|
|
desired_safe_value: std::option::Option<String>,
|
|
effective_safe_value: std::option::Option<String>,
|
|
effective_source: std::option::Option<crate::ConfigEnvironmentSource>,
|
|
shadowed_by_process_environment: bool,
|
|
}
|
|
|
|
impl ConfigEnvironmentReport {
|
|
/// Returns the environment variable name.
|
|
#[must_use]
|
|
pub fn variable_name(&self) -> &str {
|
|
return self.variable_name.as_str();
|
|
}
|
|
|
|
/// Returns the variable sensitivity derived from its namespace.
|
|
#[must_use]
|
|
pub const fn sensitivity(&self) -> crate::ConfigSensitivity {
|
|
return self.sensitivity;
|
|
}
|
|
|
|
/// Returns the persisted `.env` value in safe/redacted form when configured.
|
|
#[must_use]
|
|
pub fn desired_safe_value(&self) -> std::option::Option<&str> {
|
|
return self.desired_safe_value.as_deref();
|
|
}
|
|
|
|
/// Returns the current process-or-`.env` effective value in safe/redacted form when configured.
|
|
#[must_use]
|
|
pub fn effective_safe_value(&self) -> std::option::Option<&str> {
|
|
return self.effective_safe_value.as_deref();
|
|
}
|
|
|
|
/// Returns the source currently winning before any placeholder fallback is considered.
|
|
#[must_use]
|
|
pub const fn effective_source(&self) -> std::option::Option<crate::ConfigEnvironmentSource> {
|
|
return self.effective_source;
|
|
}
|
|
|
|
/// Returns whether a persisted `.env` value is currently shadowed by the inherited process environment.
|
|
#[must_use]
|
|
pub const fn shadowed_by_process_environment(&self) -> bool {
|
|
return self.shadowed_by_process_environment;
|
|
}
|
|
}
|
|
|
|
/// Result of one persistent `.env` mutation.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct ConfigEnvironmentChangeReport {
|
|
source_changed: bool,
|
|
effective_changed: bool,
|
|
shadowed_by_process_environment: bool,
|
|
reload_required: bool,
|
|
}
|
|
|
|
impl ConfigEnvironmentChangeReport {
|
|
/// Returns whether the persisted `.env` source changed.
|
|
#[must_use]
|
|
pub const fn source_changed(&self) -> bool {
|
|
return self.source_changed;
|
|
}
|
|
|
|
/// Returns whether the effective process-or-`.env` value changed for the current process.
|
|
#[must_use]
|
|
pub const fn effective_changed(&self) -> bool {
|
|
return self.effective_changed;
|
|
}
|
|
|
|
/// Returns whether the new persisted `.env` state is shadowed by the inherited process environment.
|
|
#[must_use]
|
|
pub const fn shadowed_by_process_environment(&self) -> bool {
|
|
return self.shadowed_by_process_environment;
|
|
}
|
|
|
|
/// Returns whether Config consumers must reload their environment snapshot to observe the effective change.
|
|
#[must_use]
|
|
pub const fn reload_required(&self) -> bool {
|
|
return self.reload_required;
|
|
}
|
|
}
|
|
|
|
/// Result of one validated Config document persistence operation.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct ConfigDocumentChangeReport {
|
|
source_changed: bool,
|
|
reload_required: bool,
|
|
}
|
|
|
|
impl ConfigDocumentChangeReport {
|
|
/// Returns whether the persisted document bytes changed.
|
|
#[must_use]
|
|
pub const fn source_changed(&self) -> bool {
|
|
return self.source_changed;
|
|
}
|
|
|
|
/// Returns whether consumers must reload the Config document to observe the change.
|
|
#[must_use]
|
|
pub const fn reload_required(&self) -> bool {
|
|
return self.reload_required;
|
|
}
|
|
}
|
|
|
|
/// Typed source contract for `config/std.logging.json`.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct LoggingConfigDocument {
|
|
format_version: u32,
|
|
logs_directory: String,
|
|
default_profile: String,
|
|
profiles: std::vec::Vec<LoggingProfileConfig>,
|
|
}
|
|
|
|
impl LoggingConfigDocument {
|
|
/// Creates a version-1 Logging Config document candidate.
|
|
#[must_use]
|
|
pub fn new(
|
|
logs_directory: impl std::convert::Into<String>,
|
|
default_profile: impl std::convert::Into<String>,
|
|
profiles: std::vec::Vec<LoggingProfileConfig>,
|
|
) -> Self {
|
|
return Self {
|
|
format_version: 1,
|
|
logs_directory: logs_directory.into(),
|
|
default_profile: default_profile.into(),
|
|
profiles,
|
|
};
|
|
}
|
|
|
|
/// Returns the source format version.
|
|
#[must_use]
|
|
pub const fn format_version(&self) -> u32 {
|
|
return self.format_version;
|
|
}
|
|
|
|
/// Returns the source Logging root expression/path.
|
|
#[must_use]
|
|
pub fn logs_directory(&self) -> &str {
|
|
return self.logs_directory.as_str();
|
|
}
|
|
|
|
/// Replaces the source Logging root expression/path.
|
|
pub fn set_logs_directory(&mut self, value: impl std::convert::Into<String>) {
|
|
self.logs_directory = value.into();
|
|
}
|
|
|
|
/// Returns the autonomous default profile identifier.
|
|
#[must_use]
|
|
pub fn default_profile(&self) -> &str {
|
|
return self.default_profile.as_str();
|
|
}
|
|
|
|
/// Replaces the autonomous default profile identifier.
|
|
pub fn set_default_profile(&mut self, value: impl std::convert::Into<String>) {
|
|
self.default_profile = value.into();
|
|
}
|
|
|
|
/// Returns the source Logging profiles.
|
|
#[must_use]
|
|
pub fn profiles(&self) -> &[LoggingProfileConfig] {
|
|
return self.profiles.as_slice();
|
|
}
|
|
|
|
/// Returns mutable source Logging profiles; persistence validates the complete candidate before commit.
|
|
pub fn profiles_mut(&mut self) -> &mut std::vec::Vec<LoggingProfileConfig> {
|
|
return &mut self.profiles;
|
|
}
|
|
}
|
|
|
|
/// Typed source contract for one profile in `std.logging.json`.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct LoggingProfileConfig {
|
|
profile_id: String,
|
|
default_filter: String,
|
|
span_events: String,
|
|
console: LoggingConsoleConfig,
|
|
files: std::vec::Vec<LoggingFileConfig>,
|
|
target_filters: std::vec::Vec<LoggingTargetFilterConfig>,
|
|
}
|
|
|
|
impl LoggingProfileConfig {
|
|
/// Creates one Logging profile candidate.
|
|
#[must_use]
|
|
pub fn new(
|
|
profile_id: impl std::convert::Into<String>,
|
|
default_filter: impl std::convert::Into<String>,
|
|
span_events: impl std::convert::Into<String>,
|
|
console: LoggingConsoleConfig,
|
|
files: std::vec::Vec<LoggingFileConfig>,
|
|
target_filters: std::vec::Vec<LoggingTargetFilterConfig>,
|
|
) -> Self {
|
|
return Self {
|
|
profile_id: profile_id.into(),
|
|
default_filter: default_filter.into(),
|
|
span_events: span_events.into(),
|
|
console,
|
|
files,
|
|
target_filters,
|
|
};
|
|
}
|
|
|
|
/// Returns the stable profile identifier.
|
|
#[must_use]
|
|
pub fn profile_id(&self) -> &str {
|
|
return self.profile_id.as_str();
|
|
}
|
|
|
|
/// Replaces the profile identifier.
|
|
pub fn set_profile_id(&mut self, value: impl std::convert::Into<String>) {
|
|
self.profile_id = value.into();
|
|
}
|
|
|
|
/// Returns the profile default filter text.
|
|
#[must_use]
|
|
pub fn default_filter(&self) -> &str {
|
|
return self.default_filter.as_str();
|
|
}
|
|
|
|
/// Replaces the profile default filter text.
|
|
pub fn set_default_filter(&mut self, value: impl std::convert::Into<String>) {
|
|
self.default_filter = value.into();
|
|
}
|
|
|
|
/// Returns the span lifecycle setting text.
|
|
#[must_use]
|
|
pub fn span_events(&self) -> &str {
|
|
return self.span_events.as_str();
|
|
}
|
|
|
|
/// Replaces the span lifecycle setting text.
|
|
pub fn set_span_events(&mut self, value: impl std::convert::Into<String>) {
|
|
self.span_events = value.into();
|
|
}
|
|
|
|
/// Returns the console source configuration.
|
|
#[must_use]
|
|
pub const fn console(&self) -> &LoggingConsoleConfig {
|
|
return &self.console;
|
|
}
|
|
|
|
/// Returns mutable console source configuration.
|
|
pub fn console_mut(&mut self) -> &mut LoggingConsoleConfig {
|
|
return &mut self.console;
|
|
}
|
|
|
|
/// Returns the persistent file source configurations.
|
|
#[must_use]
|
|
pub fn files(&self) -> &[LoggingFileConfig] {
|
|
return self.files.as_slice();
|
|
}
|
|
|
|
/// Returns mutable persistent file source configurations.
|
|
pub fn files_mut(&mut self) -> &mut std::vec::Vec<LoggingFileConfig> {
|
|
return &mut self.files;
|
|
}
|
|
|
|
/// Returns the global target overrides.
|
|
#[must_use]
|
|
pub fn target_filters(&self) -> &[LoggingTargetFilterConfig] {
|
|
return self.target_filters.as_slice();
|
|
}
|
|
|
|
/// Returns mutable global target overrides.
|
|
pub fn target_filters_mut(&mut self) -> &mut std::vec::Vec<LoggingTargetFilterConfig> {
|
|
return &mut self.target_filters;
|
|
}
|
|
}
|
|
|
|
/// Typed source contract for the Logging console output.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct LoggingConsoleConfig {
|
|
enabled: bool,
|
|
output: String,
|
|
ansi: bool,
|
|
format: String,
|
|
filter: LoggingOutputFilterConfig,
|
|
}
|
|
|
|
impl LoggingConsoleConfig {
|
|
/// Creates a console source candidate.
|
|
#[must_use]
|
|
pub fn new(
|
|
enabled: bool,
|
|
output: impl std::convert::Into<String>,
|
|
ansi: bool,
|
|
format: impl std::convert::Into<String>,
|
|
filter: LoggingOutputFilterConfig,
|
|
) -> Self {
|
|
return Self { enabled, output: output.into(), ansi, format: format.into(), filter };
|
|
}
|
|
|
|
/// Returns whether the console output is enabled.
|
|
#[must_use]
|
|
pub const fn enabled(&self) -> bool {
|
|
return self.enabled;
|
|
}
|
|
|
|
/// Sets whether the console output is enabled.
|
|
pub fn set_enabled(&mut self, value: bool) {
|
|
self.enabled = value;
|
|
}
|
|
|
|
/// Returns `stdout` or `stderr` source text.
|
|
#[must_use]
|
|
pub fn output(&self) -> &str {
|
|
return self.output.as_str();
|
|
}
|
|
|
|
/// Replaces the console output source text.
|
|
pub fn set_output(&mut self, value: impl std::convert::Into<String>) {
|
|
self.output = value.into();
|
|
}
|
|
|
|
/// Returns whether ANSI output is requested.
|
|
#[must_use]
|
|
pub const fn ansi(&self) -> bool {
|
|
return self.ansi;
|
|
}
|
|
|
|
/// Sets whether ANSI output is requested.
|
|
pub fn set_ansi(&mut self, value: bool) {
|
|
self.ansi = value;
|
|
}
|
|
|
|
/// Returns the console format source text.
|
|
#[must_use]
|
|
pub fn format(&self) -> &str {
|
|
return self.format.as_str();
|
|
}
|
|
|
|
/// Replaces the console format source text.
|
|
pub fn set_format(&mut self, value: impl std::convert::Into<String>) {
|
|
self.format = value.into();
|
|
}
|
|
|
|
/// Returns the console output filter.
|
|
#[must_use]
|
|
pub const fn filter(&self) -> &LoggingOutputFilterConfig {
|
|
return &self.filter;
|
|
}
|
|
|
|
/// Returns the mutable console output filter.
|
|
pub fn filter_mut(&mut self) -> &mut LoggingOutputFilterConfig {
|
|
return &mut self.filter;
|
|
}
|
|
}
|
|
|
|
/// Typed source contract for one persistent Logging file output.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct LoggingFileConfig {
|
|
output_id: String,
|
|
enabled: bool,
|
|
path: String,
|
|
rotation: String,
|
|
format: String,
|
|
ansi: bool,
|
|
filter: LoggingOutputFilterConfig,
|
|
}
|
|
|
|
impl LoggingFileConfig {
|
|
/// Creates one persistent Logging source candidate. Persistent ANSI defaults to `false` as required by the schema.
|
|
#[must_use]
|
|
pub fn new(
|
|
output_id: impl std::convert::Into<String>,
|
|
enabled: bool,
|
|
path: impl std::convert::Into<String>,
|
|
rotation: impl std::convert::Into<String>,
|
|
format: impl std::convert::Into<String>,
|
|
filter: LoggingOutputFilterConfig,
|
|
) -> Self {
|
|
return Self {
|
|
output_id: output_id.into(),
|
|
enabled,
|
|
path: path.into(),
|
|
rotation: rotation.into(),
|
|
format: format.into(),
|
|
ansi: false,
|
|
filter,
|
|
};
|
|
}
|
|
|
|
/// Returns the stable sink identifier.
|
|
#[must_use]
|
|
pub fn output_id(&self) -> &str {
|
|
return self.output_id.as_str();
|
|
}
|
|
|
|
/// Replaces the sink identifier.
|
|
pub fn set_output_id(&mut self, value: impl std::convert::Into<String>) {
|
|
self.output_id = value.into();
|
|
}
|
|
|
|
/// Returns whether the sink is enabled.
|
|
#[must_use]
|
|
pub const fn enabled(&self) -> bool {
|
|
return self.enabled;
|
|
}
|
|
|
|
/// Sets whether the sink is enabled.
|
|
pub fn set_enabled(&mut self, value: bool) {
|
|
self.enabled = value;
|
|
}
|
|
|
|
/// Returns the source path relative to `logs_directory`.
|
|
#[must_use]
|
|
pub fn path(&self) -> &str {
|
|
return self.path.as_str();
|
|
}
|
|
|
|
/// Replaces the source path relative to `logs_directory`.
|
|
pub fn set_path(&mut self, value: impl std::convert::Into<String>) {
|
|
self.path = value.into();
|
|
}
|
|
|
|
/// Returns the rotation source text.
|
|
#[must_use]
|
|
pub fn rotation(&self) -> &str {
|
|
return self.rotation.as_str();
|
|
}
|
|
|
|
/// Replaces the rotation source text.
|
|
pub fn set_rotation(&mut self, value: impl std::convert::Into<String>) {
|
|
self.rotation = value.into();
|
|
}
|
|
|
|
/// Returns the format source text.
|
|
#[must_use]
|
|
pub fn format(&self) -> &str {
|
|
return self.format.as_str();
|
|
}
|
|
|
|
/// Replaces the format source text.
|
|
pub fn set_format(&mut self, value: impl std::convert::Into<String>) {
|
|
self.format = value.into();
|
|
}
|
|
|
|
/// Returns whether ANSI output is requested.
|
|
#[must_use]
|
|
pub const fn ansi(&self) -> bool {
|
|
return self.ansi;
|
|
}
|
|
|
|
/// Sets whether ANSI output is requested. Persistence validation currently requires persistent file ANSI to remain disabled.
|
|
pub fn set_ansi(&mut self, value: bool) {
|
|
self.ansi = value;
|
|
}
|
|
|
|
/// Returns the output filter.
|
|
#[must_use]
|
|
pub const fn filter(&self) -> &LoggingOutputFilterConfig {
|
|
return &self.filter;
|
|
}
|
|
|
|
/// Returns the mutable output filter.
|
|
pub fn filter_mut(&mut self) -> &mut LoggingOutputFilterConfig {
|
|
return &mut self.filter;
|
|
}
|
|
}
|
|
|
|
/// Typed source contract for one Logging sink selector/filter.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct LoggingOutputFilterConfig {
|
|
level: String,
|
|
targets: std::vec::Vec<String>,
|
|
domains: std::vec::Vec<String>,
|
|
}
|
|
|
|
impl LoggingOutputFilterConfig {
|
|
/// Creates one Logging output filter candidate.
|
|
#[must_use]
|
|
pub fn new(level: impl std::convert::Into<String>, targets: std::vec::Vec<String>, domains: std::vec::Vec<String>) -> Self {
|
|
return Self { level: level.into(), targets, domains };
|
|
}
|
|
|
|
/// Returns the filter level source text.
|
|
#[must_use]
|
|
pub fn level(&self) -> &str {
|
|
return self.level.as_str();
|
|
}
|
|
|
|
/// Replaces the filter level source text.
|
|
pub fn set_level(&mut self, value: impl std::convert::Into<String>) {
|
|
self.level = value.into();
|
|
}
|
|
|
|
/// Returns target selectors.
|
|
#[must_use]
|
|
pub fn targets(&self) -> &[String] {
|
|
return self.targets.as_slice();
|
|
}
|
|
|
|
/// Returns mutable target selectors.
|
|
pub fn targets_mut(&mut self) -> &mut std::vec::Vec<String> {
|
|
return &mut self.targets;
|
|
}
|
|
|
|
/// Returns structured domain selectors.
|
|
#[must_use]
|
|
pub fn domains(&self) -> &[String] {
|
|
return self.domains.as_slice();
|
|
}
|
|
|
|
/// Returns mutable structured domain selectors.
|
|
pub fn domains_mut(&mut self) -> &mut std::vec::Vec<String> {
|
|
return &mut self.domains;
|
|
}
|
|
}
|
|
|
|
/// Typed source contract for one global Logging target override.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct LoggingTargetFilterConfig {
|
|
target_prefix: String,
|
|
level: String,
|
|
}
|
|
|
|
impl LoggingTargetFilterConfig {
|
|
/// Creates one global target override candidate.
|
|
#[must_use]
|
|
pub fn new(target_prefix: impl std::convert::Into<String>, level: impl std::convert::Into<String>) -> Self {
|
|
return Self { target_prefix: target_prefix.into(), level: level.into() };
|
|
}
|
|
|
|
/// Returns the KSP target prefix.
|
|
#[must_use]
|
|
pub fn target_prefix(&self) -> &str {
|
|
return self.target_prefix.as_str();
|
|
}
|
|
|
|
/// Replaces the KSP target prefix.
|
|
pub fn set_target_prefix(&mut self, value: impl std::convert::Into<String>) {
|
|
self.target_prefix = value.into();
|
|
}
|
|
|
|
/// Returns the override level source text.
|
|
#[must_use]
|
|
pub fn level(&self) -> &str {
|
|
return self.level.as_str();
|
|
}
|
|
|
|
/// Replaces the override level source text.
|
|
pub fn set_level(&mut self, value: impl std::convert::Into<String>) {
|
|
self.level = value.into();
|
|
}
|
|
}
|
|
|
|
/// Explicit Config management facade for source inspection and validated persistent mutations.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct ConfigManagement {
|
|
engine: crate::ConfigDocumentEngine,
|
|
dotenv_path: std::path::PathBuf,
|
|
}
|
|
|
|
impl ConfigManagement {
|
|
/// Creates a management facade using the conventional process-launch-root `./.env`.
|
|
#[must_use]
|
|
pub fn new(engine: crate::ConfigDocumentEngine) -> Self {
|
|
return Self { engine, dotenv_path: std::path::PathBuf::from(crate::DEFAULT_DOTENV_PATH) };
|
|
}
|
|
|
|
/// Returns the Config document engine used by this management facade.
|
|
#[must_use]
|
|
pub const fn engine(&self) -> &crate::ConfigDocumentEngine {
|
|
return &self.engine;
|
|
}
|
|
|
|
/// Reads raw source text for one registered Config document without requiring schema validity.
|
|
///
|
|
/// This explicit management-only read allows a future editor to inspect/correct an invalid document while still preventing arbitrary filesystem paths.
|
|
pub fn read_source(&self, file_id: &crate::ConfigFileId) -> ksp_core_lib::Result<ConfigManagedSource> {
|
|
let descriptor = self.engine.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(management_error("management source read requires a Config document").with_context("file_id", file_id.as_str()));
|
|
}
|
|
let path = self.engine.registry().resolve_path(self.engine.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());
|
|
return match content {
|
|
std::result::Result::Ok(content) => std::result::Result::Ok(ConfigManagedSource { file_id: file_id.clone(), path, content }),
|
|
std::result::Result::Err(error) => std::result::Result::Err(
|
|
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(error),
|
|
),
|
|
};
|
|
}
|
|
|
|
/// Validates and atomically persists a raw source candidate for one registered Config document.
|
|
///
|
|
/// The candidate is parsed, schema-validated and checked against KSP semantic invariants before any destination bytes are replaced. The raw source text is
|
|
/// preserved exactly when persistence succeeds, allowing a management editor to repair an invalid document without bypassing Config ownership.
|
|
pub fn save_source_candidate(&self, file_id: &crate::ConfigFileId, source: &str) -> ksp_core_lib::Result<ConfigDocumentChangeReport> {
|
|
let descriptor = self.engine.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(
|
|
management_error("management source persistence requires a Config document").with_context("file_id", file_id.as_str()),
|
|
);
|
|
}
|
|
let validated = self.engine.validate_source_candidate(file_id, source);
|
|
let validated = match validated {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return persist_document_source(file_id, validated.path(), source.as_bytes());
|
|
}
|
|
|
|
/// Loads the validated `std.logging.json` source into its typed management contract.
|
|
pub fn load_logging_document(&self) -> ksp_core_lib::Result<LoggingConfigDocument> {
|
|
let file_id = logging_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 document = self.engine.load_validated_document(&file_id);
|
|
let document = match document {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let parsed = serde_json::from_value::<LoggingConfigDocument>(document.value().clone());
|
|
return match parsed {
|
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
|
std::result::Result::Err(error) => {
|
|
std::result::Result::Err(management_error("validated Logging source cannot be decoded into management contract").with_source(error))
|
|
},
|
|
};
|
|
}
|
|
|
|
/// Validates and atomically persists one typed `std.logging.json` candidate.
|
|
///
|
|
/// Validation occurs fully before the destination is replaced. The persisted JSON is pretty-printed and terminated by one newline.
|
|
pub fn save_logging_document(&self, candidate: &LoggingConfigDocument) -> ksp_core_lib::Result<ConfigDocumentChangeReport> {
|
|
let file_id = logging_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 value = serde_json::to_value(candidate);
|
|
let value = match value {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(management_error("Logging management candidate cannot be serialized").with_source(error));
|
|
},
|
|
};
|
|
let validated = self.engine.validate_candidate(&file_id, value);
|
|
let validated = match validated {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let serialized = serde_json::to_string_pretty(validated.value());
|
|
let mut serialized = match serialized {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(management_error("validated Logging candidate cannot be serialized").with_source(error));
|
|
},
|
|
};
|
|
serialized.push('\n');
|
|
return persist_document_source(&file_id, validated.path(), serialized.as_bytes());
|
|
}
|
|
|
|
/// Returns safe desired/effective/shadow reports for every KSP/KSPB variable present in process or `.env` sources.
|
|
pub fn environment_report(&self) -> ksp_core_lib::Result<std::vec::Vec<ConfigEnvironmentReport>> {
|
|
let environment = self.load_environment();
|
|
let environment = match environment {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return build_environment_reports(&environment);
|
|
}
|
|
|
|
/// Explicitly reveals the real effective process-or-`.env` value for management display/editing.
|
|
///
|
|
/// Authentication/authorization of the human user belongs to the calling application. Calling this method is the explicit Config boundary that opts into
|
|
/// real-value access; the returned value must never be logged.
|
|
pub fn reveal_effective_environment_value(&self, variable_name: &str) -> ksp_core_lib::Result<std::option::Option<String>> {
|
|
let validation = crate::validate_supported_variable_name(variable_name);
|
|
if let std::result::Result::Err(error) = validation {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
let environment = self.load_environment();
|
|
let environment = match environment {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if let std::option::Option::Some(value) = environment.process_values().get(variable_name) {
|
|
return std::result::Result::Ok(std::option::Option::Some(value.clone()));
|
|
}
|
|
return std::result::Result::Ok(environment.dotenv_values().get(variable_name).cloned());
|
|
}
|
|
|
|
/// Explicitly reveals the real persisted `.env` value for management display/editing.
|
|
pub fn reveal_dotenv_value(&self, variable_name: &str) -> ksp_core_lib::Result<std::option::Option<String>> {
|
|
let validation = crate::validate_supported_variable_name(variable_name);
|
|
if let std::result::Result::Err(error) = validation {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
let environment = self.load_environment();
|
|
let environment = match environment {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return std::result::Result::Ok(environment.dotenv_values().get(variable_name).cloned());
|
|
}
|
|
|
|
/// Creates or updates one supported KSP/KSPB `.env` entry atomically without mutating the inherited process environment.
|
|
pub fn set_dotenv_value(&self, variable_name: &str, value: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeReport> {
|
|
let validation = crate::validate_supported_variable_name(variable_name);
|
|
if let std::result::Result::Err(error) = validation {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
let before = self.load_environment();
|
|
let before = match before {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if before.dotenv_values().get(variable_name).map(String::as_str) == std::option::Option::Some(value) {
|
|
return std::result::Result::Ok(environment_change_report(variable_name, &before, &before));
|
|
}
|
|
let content = read_dotenv_source(self.dotenv_path.as_path());
|
|
let content = match content {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let candidate = update_dotenv_source(self.dotenv_path.as_path(), content.as_str(), variable_name, std::option::Option::Some(value));
|
|
let candidate = match candidate {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if candidate != content {
|
|
let write = crate::atomic_write_private(self.dotenv_path.as_path(), candidate.as_bytes());
|
|
match write {
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
}
|
|
}
|
|
let after = self.load_environment();
|
|
let after = match after {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return std::result::Result::Ok(environment_change_report(variable_name, &before, &after));
|
|
}
|
|
|
|
/// Removes one supported KSP/KSPB `.env` entry atomically without mutating the inherited process environment.
|
|
pub fn remove_dotenv_value(&self, variable_name: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeReport> {
|
|
let validation = crate::validate_supported_variable_name(variable_name);
|
|
if let std::result::Result::Err(error) = validation {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
let before = self.load_environment();
|
|
let before = match before {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if !before.dotenv_values().contains_key(variable_name) {
|
|
return std::result::Result::Ok(environment_change_report(variable_name, &before, &before));
|
|
}
|
|
let content = read_dotenv_source(self.dotenv_path.as_path());
|
|
let content = match content {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let candidate = update_dotenv_source(self.dotenv_path.as_path(), content.as_str(), variable_name, std::option::Option::None);
|
|
let candidate = match candidate {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if candidate != content {
|
|
let write = crate::atomic_write_private(self.dotenv_path.as_path(), candidate.as_bytes());
|
|
match write {
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
}
|
|
}
|
|
let after = self.load_environment();
|
|
let after = match after {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return std::result::Result::Ok(environment_change_report(variable_name, &before, &after));
|
|
}
|
|
|
|
fn load_environment(&self) -> ksp_core_lib::Result<crate::ConfigEnvironment> {
|
|
return crate::ConfigEnvironment::load_from_dotenv_path(self.dotenv_path.as_path());
|
|
}
|
|
|
|
/// Executes the crate-internal with dotenv path operation for `ConfigManagement`.
|
|
#[cfg(test)]
|
|
pub(crate) fn with_dotenv_path(engine: crate::ConfigDocumentEngine, dotenv_path: std::path::PathBuf) -> Self {
|
|
return Self { engine, dotenv_path };
|
|
}
|
|
}
|
|
|
|
fn logging_file_id() -> ksp_core_lib::Result<crate::ConfigFileId> {
|
|
return crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
|
|
}
|
|
|
|
fn build_environment_reports(environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result<std::vec::Vec<ConfigEnvironmentReport>> {
|
|
let mut names = std::collections::BTreeSet::<String>::new();
|
|
for name in environment.process_values().keys() {
|
|
names.insert(name.clone());
|
|
}
|
|
for name in environment.dotenv_values().keys() {
|
|
names.insert(name.clone());
|
|
}
|
|
let mut reports = std::vec::Vec::<ConfigEnvironmentReport>::with_capacity(names.len());
|
|
for variable_name in names {
|
|
let sensitivity = crate::ConfigSensitivity::from_variable_name(variable_name.as_str());
|
|
let sensitivity = match sensitivity {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let desired = environment.dotenv_values().get(variable_name.as_str());
|
|
let process = environment.process_values().get(variable_name.as_str());
|
|
let (effective, effective_source) = match process {
|
|
std::option::Option::Some(value) => (std::option::Option::Some(value), std::option::Option::Some(crate::ConfigEnvironmentSource::Process)),
|
|
std::option::Option::None => match desired {
|
|
std::option::Option::Some(value) => (std::option::Option::Some(value), std::option::Option::Some(crate::ConfigEnvironmentSource::DotEnv)),
|
|
std::option::Option::None => (std::option::Option::None, std::option::Option::None),
|
|
},
|
|
};
|
|
reports.push(ConfigEnvironmentReport {
|
|
variable_name,
|
|
sensitivity,
|
|
desired_safe_value: desired.map(|value| -> String {
|
|
return safe_environment_text(sensitivity, value.as_str());
|
|
}),
|
|
effective_safe_value: effective.map(|value| -> String {
|
|
return safe_environment_text(sensitivity, value.as_str());
|
|
}),
|
|
effective_source,
|
|
shadowed_by_process_environment: process.is_some() && desired.is_some(),
|
|
});
|
|
}
|
|
return std::result::Result::Ok(reports);
|
|
}
|
|
|
|
fn safe_environment_text(sensitivity: crate::ConfigSensitivity, value: &str) -> String {
|
|
if sensitivity.is_secret() {
|
|
return crate::REDACTED_CONFIG_VALUE.to_owned();
|
|
}
|
|
return value.to_owned();
|
|
}
|
|
|
|
fn environment_change_report(variable_name: &str, before: &crate::ConfigEnvironment, after: &crate::ConfigEnvironment) -> ConfigEnvironmentChangeReport {
|
|
let before_desired = before.dotenv_values().get(variable_name);
|
|
let after_desired = after.dotenv_values().get(variable_name);
|
|
let before_effective = effective_environment_value(before, variable_name);
|
|
let after_effective = effective_environment_value(after, variable_name);
|
|
let source_changed = before_desired != after_desired;
|
|
let effective_changed = before_effective != after_effective;
|
|
return ConfigEnvironmentChangeReport {
|
|
source_changed,
|
|
effective_changed,
|
|
shadowed_by_process_environment: after.process_values().contains_key(variable_name) && after.dotenv_values().contains_key(variable_name),
|
|
reload_required: effective_changed,
|
|
};
|
|
}
|
|
|
|
fn effective_environment_value<'a>(environment: &'a crate::ConfigEnvironment, variable_name: &str) -> std::option::Option<&'a String> {
|
|
if let std::option::Option::Some(value) = environment.process_values().get(variable_name) {
|
|
return std::option::Option::Some(value);
|
|
}
|
|
return environment.dotenv_values().get(variable_name);
|
|
}
|
|
|
|
fn read_dotenv_source(path: &std::path::Path) -> ksp_core_lib::Result<String> {
|
|
let content = std::fs::read_to_string(path);
|
|
return match content {
|
|
std::result::Result::Ok(value) => {
|
|
let validation = crate::parse_dotenv_content(path, value.as_str());
|
|
match validation {
|
|
std::result::Result::Ok(_) => std::result::Result::Ok(value),
|
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
|
}
|
|
},
|
|
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => std::result::Result::Ok(String::new()),
|
|
std::result::Result::Err(error) => std::result::Result::Err(
|
|
ksp_core_lib::Error::new(crate::ERROR_CODE_DOTENV_FILE_READ_FAILED, "Config cannot read the local .env file")
|
|
.with_context("path", path.to_string_lossy().into_owned())
|
|
.with_source(error),
|
|
),
|
|
};
|
|
}
|
|
|
|
fn update_dotenv_source(path: &std::path::Path, source: &str, variable_name: &str, replacement: std::option::Option<&str>) -> ksp_core_lib::Result<String> {
|
|
let validation = crate::parse_dotenv_content(path, source);
|
|
if let std::result::Result::Err(error) = validation {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
let mut lines = source.lines().map(str::to_owned).collect::<std::vec::Vec<String>>();
|
|
let mut match_index: std::option::Option<usize> = std::option::Option::None;
|
|
for (index, line) in lines.iter().enumerate() {
|
|
if dotenv_assignment_name(line.as_str()) == std::option::Option::Some(variable_name) {
|
|
match_index = std::option::Option::Some(index);
|
|
break;
|
|
}
|
|
}
|
|
match replacement {
|
|
std::option::Option::Some(value) => {
|
|
let assignment = format!("{variable_name}={}", encode_dotenv_value(value));
|
|
match match_index {
|
|
std::option::Option::Some(index) => lines[index] = assignment,
|
|
std::option::Option::None => lines.push(assignment),
|
|
}
|
|
},
|
|
std::option::Option::None => {
|
|
if let std::option::Option::Some(index) = match_index {
|
|
lines.remove(index);
|
|
}
|
|
},
|
|
}
|
|
let mut candidate = lines.join("\n");
|
|
if !candidate.is_empty() {
|
|
candidate.push('\n');
|
|
}
|
|
let candidate_validation = crate::parse_dotenv_content(path, candidate.as_str());
|
|
return match candidate_validation {
|
|
std::result::Result::Ok(_) => std::result::Result::Ok(candidate),
|
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
|
};
|
|
}
|
|
|
|
fn dotenv_assignment_name(line: &str) -> std::option::Option<&str> {
|
|
let line = line.trim();
|
|
if line.is_empty() || line.starts_with('#') {
|
|
return std::option::Option::None;
|
|
}
|
|
let assignment = match line.strip_prefix("export ") {
|
|
std::option::Option::Some(value) => value.trim_start(),
|
|
std::option::Option::None => line,
|
|
};
|
|
let separator = assignment.find('=');
|
|
let separator = match separator {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
return std::option::Option::Some(assignment[..separator].trim());
|
|
}
|
|
|
|
fn encode_dotenv_value(value: &str) -> String {
|
|
let simple = value.chars().all(|character| -> bool {
|
|
return character.is_ascii_alphanumeric() || matches!(character, '_' | '-' | '.' | '/' | ':' | '@' | '+');
|
|
});
|
|
if simple {
|
|
return value.to_owned();
|
|
}
|
|
let mut encoded = String::from("\"");
|
|
for character in value.chars() {
|
|
match character {
|
|
'\\' => encoded.push_str("\\\\"),
|
|
'"' => encoded.push_str("\\\""),
|
|
'\n' => encoded.push_str("\\n"),
|
|
'\r' => encoded.push_str("\\r"),
|
|
'\t' => encoded.push_str("\\t"),
|
|
_ => encoded.push(character),
|
|
}
|
|
}
|
|
encoded.push('"');
|
|
return encoded;
|
|
}
|
|
|
|
fn persist_document_source(file_id: &crate::ConfigFileId, path: &std::path::Path, source: &[u8]) -> ksp_core_lib::Result<ConfigDocumentChangeReport> {
|
|
let existing = std::fs::read(path);
|
|
let existing = match existing {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => std::vec::Vec::new(),
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(
|
|
ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_FILE_READ_FAILED, "existing Config document cannot be read before persistence")
|
|
.with_context("file_id", file_id.as_str())
|
|
.with_context("path", path.to_string_lossy().into_owned())
|
|
.with_source(error),
|
|
);
|
|
},
|
|
};
|
|
if existing.as_slice() == source {
|
|
return std::result::Result::Ok(ConfigDocumentChangeReport { source_changed: false, reload_required: false });
|
|
}
|
|
let write = crate::atomic_write(path, source);
|
|
if let std::result::Result::Err(error) = write {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
return std::result::Result::Ok(ConfigDocumentChangeReport { source_changed: true, reload_required: true });
|
|
}
|
|
|
|
fn management_error(reason: &'static str) -> ksp_core_lib::Error {
|
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_MANAGEMENT_OPERATION_INVALID, "Config management operation is invalid").with_context("reason", reason);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "../unit_tests/management.rs"]
|
|
mod tests;
|