v0.1.4-pre.003

This commit is contained in:
2026-08-16 10:21:15 +02:00
parent 4a68a54d85
commit fa6d4e1039
9 changed files with 517 additions and 35 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/document.rs
// version: 4
// version: 5
/// A Config-managed JSON document that has passed syntax, schema and current semantic validation.
#[derive(Clone, Debug, PartialEq)]
@@ -85,6 +85,20 @@ impl ConfigDocumentEngine {
return self.validate_document(document, &schema_file_id);
}
pub(crate) fn validate_source_candidate(&self, file_id: &crate::ConfigFileId, source: &str) -> ksp_core_lib::Result<ConfigJsonDocument> {
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 value = serde_json::from_str::<serde_json::Value>(source);
let value = match value {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(json_syntax_error(file_id, path.as_path(), error)),
};
return self.validate_candidate(file_id, value);
}
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 {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/management.rs
// version: 1
// version: 2
/// Raw source of one registered Config document read for explicit management/correction.
#[derive(Clone, Eq, PartialEq)]
@@ -625,6 +625,29 @@ impl ConfigManagement {
};
}
/// 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();
@@ -675,27 +698,7 @@ impl ConfigManagement {
},
};
serialized.push('\n');
let existing = std::fs::read(validated.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", validated.path().to_string_lossy().into_owned())
.with_source(error),
);
},
};
if existing.as_slice() == serialized.as_bytes() {
return std::result::Result::Ok(ConfigDocumentChangeReport { source_changed: false, reload_required: false });
}
let write = crate::persistence::atomic_write(validated.path(), serialized.as_bytes());
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 });
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.
@@ -999,6 +1002,30 @@ fn encode_dotenv_value(value: &str) -> String {
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::persistence::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);
}