mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-07 18:42:11 +08:00
feat(contract): export wire contract from backend and generate frontend types
Adds a zero-dependency `frontend-contract` bin that serializes the real NoticeStatus variants and FrontendEvent payloads, scripts/gen-contract.mjs which emits the committed src/services/contract.ts, and a CI job that regenerates it and fails on a non-empty diff. events.ts now derives its event map from the generated contract instead of hand-typed shapes.
This commit is contained in:
@@ -3,6 +3,7 @@ pub use announce::{Refresh, after_commit, announce};
|
||||
pub mod desktop;
|
||||
pub mod handle;
|
||||
pub mod notification;
|
||||
pub use notification::frontend_wire_contract;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
|
||||
pub enum NoticeStatus {
|
||||
@@ -106,55 +107,62 @@ pub enum NoticeStatus {
|
||||
SetConfigOk,
|
||||
}
|
||||
|
||||
impl NoticeStatus {
|
||||
/// Every variant in golden order. The wire-contract export and the golden
|
||||
/// test both derive their variant list from here; expected strings stay
|
||||
/// hand-written in the test so renames cannot slip through.
|
||||
pub const ALL: [Self; 41] = [
|
||||
Self::Info,
|
||||
Self::ImportSubUrlOk,
|
||||
Self::ImportSubUrlError,
|
||||
Self::SetConfigError,
|
||||
Self::CoreStartError,
|
||||
Self::ServiceCoreRepairRequired,
|
||||
Self::ServiceCoreAppDataNotOwned,
|
||||
Self::ServiceCoreSidecarFallback,
|
||||
Self::DnsOverrideAutoDisabled,
|
||||
Self::EnhanceDiscardedKeys,
|
||||
Self::TunModeAutoDisabled,
|
||||
Self::TunModeAutoDisableFailed,
|
||||
Self::AppRestartCoreStopFailed,
|
||||
Self::AppQuitCoreStopFailed,
|
||||
Self::UpdateWithClashProxy,
|
||||
Self::UpdateFailedEvenWithClash,
|
||||
Self::ReactivateProfilesError,
|
||||
Self::UpdateFailed,
|
||||
Self::ConfigValidateBootError,
|
||||
Self::ConfigValidateError,
|
||||
Self::ConfigValidateProcessTerminated,
|
||||
Self::ConfigValidateScriptError,
|
||||
Self::ConfigValidateScriptSyntaxError,
|
||||
Self::ConfigValidateScriptMissingMain,
|
||||
Self::ConfigValidateFileNotFound,
|
||||
Self::ConfigValidateYamlSyntaxError,
|
||||
Self::ConfigValidateYamlReadError,
|
||||
Self::ConfigValidateYamlMappingError,
|
||||
Self::ConfigValidateMergeSyntaxError,
|
||||
Self::ConfigValidateMergeMappingError,
|
||||
Self::ConfigCoreChangeSuccess,
|
||||
Self::ConfigCoreChangeError,
|
||||
Self::MixedPortFallback,
|
||||
Self::MixedPortFallbackError,
|
||||
Self::ConfigValidateCoreChange,
|
||||
Self::ConfigValidateStdoutError,
|
||||
Self::ConfigValidateYamlKeyError,
|
||||
Self::ConfigValidateYamlError,
|
||||
Self::ConfigValidateMergeKeyError,
|
||||
Self::ConfigValidateMergeError,
|
||||
Self::SetConfigOk,
|
||||
];
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::NoticeStatus;
|
||||
|
||||
#[test]
|
||||
fn notice_status_wire_contract() -> Result<(), serde_json::Error> {
|
||||
let statuses = [
|
||||
NoticeStatus::Info,
|
||||
NoticeStatus::ImportSubUrlOk,
|
||||
NoticeStatus::ImportSubUrlError,
|
||||
NoticeStatus::SetConfigError,
|
||||
NoticeStatus::CoreStartError,
|
||||
NoticeStatus::ServiceCoreRepairRequired,
|
||||
NoticeStatus::ServiceCoreAppDataNotOwned,
|
||||
NoticeStatus::ServiceCoreSidecarFallback,
|
||||
NoticeStatus::DnsOverrideAutoDisabled,
|
||||
NoticeStatus::EnhanceDiscardedKeys,
|
||||
NoticeStatus::TunModeAutoDisabled,
|
||||
NoticeStatus::TunModeAutoDisableFailed,
|
||||
NoticeStatus::AppRestartCoreStopFailed,
|
||||
NoticeStatus::AppQuitCoreStopFailed,
|
||||
NoticeStatus::UpdateWithClashProxy,
|
||||
NoticeStatus::UpdateFailedEvenWithClash,
|
||||
NoticeStatus::ReactivateProfilesError,
|
||||
NoticeStatus::UpdateFailed,
|
||||
NoticeStatus::ConfigValidateBootError,
|
||||
NoticeStatus::ConfigValidateError,
|
||||
NoticeStatus::ConfigValidateProcessTerminated,
|
||||
NoticeStatus::ConfigValidateScriptError,
|
||||
NoticeStatus::ConfigValidateScriptSyntaxError,
|
||||
NoticeStatus::ConfigValidateScriptMissingMain,
|
||||
NoticeStatus::ConfigValidateFileNotFound,
|
||||
NoticeStatus::ConfigValidateYamlSyntaxError,
|
||||
NoticeStatus::ConfigValidateYamlReadError,
|
||||
NoticeStatus::ConfigValidateYamlMappingError,
|
||||
NoticeStatus::ConfigValidateMergeSyntaxError,
|
||||
NoticeStatus::ConfigValidateMergeMappingError,
|
||||
NoticeStatus::ConfigCoreChangeSuccess,
|
||||
NoticeStatus::ConfigCoreChangeError,
|
||||
NoticeStatus::MixedPortFallback,
|
||||
NoticeStatus::MixedPortFallbackError,
|
||||
NoticeStatus::ConfigValidateCoreChange,
|
||||
NoticeStatus::ConfigValidateStdoutError,
|
||||
NoticeStatus::ConfigValidateYamlKeyError,
|
||||
NoticeStatus::ConfigValidateYamlError,
|
||||
NoticeStatus::ConfigValidateMergeKeyError,
|
||||
NoticeStatus::ConfigValidateMergeError,
|
||||
NoticeStatus::SetConfigOk,
|
||||
];
|
||||
let statuses = NoticeStatus::ALL;
|
||||
let expected = [
|
||||
"info",
|
||||
"import_sub_url::ok",
|
||||
|
||||
@@ -12,7 +12,7 @@ use std::{
|
||||
};
|
||||
use tauri::{AppHandle, Emitter as _, Manager as _, WebviewWindow};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum FrontendEvent<'a> {
|
||||
RefreshClash,
|
||||
RefreshVerge,
|
||||
@@ -324,67 +324,7 @@ mod wire_tests {
|
||||
#[test]
|
||||
fn frontend_event_wire_contract() -> Result<(), serde_json::Error> {
|
||||
let uid = String::from("profile");
|
||||
let events = [
|
||||
(
|
||||
FrontendEvent::RefreshClash,
|
||||
"verge://refresh-clash-config",
|
||||
json!("yes"),
|
||||
),
|
||||
(
|
||||
FrontendEvent::RefreshVerge,
|
||||
"verge://refresh-verge-config",
|
||||
json!("yes"),
|
||||
),
|
||||
(FrontendEvent::RefreshProfiles, "verge://refresh-profiles", json!("yes")),
|
||||
(
|
||||
FrontendEvent::RefreshProxyConfig,
|
||||
"verge://refresh-proxy-config",
|
||||
json!(null),
|
||||
),
|
||||
(
|
||||
FrontendEvent::NoticeMessage {
|
||||
status: NoticeStatus::SetConfigOk,
|
||||
message: Arc::from("ok"),
|
||||
},
|
||||
"verge://notice-message",
|
||||
json!(["set_config::ok", "ok"]),
|
||||
),
|
||||
(
|
||||
FrontendEvent::TimerUpdated { profile_index: &uid },
|
||||
"verge://timer-updated",
|
||||
json!("profile"),
|
||||
),
|
||||
(
|
||||
FrontendEvent::RunStateChanged {
|
||||
state: json!({"running": true}),
|
||||
},
|
||||
"verge://run-state-changed",
|
||||
json!({"running": true}),
|
||||
),
|
||||
(
|
||||
FrontendEvent::PendingFailuresChanged,
|
||||
"verge://pending-failures-changed",
|
||||
json!(null),
|
||||
),
|
||||
(
|
||||
FrontendEvent::ProfileChanged {
|
||||
current_profile_id: &uid,
|
||||
},
|
||||
"profile-changed",
|
||||
json!("profile"),
|
||||
),
|
||||
(
|
||||
FrontendEvent::ProfileUpdateStarted { uid: &uid },
|
||||
"profile-update-started",
|
||||
json!({"uid": "profile"}),
|
||||
),
|
||||
(
|
||||
FrontendEvent::ProfileUpdateCompleted { uid: &uid },
|
||||
"profile-update-completed",
|
||||
json!({"uid": "profile"}),
|
||||
),
|
||||
];
|
||||
for (event, name, payload) in events {
|
||||
for (event, name, payload) in super::sample_frontend_events(&uid) {
|
||||
let (actual_name, actual_payload) = NotificationSystem::serialize_event(event);
|
||||
assert_eq!(actual_name, name);
|
||||
assert_eq!(actual_payload?, payload);
|
||||
@@ -392,3 +332,93 @@ mod wire_tests {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// One sample per `FrontendEvent` variant, paired with its wire name and payload.
|
||||
/// Shared by the wire golden test and the contract export so the two cannot drift.
|
||||
fn sample_frontend_events(uid: &String) -> Vec<(FrontendEvent<'_>, &'static str, serde_json::Value)> {
|
||||
vec![
|
||||
(
|
||||
FrontendEvent::RefreshClash,
|
||||
"verge://refresh-clash-config",
|
||||
json!("yes"),
|
||||
),
|
||||
(
|
||||
FrontendEvent::RefreshVerge,
|
||||
"verge://refresh-verge-config",
|
||||
json!("yes"),
|
||||
),
|
||||
(
|
||||
FrontendEvent::RefreshProfiles,
|
||||
"verge://refresh-profiles",
|
||||
json!("yes"),
|
||||
),
|
||||
(
|
||||
FrontendEvent::RefreshProxyConfig,
|
||||
"verge://refresh-proxy-config",
|
||||
json!(null),
|
||||
),
|
||||
(
|
||||
FrontendEvent::NoticeMessage {
|
||||
status: NoticeStatus::SetConfigOk,
|
||||
message: Arc::from("ok"),
|
||||
},
|
||||
"verge://notice-message",
|
||||
json!(["set_config::ok", "ok"]),
|
||||
),
|
||||
(
|
||||
FrontendEvent::TimerUpdated { profile_index: uid },
|
||||
"verge://timer-updated",
|
||||
json!("profile"),
|
||||
),
|
||||
(
|
||||
FrontendEvent::RunStateChanged {
|
||||
state: json!({"running": true}),
|
||||
},
|
||||
"verge://run-state-changed",
|
||||
json!({"running": true}),
|
||||
),
|
||||
(
|
||||
FrontendEvent::PendingFailuresChanged,
|
||||
"verge://pending-failures-changed",
|
||||
json!(null),
|
||||
),
|
||||
(
|
||||
FrontendEvent::ProfileChanged {
|
||||
current_profile_id: uid,
|
||||
},
|
||||
"profile-changed",
|
||||
json!("profile"),
|
||||
),
|
||||
(
|
||||
FrontendEvent::ProfileUpdateStarted { uid },
|
||||
"profile-update-started",
|
||||
json!({"uid": "profile"}),
|
||||
),
|
||||
(
|
||||
FrontendEvent::ProfileUpdateCompleted { uid },
|
||||
"profile-update-completed",
|
||||
json!({"uid": "profile"}),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/// Machine-readable form of the frontend wire contract: every event name with a
|
||||
/// serialized sample payload, plus every notice status string. The sample
|
||||
/// payload's JSON shape (string / null / array / object) is what the frontend
|
||||
/// generator maps to TypeScript types. `ThemeChanged` is a Tauri built-in
|
||||
/// linux-only window event, not part of this contract.
|
||||
pub fn frontend_wire_contract() -> serde_json::Value {
|
||||
let uid = String::from("sample");
|
||||
let events: Vec<_> = sample_frontend_events(&uid)
|
||||
.into_iter()
|
||||
.map(|(event, _, _)| {
|
||||
let (name, payload) = NotificationSystem::serialize_event(event);
|
||||
json!({ "name": name, "sample": payload.unwrap_or(serde_json::Value::Null) })
|
||||
})
|
||||
.collect();
|
||||
let statuses: Vec<_> = NoticeStatus::ALL
|
||||
.iter()
|
||||
.map(|status| serde_json::to_value(status).expect("NoticeStatus serializes"))
|
||||
.collect();
|
||||
json!({ "version": 1, "events": events, "noticeStatuses": statuses })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user