优化结构
This commit is contained in:
@@ -10,3 +10,44 @@ func CloneAdmin(value *Admin) *Admin { return cloneAdmin(value) }
|
||||
func CloneDatabase(value *Database) *Database { return clonePtr(value) }
|
||||
func CloneStorage(value *Storage) *Storage { return cloneStorage(value) }
|
||||
func CloneEmail(value *Email) *Email { return clonePtr(value) }
|
||||
|
||||
// MergeRuntimeConfig applies the runtime replacement contract shared by file
|
||||
// watching and management updates. Omitted top-level sections keep their
|
||||
// current values, while database-backed storage/email settings are never
|
||||
// erased merely because they are absent from config.yaml.
|
||||
func MergeRuntimeConfig(current, next *Config) *Config {
|
||||
merged := cloneConfig(next)
|
||||
if merged == nil {
|
||||
merged = &Config{}
|
||||
}
|
||||
if current == nil {
|
||||
if merged.Admin == nil {
|
||||
merged.Admin = &Admin{}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
if merged.Server == nil {
|
||||
merged.Server = clonePtr(current.Server)
|
||||
}
|
||||
if merged.Data == nil {
|
||||
merged.Data = cloneData(current.Data)
|
||||
}
|
||||
if merged.Admin == nil {
|
||||
merged.Admin = cloneAdmin(current.Admin)
|
||||
}
|
||||
if merged.Admin == nil {
|
||||
merged.Admin = &Admin{}
|
||||
}
|
||||
if current.Admin != nil {
|
||||
if merged.Admin.Storage == nil {
|
||||
merged.Admin.Storage = cloneStorage(current.Admin.Storage)
|
||||
}
|
||||
if merged.Admin.Email == nil {
|
||||
merged.Admin.Email = clonePtr(current.Admin.Email)
|
||||
}
|
||||
if merged.Admin.ConfigPath == "" {
|
||||
merged.Admin.ConfigPath = current.Admin.ConfigPath
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
@@ -45,6 +45,13 @@ func (d *Document) Set(key string, value any) error {
|
||||
// Delete removes the nested key path if present.
|
||||
func (d *Document) Delete(keys ...string) { deleteYAMLMapping(&d.node, keys...) }
|
||||
|
||||
// DeleteIntegrationConfig removes settings owned by the database-backed
|
||||
// integration store rather than by config.yaml.
|
||||
func (d *Document) DeleteIntegrationConfig() {
|
||||
d.Delete("admin", "storage")
|
||||
d.Delete("admin", "email")
|
||||
}
|
||||
|
||||
// Has reports whether the nested key path exists.
|
||||
func (d *Document) Has(keys ...string) bool { return yamlMappingValue(&d.node, keys...) != nil }
|
||||
|
||||
|
||||
@@ -268,23 +268,7 @@ func (r *Store) watchLoop(watcher *fsnotify.Watcher, stop <-chan struct{}, done
|
||||
// take down a running process or publish invalid state.
|
||||
continue
|
||||
}
|
||||
current := r.Snapshot()
|
||||
if next.Admin == nil {
|
||||
next.Admin = &Admin{}
|
||||
}
|
||||
// Storage and Email move to database-backed integration settings after
|
||||
// first initialization. External config file edits must not erase the
|
||||
// active values when those sections are absent from config.yaml.
|
||||
if current != nil && current.Admin != nil {
|
||||
if next.Admin.Storage == nil {
|
||||
next.Admin.Storage = cloneStorage(current.Admin.Storage)
|
||||
}
|
||||
if next.Admin.Email == nil && current.Admin.Email != nil {
|
||||
email := *current.Admin.Email
|
||||
next.Admin.Email = &email
|
||||
}
|
||||
}
|
||||
r.Replace(next)
|
||||
r.Replace(MergeRuntimeConfig(r.Snapshot(), next))
|
||||
case _, ok := <-errors:
|
||||
// fsnotify errors are intentionally non-fatal; the watcher remains
|
||||
// useful for subsequent events and Close always terminates it.
|
||||
|
||||
@@ -145,3 +145,26 @@ func TestStoreWatchPreservesDatabaseBackedIntegrationSettings(t *testing.T) {
|
||||
}
|
||||
t.Fatal("watcher did not publish the changed file")
|
||||
}
|
||||
|
||||
func TestMergeRuntimeConfigPreservesOmittedSections(t *testing.T) {
|
||||
current := &Config{
|
||||
Server: &Server{HTTP: &ServerHTTP{Addr: ":8080"}},
|
||||
Data: &Data{Database: &Database{Source: "current-dsn"}},
|
||||
Admin: &Admin{ConfigPath: "config.yaml", Storage: &Storage{Type: "local"}, Email: &Email{Host: "smtp.example.com"}},
|
||||
}
|
||||
next := &Config{Admin: &Admin{App: &App{AppID: "next"}}}
|
||||
merged := MergeRuntimeConfig(current, next)
|
||||
if merged.Server == nil || merged.Server.HTTP == nil || merged.Server.HTTP.Addr != ":8080" {
|
||||
t.Fatalf("server was not preserved: %#v", merged.Server)
|
||||
}
|
||||
if merged.Data == nil || merged.Data.Database == nil || merged.Data.Database.Source != "current-dsn" {
|
||||
t.Fatalf("data was not preserved: %#v", merged.Data)
|
||||
}
|
||||
if merged.Admin.Storage == nil || merged.Admin.Storage.Type != "local" || merged.Admin.Email == nil || merged.Admin.Email.Host != "smtp.example.com" {
|
||||
t.Fatalf("integration settings were not preserved: %#v", merged.Admin)
|
||||
}
|
||||
merged.Admin.Storage.Type = "changed"
|
||||
if current.Admin.Storage.Type != "local" {
|
||||
t.Fatal("MergeRuntimeConfig leaked mutable storage state into current config")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user