v0.1.4-pre.009
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/src/app_state.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Shared backend state owned by the Tauri application.
|
||||
|
||||
@@ -104,6 +104,12 @@ impl AppState {
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the Config management facade owned by the application state.
|
||||
#[must_use]
|
||||
pub(crate) const fn config_management(&self) -> &ksp_config_lib::ConfigManagement {
|
||||
return &self.config_management;
|
||||
}
|
||||
|
||||
/// Returns the resolved splash timings captured during application bootstrap.
|
||||
#[must_use]
|
||||
pub(crate) const fn splash_settings(&self) -> crate::SplashSettings {
|
||||
|
||||
@@ -17,3 +17,5 @@ pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "config.bootstrap";
|
||||
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
|
||||
/// Structured domain used by Tauri window lifecycle operations.
|
||||
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
|
||||
/// Structured domain for Config document inventory, diagnostics and repair.
|
||||
pub(crate) const TRACING_DOMAIN_DOCUMENTS: &str = "config.documents";
|
||||
|
||||
275
crates/ksp-app-config-desk/src/documents.rs
Normal file
275
crates/ksp-app-config-desk/src/documents.rs
Normal file
@@ -0,0 +1,275 @@
|
||||
// file: crates/ksp-app-config-desk/src/documents.rs
|
||||
// version: 1
|
||||
|
||||
//! Generic Config document inventory, diagnostics and validated repair services.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
const STATUS_VALID: &str = "valid";
|
||||
const STATUS_INVALID: &str = "invalid";
|
||||
const STAGE_VALID: &str = "valid";
|
||||
const STAGE_READ: &str = "read";
|
||||
const STAGE_JSON: &str = "json";
|
||||
const STAGE_SCHEMA: &str = "schema";
|
||||
const STAGE_SEMANTIC: &str = "semantic";
|
||||
const STAGE_EFFECTIVE: &str = "effective";
|
||||
const STAGE_OTHER: &str = "other";
|
||||
|
||||
/// Safe inventory row for one Config document registered by `ksp-config-lib`.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/documents/ConfigDocumentSummaryDto.ts")]
|
||||
pub(crate) struct ConfigDocumentSummaryDto {
|
||||
/// Stable logical Config identifier.
|
||||
pub(crate) file_id: String,
|
||||
/// Current Config-managed relative filename mapping.
|
||||
pub(crate) filename: String,
|
||||
/// Registered validation schema identifier.
|
||||
pub(crate) schema_file_id: std::option::Option<String>,
|
||||
/// Resolved filesystem path controlled by Config.
|
||||
pub(crate) path: String,
|
||||
/// `valid` or `invalid` according to the backend Config authority.
|
||||
pub(crate) validation_status: String,
|
||||
/// Backend diagnostic stage: valid/read/json/schema/semantic/effective/other.
|
||||
pub(crate) diagnostic_stage: String,
|
||||
/// Safe bounded diagnostic when the document is invalid.
|
||||
pub(crate) diagnostic: std::option::Option<crate::CommandErrorDto>,
|
||||
}
|
||||
|
||||
/// Detailed management view for one registered Config document.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/documents/ConfigDocumentDetailDto.ts")]
|
||||
pub(crate) struct ConfigDocumentDetailDto {
|
||||
/// Current validation/inventory summary.
|
||||
pub(crate) summary: ConfigDocumentSummaryDto,
|
||||
/// Raw Config-managed source when the file can be read, even if its JSON/schema/semantics are invalid.
|
||||
pub(crate) source: std::option::Option<String>,
|
||||
}
|
||||
|
||||
/// Result of one validated raw-source repair attempt that reached persistence successfully.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/documents/ConfigDocumentSaveResultDto.ts")]
|
||||
pub(crate) struct ConfigDocumentSaveResultDto {
|
||||
/// Whether persisted source bytes changed.
|
||||
pub(crate) source_changed: bool,
|
||||
/// Whether Config consumers need a reload to observe the changed document.
|
||||
pub(crate) reload_required: bool,
|
||||
/// Fresh detail reloaded after persistence.
|
||||
pub(crate) document: ConfigDocumentDetailDto,
|
||||
}
|
||||
|
||||
/// Safe document-specific command error with backend-owned diagnostic classification.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/documents/ConfigDocumentErrorDto.ts")]
|
||||
pub(crate) struct ConfigDocumentErrorDto {
|
||||
/// Backend diagnostic stage associated with the failure.
|
||||
pub(crate) diagnostic_stage: String,
|
||||
/// Bounded KSP error projection without arbitrary context/source values.
|
||||
pub(crate) error: crate::CommandErrorDto,
|
||||
}
|
||||
|
||||
impl ConfigDocumentErrorDto {
|
||||
fn from_error(error: &ksp_core_lib::Error) -> Self {
|
||||
return Self { diagnostic_stage: classify_error(error).to_owned(), error: crate::CommandErrorDto::from_error(error) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Lists all Config-kind documents from the Config registry and evaluates their current backend validation state.
|
||||
pub(crate) fn inventory(state: &crate::AppState) -> std::vec::Vec<ConfigDocumentSummaryDto> {
|
||||
let management = state.config_management();
|
||||
let engine = management.engine();
|
||||
let mut documents = std::vec::Vec::new();
|
||||
for descriptor in engine.registry().descriptors() {
|
||||
if descriptor.kind() != ksp_config_lib::ConfigFileKind::Config {
|
||||
continue;
|
||||
}
|
||||
documents.push(build_summary(management, descriptor));
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_DOCUMENTS,
|
||||
document_count = documents.len(),
|
||||
"Config document inventory evaluated"
|
||||
);
|
||||
return documents;
|
||||
}
|
||||
|
||||
/// Loads one Config document detail, including raw source when Config can read it.
|
||||
pub(crate) fn detail(state: &crate::AppState, file_id_text: &str) -> std::result::Result<ConfigDocumentDetailDto, ConfigDocumentErrorDto> {
|
||||
let file_id = parse_config_file_id(file_id_text);
|
||||
let file_id = match file_id {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(ConfigDocumentErrorDto::from_error(&error)),
|
||||
};
|
||||
let management = state.config_management();
|
||||
let descriptor = management.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(ConfigDocumentErrorDto::from_error(&error)),
|
||||
};
|
||||
if descriptor.kind() != ksp_config_lib::ConfigFileKind::Config {
|
||||
let error = ksp_core_lib::Error::new(crate::ERROR_CODE_DOCUMENT_KIND_INVALID, "requested document is not a Config-kind document");
|
||||
return std::result::Result::Err(ConfigDocumentErrorDto::from_error(&error));
|
||||
}
|
||||
let summary = build_summary(management, descriptor);
|
||||
let source = management.read_source(&file_id);
|
||||
let source = match source {
|
||||
std::result::Result::Ok(value) => std::option::Option::Some(value.content().to_owned()),
|
||||
std::result::Result::Err(_) => std::option::Option::None,
|
||||
};
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_DOCUMENTS,
|
||||
file_id = file_id.as_str(),
|
||||
validation_status = summary.validation_status.as_str(),
|
||||
diagnostic_stage = summary.diagnostic_stage.as_str(),
|
||||
source_available = source.is_some(),
|
||||
"Config document detail loaded"
|
||||
);
|
||||
return std::result::Result::Ok(ConfigDocumentDetailDto { summary, source });
|
||||
}
|
||||
|
||||
/// Validates and atomically persists one raw Config source candidate, then reloads the document detail.
|
||||
pub(crate) fn save_source(
|
||||
state: &crate::AppState,
|
||||
file_id_text: &str,
|
||||
source: &str,
|
||||
) -> std::result::Result<ConfigDocumentSaveResultDto, ConfigDocumentErrorDto> {
|
||||
let file_id = parse_config_file_id(file_id_text);
|
||||
let file_id = match file_id {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(ConfigDocumentErrorDto::from_error(&error)),
|
||||
};
|
||||
let report = state.config_management().save_source_candidate(&file_id, source);
|
||||
let report = match report {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(ConfigDocumentErrorDto::from_error(&error)),
|
||||
};
|
||||
let document = detail(state, file_id.as_str());
|
||||
let document = match document {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
ksp_logging_lib::info!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_DOCUMENTS,
|
||||
file_id = file_id.as_str(),
|
||||
source_changed = report.source_changed(),
|
||||
reload_required = report.reload_required(),
|
||||
"validated Config document source candidate persisted"
|
||||
);
|
||||
return std::result::Result::Ok(ConfigDocumentSaveResultDto {
|
||||
source_changed: report.source_changed(),
|
||||
reload_required: report.reload_required(),
|
||||
document,
|
||||
});
|
||||
}
|
||||
|
||||
fn parse_config_file_id(value: &str) -> ksp_core_lib::Result<ksp_config_lib::ConfigFileId> {
|
||||
let file_id = ksp_config_lib::ConfigFileId::new(value.to_owned());
|
||||
let file_id = match file_id {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(file_id);
|
||||
}
|
||||
|
||||
fn build_summary(management: &ksp_config_lib::ConfigManagement, descriptor: &ksp_config_lib::ConfigFileDescriptor) -> ConfigDocumentSummaryDto {
|
||||
let engine = management.engine();
|
||||
let path = engine.registry().resolve_path(engine.bootstrap(), descriptor.file_id());
|
||||
let path = match path {
|
||||
std::result::Result::Ok(value) => value.to_string_lossy().into_owned(),
|
||||
std::result::Result::Err(_) => descriptor.filename().to_string_lossy().into_owned(),
|
||||
};
|
||||
let validation = validate_effective_document(management, descriptor.file_id());
|
||||
let (validation_status, diagnostic_stage, diagnostic) = match validation {
|
||||
std::result::Result::Ok(()) => (STATUS_VALID.to_owned(), STAGE_VALID.to_owned(), std::option::Option::None),
|
||||
std::result::Result::Err(error) => {
|
||||
(STATUS_INVALID.to_owned(), classify_error(&error).to_owned(), std::option::Option::Some(crate::CommandErrorDto::from_error(&error)))
|
||||
},
|
||||
};
|
||||
return ConfigDocumentSummaryDto {
|
||||
file_id: descriptor.file_id().as_str().to_owned(),
|
||||
filename: descriptor.filename().to_string_lossy().into_owned(),
|
||||
schema_file_id: descriptor.schema_file_id().map(|value| {
|
||||
return value.as_str().to_owned();
|
||||
}),
|
||||
path,
|
||||
validation_status,
|
||||
diagnostic_stage,
|
||||
diagnostic,
|
||||
};
|
||||
}
|
||||
|
||||
fn validate_effective_document(management: &ksp_config_lib::ConfigManagement, file_id: &ksp_config_lib::ConfigFileId) -> ksp_core_lib::Result<()> {
|
||||
let validated = management.engine().load_validated_document(file_id);
|
||||
match validated {
|
||||
std::result::Result::Ok(_) => {},
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
if file_id.as_str() != ksp_config_lib::FILE_ID_STD_LOGGING {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
let environment = ksp_config_lib::ConfigEnvironment::load();
|
||||
let environment = match environment {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let resolved = management.engine().load_resolved_logging_config(std::option::Option::None, &environment);
|
||||
return match resolved {
|
||||
std::result::Result::Ok(_) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
fn classify_error(error: &ksp_core_lib::Error) -> &'static str {
|
||||
let code = error.code();
|
||||
if code == ksp_config_lib::ERROR_CODE_JSON_FILE_READ_FAILED {
|
||||
if error_context_file_id_is_schema(error) {
|
||||
return STAGE_SCHEMA;
|
||||
}
|
||||
return STAGE_READ;
|
||||
}
|
||||
if code == ksp_config_lib::ERROR_CODE_JSON_SYNTAX_INVALID {
|
||||
if error_context_file_id_is_schema(error) {
|
||||
return STAGE_SCHEMA;
|
||||
}
|
||||
return STAGE_JSON;
|
||||
}
|
||||
if code == ksp_config_lib::ERROR_CODE_SCHEMA_INVALID || code == ksp_config_lib::ERROR_CODE_SCHEMA_VALIDATION_FAILED {
|
||||
return STAGE_SCHEMA;
|
||||
}
|
||||
if code == ksp_config_lib::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID
|
||||
|| code == ksp_config_lib::ERROR_CODE_PROFILE_NOT_FOUND
|
||||
|| code == ksp_config_lib::ERROR_CODE_COMPOSITE_REFERENCE_INVALID
|
||||
{
|
||||
return STAGE_SEMANTIC;
|
||||
}
|
||||
if code == ksp_config_lib::ERROR_CODE_EFFECTIVE_CONFIG_INVALID
|
||||
|| code == ksp_config_lib::ERROR_CODE_ENVIRONMENT_PLACEHOLDER_INVALID
|
||||
|| code == ksp_config_lib::ERROR_CODE_ENVIRONMENT_VALUE_INVALID
|
||||
|| code == ksp_config_lib::ERROR_CODE_ENVIRONMENT_VARIABLE_INVALID
|
||||
|| code == ksp_config_lib::ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING
|
||||
|| code == ksp_config_lib::ERROR_CODE_DOTENV_FILE_READ_FAILED
|
||||
|| code == ksp_config_lib::ERROR_CODE_DOTENV_SYNTAX_INVALID
|
||||
{
|
||||
return STAGE_EFFECTIVE;
|
||||
}
|
||||
return STAGE_OTHER;
|
||||
}
|
||||
|
||||
fn error_context_file_id_is_schema(error: &ksp_core_lib::Error) -> bool {
|
||||
for field in error.context() {
|
||||
if field.key() == "file_id" {
|
||||
return field.value().starts_with("schema.");
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/documents.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/src/errors.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
//! Application-local error codes for the configuration desktop shell.
|
||||
|
||||
@@ -24,3 +24,6 @@ pub(crate) const ERROR_CODE_TAURI_WINDOW_MISSING: ksp_core_lib::ErrorCode = ksp_
|
||||
/// A Tauri window show/focus/destroy/event operation failed.
|
||||
pub(crate) const ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("config_desk", "tauri_window_operation_failed");
|
||||
|
||||
/// A Documents command requested a registered file that is not a Config-kind document.
|
||||
pub(crate) const ERROR_CODE_DOCUMENT_KIND_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "document_kind_invalid");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/src/lib.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Tauri desktop application for managing and validating KSP configuration.
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
mod app_state;
|
||||
mod bootstrap;
|
||||
mod constants;
|
||||
mod documents;
|
||||
mod dto_common;
|
||||
mod errors;
|
||||
mod frontend_logging;
|
||||
@@ -25,16 +26,22 @@ pub(crate) use self::app_state::AppState;
|
||||
pub(crate) use self::bootstrap::config_management;
|
||||
pub(crate) use self::bootstrap::initialize_logging;
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP;
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_DOCUMENTS;
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
pub(crate) use self::constants::TRACING_TARGET_FRONTEND;
|
||||
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN;
|
||||
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH;
|
||||
pub(crate) use self::documents::ConfigDocumentDetailDto;
|
||||
pub(crate) use self::documents::ConfigDocumentErrorDto;
|
||||
pub(crate) use self::documents::ConfigDocumentSaveResultDto;
|
||||
pub(crate) use self::documents::ConfigDocumentSummaryDto;
|
||||
pub(crate) use self::dto_common::AppSnapshotDto;
|
||||
pub(crate) use self::dto_common::CommandErrorDto;
|
||||
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
|
||||
pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
|
||||
pub(crate) use self::errors::ERROR_CODE_DOCUMENT_KIND_INVALID;
|
||||
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID;
|
||||
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID;
|
||||
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/src/tauri.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
//! Tauri runtime assembly for the KSP configuration desktop application.
|
||||
|
||||
@@ -37,7 +37,14 @@ fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<taur
|
||||
|
||||
#[allow(clippy::question_mark_used)] // Tauri generates the question-mark operator internally for async command dispatch.
|
||||
fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||
return builder.invoke_handler(tauri::generate_handler![get_app_snapshot, emit_frontend_log, splash_frontend_ready]);
|
||||
return builder.invoke_handler(tauri::generate_handler![
|
||||
get_app_snapshot,
|
||||
get_config_documents,
|
||||
get_config_document_detail,
|
||||
save_config_document_source,
|
||||
emit_frontend_log,
|
||||
splash_frontend_ready
|
||||
]);
|
||||
}
|
||||
|
||||
fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||
@@ -63,6 +70,28 @@ fn get_app_snapshot(state: tauri::State<'_, crate::AppState>) -> std::result::Re
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_config_documents(state: tauri::State<'_, crate::AppState>) -> std::vec::Vec<crate::ConfigDocumentSummaryDto> {
|
||||
return crate::documents::inventory(&state);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_config_document_detail(
|
||||
file_id: String,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::ConfigDocumentDetailDto, crate::ConfigDocumentErrorDto> {
|
||||
return crate::documents::detail(&state, file_id.as_str());
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn save_config_document_source(
|
||||
file_id: String,
|
||||
source: String,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::ConfigDocumentSaveResultDto, crate::ConfigDocumentErrorDto> {
|
||||
return crate::documents::save_source(&state, file_id.as_str(), source.as_str());
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> {
|
||||
let result = crate::emit_frontend_log_event(payload);
|
||||
|
||||
Reference in New Issue
Block a user