refactor: structure config validation outcomes

- Replace boolean/string validation results with structured ValidationOutcome values across config validation, profile switching, DNS validation, and script validation. Callers can now distinguish valid, invalid, skipped, and busy states, with invalid results carrying an error kind and message.

- Route core config updates through structured outcomes and checked forced updates. Generation and validation failures now discard runtime drafts and surface validation failures instead of relying on empty success messages.

- Preserve existing frontend ergonomics by keeping boolean service wrappers where callers only need success, while direct DNS and script validation consumers receive ValidationOutcome. Failed profile editor saves reload the backend-restored file content and synchronize Monaco models so reopened editors show the restored state.
This commit is contained in:
Tunglies
2026-04-25 13:31:08 +08:00
parent 5882722031
commit b85b435e00
26 changed files with 612 additions and 482 deletions
+43 -13
View File
@@ -2,7 +2,10 @@ use super::CoreManager;
use crate::{
config::{Config, ConfigType, runtime::IRuntime},
constants::timing,
core::{handle, validate::CoreConfigValidator},
core::{
handle,
validate::{CoreConfigValidator, ValidationOutcome, ValidationSkipReason},
},
utils::{dirs, help},
};
use anyhow::{Result, anyhow};
@@ -31,18 +34,40 @@ impl CoreManager {
Ok(())
}
pub async fn update_config(&self) -> Result<(bool, String)> {
pub async fn update_config_forced(&self) -> Result<ValidationOutcome> {
self.update_config_with_force(true).await
}
pub async fn update_config_with_force(&self, force: bool) -> Result<ValidationOutcome> {
if handle::Handle::global().is_exiting() {
return Ok((true, String::new()));
return Ok(ValidationOutcome::Skipped {
reason: ValidationSkipReason::Exiting,
});
}
if !self.should_update_config() {
return Ok((true, String::new()));
if !force && !self.should_update_config() {
logging!(debug, Type::Core, "Skipping config update due to debounce");
return Ok(ValidationOutcome::Skipped {
reason: ValidationSkipReason::Debounced,
});
}
if force {
self.set_last_update(Instant::now());
}
self.perform_config_update().await
}
pub async fn update_config_checked(&self) -> Result<()> {
let outcome = self.update_config_forced().await?;
if outcome.is_valid() {
Ok(())
} else {
Err(anyhow!("{outcome}"))
}
}
fn should_update_config(&self) -> bool {
let now = Instant::now();
let last = self.get_last_update();
@@ -57,21 +82,26 @@ impl CoreManager {
true
}
async fn perform_config_update(&self) -> Result<(bool, String)> {
Config::generate().await?;
async fn perform_config_update(&self) -> Result<ValidationOutcome> {
if let Err(err) = Config::generate().await {
let message: String = err.to_string().into();
Config::runtime().await.discard();
return Ok(ValidationOutcome::invalid_from_message(message));
}
self.apply_generate_config().await
}
pub async fn apply_generate_config(&self) -> Result<(bool, String)> {
match CoreConfigValidator::global().validate_config().await {
Ok((true, _)) => {
pub async fn apply_generate_config(&self) -> Result<ValidationOutcome> {
match CoreConfigValidator::global().validate_config_outcome().await {
Ok(outcome) if outcome.is_valid() => {
let run_path = Config::generate_file(ConfigType::Run).await?;
self.apply_config(run_path).await?;
Ok((true, String::new()))
Ok(ValidationOutcome::Valid)
}
Ok((false, error_msg)) => {
Ok(outcome) => {
Config::runtime().await.discard();
Ok((false, error_msg))
Ok(outcome)
}
Err(e) => {
Config::runtime().await.discard();
+1 -1
View File
@@ -58,7 +58,7 @@ impl CoreManager {
let verge_data = Config::verge().await.latest_arc();
verge_data.save_file().await.map_err(|e| e.to_string())?;
self.update_config().await.stringify_err()?;
self.update_config_checked().await.stringify_err()?;
Ok(())
}