// 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, effective_safe_value: std::option::Option, effective_source: std::option::Option, 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 { 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, } impl LoggingConfigDocument { /// Creates a version-1 Logging Config document candidate. #[must_use] pub fn new( logs_directory: impl std::convert::Into, default_profile: impl std::convert::Into, profiles: std::vec::Vec, ) -> 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) { 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) { 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 { 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, target_filters: std::vec::Vec, } impl LoggingProfileConfig { /// Creates one Logging profile candidate. #[must_use] pub fn new( profile_id: impl std::convert::Into, default_filter: impl std::convert::Into, span_events: impl std::convert::Into, console: LoggingConsoleConfig, files: std::vec::Vec, target_filters: std::vec::Vec, ) -> 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) { 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) { 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) { 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 { 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 { 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, ansi: bool, format: impl std::convert::Into, 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) { 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) { 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, enabled: bool, path: impl std::convert::Into, rotation: impl std::convert::Into, format: impl std::convert::Into, 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) { 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) { 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) { 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) { 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, domains: std::vec::Vec, } impl LoggingOutputFilterConfig { /// Creates one Logging output filter candidate. #[must_use] pub fn new(level: impl std::convert::Into, targets: std::vec::Vec, domains: std::vec::Vec) -> 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) { 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 { 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 { 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, level: impl std::convert::Into) -> 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) { 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) { 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 { 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 { 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 { 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::(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 { 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> { 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> { 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> { 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 { 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 { 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 { 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 { return crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING); } fn build_environment_reports(environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result> { let mut names = std::collections::BTreeSet::::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::::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 { 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 { 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::>(); let mut match_index: std::option::Option = 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 { 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;