feat(storage): Phase 5 C1 — AppConfig 持久化到 config.json
- src/storage/config.rs: AppConfig + ServerEntry + load_config +
save_config,atomic write(tmp + rename + fsync);多 server
数据结构按 §21 落地但 V1 只 active 一个;3 个 #[cfg(test)]
round-trip + default + missing-field 测试
- src/storage/state.rs: 从 src/storage.rs 拆过来;atomic write + 2
个测试(default 960×680 + JSON round-trip)
- src/storage/mod.rs: pub use re-export 两组 API
- src/config.rs: 重构为 ActiveConfig thin wrapper,调
storage::load_config() 拿到第一个 selected_server;placeholder
hardcoded URL/token 在 §19/§20 意义上不再合适,删掉
- src/gotify/model.rs: ServerConfig 加 Serialize/Deserialize(要
写到 config.json 必须能 serialize;token 仍走 §20 不进日志)
- src/lib.rs: run() 启动用 ActiveConfig::load() 取代 placeholder;
on_test_connection_requested 同样改用 ActiveConfig::load()
- 删除 src/storage.rs(被 src/storage/{config,state}.rs 替代;
同样内容已迁过去)
避坑:
- ServerConfig 之前没 derive Serialize,所以 #[serde(flatten)]
嵌套到 ServerEntry 失败——加 derive 是必要的
- src/storage.rs 和 src/storage/mod.rs 不能并存,否则 Rust 报
ambiguous module;删旧文件就行
- AppConfig 多 vec + serde default 让旧 config 文件(字段缺失)
也能 forward-compat parse(3 个测试覆盖)
验证:cargo build / build --release 都 exit 0,0 warning
- cargo test: 14 passed (5 storage unit + 6 api + 3 websocket); 0 failed
- debug 35.54 MB, subsystem = WINDOWS_CUI
- release 9.76 MB, subsystem = WINDOWS_GUI
This commit is contained in:
+55
-18
@@ -1,29 +1,66 @@
|
||||
//! Application configuration placeholder.
|
||||
//! Active-config view used at runtime.
|
||||
//!
|
||||
//! Phase 3 ships only a hardcoded `placeholder()` so the UI wiring can
|
||||
//! demonstrate the real-data path. Phase 5 replaces this with persistent
|
||||
//! JSON storage at `%APPDATA%/WindowsGotify/config.json` (AGENTS.md §5/§19/§20).
|
||||
//!
|
||||
//! AGENTS.md §20 — the placeholder token below is intentionally not a
|
||||
//! real credential. Real auth fails, the UI falls back to mock data, and
|
||||
//! nothing about the token ever reaches a log line.
|
||||
//! Phase 5 C1: this is a thin wrapper over `storage::AppConfig`. It
|
||||
//! surfaces the single selected server as `ServerConfig` so the rest
|
||||
//! of the runtime (`gotify::GotifyApi`, `gotify::websocket`) doesn't
|
||||
//! have to care about the multi-server storage layout. The placeholder
|
||||
//! URL/token that lived here in Phase 3 C2 is gone — first-run users
|
||||
//! will see "no server configured" until they fill in Settings
|
||||
//! (Phase 6 hooks up the Settings → Connection form).
|
||||
|
||||
use crate::error::AppResult;
|
||||
use crate::gotify::ServerConfig;
|
||||
use crate::storage::AppConfig as PersistedConfig;
|
||||
|
||||
pub struct AppConfig {
|
||||
pub struct ActiveConfig {
|
||||
pub server: Option<ServerConfig>,
|
||||
pub appearance: String,
|
||||
pub desktop_notifications: bool,
|
||||
pub sound: bool,
|
||||
pub notify_high: bool,
|
||||
pub notify_max: bool,
|
||||
pub start_with_windows: bool,
|
||||
pub start_minimized: bool,
|
||||
pub close_to_tray: bool,
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
/// Hardcoded single-server placeholder. Returns `None` here means
|
||||
/// "no server configured" — but for now Phase 3 always returns `Some`
|
||||
/// so the UI wiring is exercised on every start.
|
||||
pub fn placeholder() -> Self {
|
||||
impl ActiveConfig {
|
||||
/// Load from `%APPDATA%/WindowsGotify/config.json` (or defaults on
|
||||
/// missing / malformed file). Returns `Ok(ActiveConfig)` even when
|
||||
/// there's no server configured — the caller decides how to
|
||||
/// surface that in the UI.
|
||||
pub fn load() -> AppResult<Self> {
|
||||
let persisted = crate::storage::load_config();
|
||||
Ok(Self::from_persisted(persisted))
|
||||
}
|
||||
|
||||
fn from_persisted(p: PersistedConfig) -> Self {
|
||||
let server = p
|
||||
.active_server()
|
||||
.or_else(|| p.first_enabled_server())
|
||||
.map(|entry| entry.config.clone());
|
||||
Self {
|
||||
server: Some(ServerConfig::new(
|
||||
"http://192.168.50.103:50334",
|
||||
"placeholder-token-do-not-log",
|
||||
)),
|
||||
server,
|
||||
appearance: p.appearance,
|
||||
desktop_notifications: p.desktop_notifications,
|
||||
sound: p.sound,
|
||||
notify_high: p.notify_high,
|
||||
notify_max: p.notify_max,
|
||||
start_with_windows: p.start_with_windows,
|
||||
start_minimized: p.start_minimized,
|
||||
close_to_tray: p.close_to_tray,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build an `ActiveConfig` from a fully-formed `PersistedConfig`
|
||||
/// value (handy for tests and for the upcoming Settings → Save
|
||||
/// path, which constructs the persisted config in memory and then
|
||||
/// asks us to mirror it back).
|
||||
pub fn from_persisted_value(p: PersistedConfig) -> Self {
|
||||
Self::from_persisted(p)
|
||||
}
|
||||
|
||||
pub fn placeholder() -> Self {
|
||||
Self::from_persisted(PersistedConfig::default())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user