feat(config): add reflection-based configFields framework
This commit is contained in:
parent
55b2e7f6cb
commit
3f6ddfbcd4
2 changed files with 193 additions and 0 deletions
55
cmd/config_fields.go
Normal file
55
cmd/config_fields.go
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import "reflect"
|
||||||
|
|
||||||
|
// ConfigField represents a single leaf field in the Config struct,
|
||||||
|
// mapped to its dotted TOML key path.
|
||||||
|
type ConfigField struct {
|
||||||
|
Key string // dotted key, e.g. "git.auto_commit"
|
||||||
|
Value any // current value
|
||||||
|
Default any // value from defaultConfig()
|
||||||
|
IsDefault bool // Value == Default
|
||||||
|
Field reflect.Value // settable reflect.Value (from cfg pointer)
|
||||||
|
Kind reflect.Kind // field type kind
|
||||||
|
}
|
||||||
|
|
||||||
|
// configFields walks cfg and defaults in parallel, returning a ConfigField
|
||||||
|
// for every leaf field. Keys are built from TOML struct tags.
|
||||||
|
func configFields(cfg, defaults *Config) []ConfigField {
|
||||||
|
var fields []ConfigField
|
||||||
|
walk(reflect.ValueOf(cfg).Elem(), reflect.ValueOf(defaults).Elem(), "", &fields)
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
func walk(cv, dv reflect.Value, prefix string, out *[]ConfigField) {
|
||||||
|
ct := cv.Type()
|
||||||
|
for i := 0; i < ct.NumField(); i++ {
|
||||||
|
sf := ct.Field(i)
|
||||||
|
tag := sf.Tag.Get("toml")
|
||||||
|
if tag == "" || tag == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
key := tag
|
||||||
|
if prefix != "" {
|
||||||
|
key = prefix + "." + tag
|
||||||
|
}
|
||||||
|
|
||||||
|
cfv := cv.Field(i)
|
||||||
|
dfv := dv.Field(i)
|
||||||
|
|
||||||
|
if sf.Type.Kind() == reflect.Struct {
|
||||||
|
walk(cfv, dfv, key, out)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
*out = append(*out, ConfigField{
|
||||||
|
Key: key,
|
||||||
|
Value: cfv.Interface(),
|
||||||
|
Default: dfv.Interface(),
|
||||||
|
IsDefault: reflect.DeepEqual(cfv.Interface(), dfv.Interface()),
|
||||||
|
Field: cfv,
|
||||||
|
Kind: sf.Type.Kind(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
138
cmd/config_fields_test.go
Normal file
138
cmd/config_fields_test.go
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfigFieldsReturnsAllFields(t *testing.T) {
|
||||||
|
cfg := defaultConfig()
|
||||||
|
defaults := defaultConfig()
|
||||||
|
fields := configFields(&cfg, &defaults)
|
||||||
|
|
||||||
|
// Count expected leaf fields by walking the struct
|
||||||
|
expected := countLeafFields(reflect.TypeOf(Config{}))
|
||||||
|
if len(fields) != expected {
|
||||||
|
t.Errorf("configFields returned %d fields, want %d", len(fields), expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func countLeafFields(t reflect.Type) int {
|
||||||
|
n := 0
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
f := t.Field(i)
|
||||||
|
if f.Type.Kind() == reflect.Struct {
|
||||||
|
n += countLeafFields(f.Type)
|
||||||
|
} else {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigFieldsDottedKeys(t *testing.T) {
|
||||||
|
cfg := defaultConfig()
|
||||||
|
defaults := defaultConfig()
|
||||||
|
fields := configFields(&cfg, &defaults)
|
||||||
|
|
||||||
|
want := map[string]bool{
|
||||||
|
"display_ascii_art": true,
|
||||||
|
"key.always_prompt_delete": true,
|
||||||
|
"key.always_prompt_glob_delete": true,
|
||||||
|
"key.always_prompt_overwrite": true,
|
||||||
|
"store.default_store_name": true,
|
||||||
|
"store.list_all_stores": true,
|
||||||
|
"store.always_prompt_delete": true,
|
||||||
|
"store.always_prompt_overwrite": true,
|
||||||
|
"git.auto_fetch": true,
|
||||||
|
"git.auto_commit": true,
|
||||||
|
"git.auto_push": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
got := make(map[string]bool)
|
||||||
|
for _, f := range fields {
|
||||||
|
got[f.Key] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range want {
|
||||||
|
if !got[k] {
|
||||||
|
t.Errorf("missing key %q", k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for k := range got {
|
||||||
|
if !want[k] {
|
||||||
|
t.Errorf("unexpected key %q", k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigFieldsAllDefaults(t *testing.T) {
|
||||||
|
cfg := defaultConfig()
|
||||||
|
defaults := defaultConfig()
|
||||||
|
fields := configFields(&cfg, &defaults)
|
||||||
|
|
||||||
|
for _, f := range fields {
|
||||||
|
if !f.IsDefault {
|
||||||
|
t.Errorf("field %q should be default, got Value=%v Default=%v", f.Key, f.Value, f.Default)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigFieldsDetectsNonDefault(t *testing.T) {
|
||||||
|
cfg := defaultConfig()
|
||||||
|
cfg.Git.AutoCommit = true
|
||||||
|
defaults := defaultConfig()
|
||||||
|
fields := configFields(&cfg, &defaults)
|
||||||
|
|
||||||
|
for _, f := range fields {
|
||||||
|
if f.Key == "git.auto_commit" {
|
||||||
|
if f.IsDefault {
|
||||||
|
t.Errorf("git.auto_commit should not be default after change")
|
||||||
|
}
|
||||||
|
if f.Value != true {
|
||||||
|
t.Errorf("git.auto_commit Value = %v, want true", f.Value)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Error("git.auto_commit not found in fields")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigFieldsSettable(t *testing.T) {
|
||||||
|
cfg := defaultConfig()
|
||||||
|
defaults := defaultConfig()
|
||||||
|
fields := configFields(&cfg, &defaults)
|
||||||
|
|
||||||
|
for _, f := range fields {
|
||||||
|
if f.Key == "git.auto_push" {
|
||||||
|
if f.Kind != reflect.Bool {
|
||||||
|
t.Errorf("git.auto_push Kind = %v, want Bool", f.Kind)
|
||||||
|
}
|
||||||
|
f.Field.SetBool(true)
|
||||||
|
if !cfg.Git.AutoPush {
|
||||||
|
t.Error("setting field via reflect did not update cfg")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Error("git.auto_push not found in fields")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigFieldsStringField(t *testing.T) {
|
||||||
|
cfg := defaultConfig()
|
||||||
|
defaults := defaultConfig()
|
||||||
|
fields := configFields(&cfg, &defaults)
|
||||||
|
|
||||||
|
for _, f := range fields {
|
||||||
|
if f.Key == "store.default_store_name" {
|
||||||
|
if f.Kind != reflect.String {
|
||||||
|
t.Errorf("store.default_store_name Kind = %v, want String", f.Kind)
|
||||||
|
}
|
||||||
|
if f.Value != "default" {
|
||||||
|
t.Errorf("store.default_store_name Value = %v, want 'default'", f.Value)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Error("store.default_store_name not found in fields")
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue