Naming, layout and comments: descriptive full-word names, one item per line in wrapped calls and literals, and doc comments on exported names
Short and single-letter names in application code become full words (tests keep theirs). Wrapped calls, signatures and composite literals put one item per line, and one-line function bodies are expanded. Exported names and groups of constants get doc comments, and comments refer to the renamed identifiers.
This commit is contained in:
1 parent
043e307663
commit
465358609a
78 files changed
+2374
-1402
No files matched your search
@@ -50,7 +50,8 @@ func (c *JSCLogging) validate(key string, value any) error {
|
||||
case "debug", "info", "warn", "error":
|
||||
default:
|
||||
return errors.New(
|
||||
"invalid log level. Must be one of: debug, info, warn, error")
|
||||
"invalid log level. Must be one of: debug, info, warn, error",
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -25,8 +25,12 @@ func captureLogs(t *testing.T, level slog.Level) *bytes.Buffer {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
def := slog.Default()
|
||||
slog.SetDefault(slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: level})))
|
||||
t.Cleanup(func() { slog.SetDefault(def) })
|
||||
slog.SetDefault(slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: level,
|
||||
})))
|
||||
t.Cleanup(func() {
|
||||
slog.SetDefault(def)
|
||||
})
|
||||
return &buf
|
||||
}
|
||||
|
||||
@@ -62,7 +66,10 @@ func TestDefaults(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoggingSection(t *testing.T) {
|
||||
c := JSCLogging{Level: "info", File: "/tmp/x.log"}
|
||||
c := JSCLogging{
|
||||
Level: "info",
|
||||
File: "/tmp/x.log",
|
||||
}
|
||||
|
||||
if got, want := c.Keys(), []string{"level", "file"}; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("Keys = %v; want %v", got, want)
|
||||
@@ -142,7 +149,9 @@ func TestConfigLevelToLogLevel(t *testing.T) {
|
||||
if got := ConfigLevelToLogLevel(in); got != want {
|
||||
t.Errorf("ConfigLevelToLogLevel(%q) = %v; want %v", in, got, want)
|
||||
}
|
||||
c := JSCLogging{Level: in}
|
||||
c := JSCLogging{
|
||||
Level: in,
|
||||
}
|
||||
if got := c.LogLevel(); got != want {
|
||||
t.Errorf("LogLevel() with %q = %v; want %v", in, got, want)
|
||||
}
|
||||
@@ -150,7 +159,10 @@ func TestConfigLevelToLogLevel(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSourceSection(t *testing.T) {
|
||||
c := JSCSourcesSource{Enabled: true, Interval: time.Minute}
|
||||
c := JSCSourcesSource{
|
||||
Enabled: true,
|
||||
Interval: time.Minute,
|
||||
}
|
||||
|
||||
if got, want := c.Keys(), []string{"enabled", "interval"}; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("Keys = %v; want %v", got, want)
|
||||
@@ -458,7 +470,9 @@ func TestLoadFromViperInvalidValues(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDatabaseSection(t *testing.T) {
|
||||
c := JSCDatabase{File: "./jobsearch.db"}
|
||||
c := JSCDatabase{
|
||||
File: "./jobsearch.db",
|
||||
}
|
||||
|
||||
if got, want := c.Keys(), []string{"file"}; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("Keys = %v; want %v", got, want)
|
||||
|
||||
+19
-19
@@ -62,8 +62,8 @@ func (c *JobSearchConfig) SetValue(key string, value any) error {
|
||||
// Unknown keys are skipped; invalid values are logged and ignored.
|
||||
func LoadFromViper(conf *JobSearchConfig) {
|
||||
keys := viper.AllKeys()
|
||||
for idx := range keys {
|
||||
key := keys[idx]
|
||||
for i := range keys {
|
||||
key := keys[i]
|
||||
keyType, _ := conf.TypeOf(key)
|
||||
if keyType == reflect.Invalid {
|
||||
continue
|
||||
@@ -94,15 +94,15 @@ func viperValue(key string, kind reflect.Kind) (value any, ok bool) {
|
||||
// SetValue to split on commas; GetStringSlice would split it on
|
||||
// spaces. A list of objects (a list of sections) is passed on as it
|
||||
// is, as GetStringSlice would flatten it.
|
||||
switch v := viper.Get(key).(type) {
|
||||
switch value := viper.Get(key).(type) {
|
||||
case string:
|
||||
return v, true
|
||||
return value, true
|
||||
case []map[string]any: // as LoadToViper sets it
|
||||
return v, true
|
||||
return value, true
|
||||
case []any:
|
||||
if len(v) > 0 {
|
||||
if _, ok := v[0].(map[string]any); ok {
|
||||
return v, true
|
||||
if len(value) > 0 {
|
||||
if _, ok := value[0].(map[string]any); ok {
|
||||
return value, true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,26 +121,26 @@ func LoadToViper(conf JobSearchConfig) {
|
||||
|
||||
// String formats the configuration one "key = value <type>" line per key, with
|
||||
// dotted keys, sorted by key.
|
||||
func (conf JobSearchConfig) String() string {
|
||||
var sb strings.Builder
|
||||
flat := conf.Flatten()
|
||||
for _, k := range slices.Sorted(maps.Keys(flat)) {
|
||||
typ, _ := conf.TypeOf(k)
|
||||
fmt.Fprint(&sb, k, " = ", flat[k], " <", typ, ">\n")
|
||||
func (c JobSearchConfig) String() string {
|
||||
var builder strings.Builder
|
||||
flat := c.Flatten()
|
||||
for _, key := range slices.Sorted(maps.Keys(flat)) {
|
||||
kind, _ := c.TypeOf(key)
|
||||
fmt.Fprint(&builder, key, " = ", flat[key], " <", kind, ">\n")
|
||||
}
|
||||
return sb.String()
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// JSON formats the configuration as indented JSON.
|
||||
func (conf JobSearchConfig) JSON() string {
|
||||
confMap := conf.Map()
|
||||
func (c JobSearchConfig) JSON() string {
|
||||
confMap := c.Map()
|
||||
jbytes, _ := json.MarshalIndent(confMap, "", " ")
|
||||
return string(jbytes)
|
||||
}
|
||||
|
||||
// YAML formats the configuration as YAML.
|
||||
func (conf JobSearchConfig) YAML() string {
|
||||
confMap := conf.Map()
|
||||
func (c JobSearchConfig) YAML() string {
|
||||
confMap := c.Map()
|
||||
ybytes, _ := yaml.Marshal(confMap)
|
||||
return string(ybytes)
|
||||
}
|
||||
@@ -70,7 +70,14 @@ func countSections(uses map[reflect.Type]int, section reflect.Type) {
|
||||
}
|
||||
}
|
||||
|
||||
func checkSections(t *testing.T, conf *JobSearchConfig, all map[string]any, uses map[reflect.Type]int, prefix string, section reflect.Value) {
|
||||
func checkSections(
|
||||
t *testing.T,
|
||||
conf *JobSearchConfig,
|
||||
all map[string]any,
|
||||
uses map[reflect.Type]int,
|
||||
prefix string,
|
||||
section reflect.Value,
|
||||
) {
|
||||
for _, f := range fieldsOf(section.Type()) {
|
||||
key := joinKey(prefix, f.key)
|
||||
if isSectionList(f.typ) {
|
||||
@@ -93,8 +100,12 @@ func checkSections(t *testing.T, conf *JobSearchConfig, all map[string]any, uses
|
||||
}
|
||||
for k := range keys {
|
||||
if _, ok := all[joinKey(keyPrefix, k)]; !ok {
|
||||
t.Errorf("%s.Flatten() key %q does not match its cfg tag %q",
|
||||
name, k, key)
|
||||
t.Errorf(
|
||||
"%s.Flatten() key %q does not match its cfg tag %q",
|
||||
name,
|
||||
k,
|
||||
key,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +127,7 @@ func checkSections(t *testing.T, conf *JobSearchConfig, all map[string]any, uses
|
||||
// checkSectionList checks the items of the list of sections at key implement
|
||||
// Section and that their methods agree with the list's value in all.
|
||||
func checkSectionList(t *testing.T, all map[string]any, key string, list reflect.Value) {
|
||||
t.Helper()
|
||||
name := list.Type().Elem().Name()
|
||||
if !reflect.PointerTo(list.Type().Elem()).Implements(sectionType) {
|
||||
t.Errorf("*%s (items of %s) does not implement Section", name, key)
|
||||
@@ -145,7 +157,13 @@ func checkSectionList(t *testing.T, all map[string]any, key string, list reflect
|
||||
// checkSectionMethods checks section's Map, TypeOf, ValueOf and SetValue
|
||||
// against want, its keys and values, and kinds, their kinds.
|
||||
// Setting each key to its own value must succeed and leave section unchanged.
|
||||
func checkSectionMethods(t *testing.T, name string, section reflect.Value, want map[string]any, kinds map[string]reflect.Kind) {
|
||||
func checkSectionMethods(
|
||||
t *testing.T,
|
||||
name string,
|
||||
section reflect.Value,
|
||||
want map[string]any,
|
||||
kinds map[string]reflect.Kind,
|
||||
) {
|
||||
s := section.Addr().Interface().(Section)
|
||||
if got := flattenMap("", s.Map()); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("%s.Map() =\n%v\nwant\n%v", name, got, want)
|
||||
@@ -184,14 +202,18 @@ func flattenMap(prefix string, m map[string]any) map[string]any {
|
||||
// checkEmbedded checks that structs embedded in a section, such as
|
||||
// JSCSourcesSource, implement Section too.
|
||||
func checkEmbedded(t *testing.T, section reflect.Type) {
|
||||
t.Helper()
|
||||
for i := 0; i < section.NumField(); i++ {
|
||||
sf := section.Field(i)
|
||||
if !sf.Anonymous || sf.Type.Kind() != reflect.Struct {
|
||||
continue
|
||||
}
|
||||
if !reflect.PointerTo(sf.Type).Implements(sectionType) {
|
||||
t.Errorf("*%s (embedded in %s) does not implement Section",
|
||||
sf.Type.Name(), section.Name())
|
||||
t.Errorf(
|
||||
"*%s (embedded in %s) does not implement Section",
|
||||
sf.Type.Name(),
|
||||
section.Name(),
|
||||
)
|
||||
}
|
||||
checkEmbedded(t, sf.Type)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,9 @@ func initDir(t *testing.T, env []string, config string) string {
|
||||
// godotenv sets variables without cleaning up after the test.
|
||||
for _, kv := range env {
|
||||
key, _, _ := strings.Cut(kv, "=")
|
||||
t.Cleanup(func() { os.Unsetenv(key) })
|
||||
t.Cleanup(func() {
|
||||
os.Unsetenv(key)
|
||||
})
|
||||
}
|
||||
writeFile(t, filepath.Join(dir, ".env"), strings.Join(env, "\n")+"\n")
|
||||
}
|
||||
@@ -108,7 +110,11 @@ func TestInitKeepsCommandLineLevel(t *testing.T) {
|
||||
initDir(t, []string{}, "logging:\n level: info\n")
|
||||
logs := captureLogs(t, slog.LevelDebug)
|
||||
|
||||
conf := JobSearchConfig{Logging: JSCLogging{Level: level}}
|
||||
conf := JobSearchConfig{
|
||||
Logging: JSCLogging{
|
||||
Level: level,
|
||||
},
|
||||
}
|
||||
if err := Init("", &conf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ func StoreValue(
|
||||
|
||||
// storeValue is StoreValue for any section, so key types that
|
||||
// JobSearchConfig doesn't have yet can be tested.
|
||||
func storeValue(s Section, key string, value string) (any, reflect.Kind, error) {
|
||||
confType, err := s.TypeOf(key)
|
||||
func storeValue(section Section, key string, value string) (any, reflect.Kind, error) {
|
||||
confType, err := section.TypeOf(key)
|
||||
if err != nil {
|
||||
return nil, confType, err
|
||||
}
|
||||
@@ -38,10 +38,10 @@ func storeValue(s Section, key string, value string) (any, reflect.Kind, error)
|
||||
// SetValue parses the string into the field's own type (e.g. int8,
|
||||
// float32 or time.Duration) and rejects values the configuration itself
|
||||
// would not accept, e.g. an unknown log level or an unparseable duration.
|
||||
if err := s.SetValue(key, value); err != nil {
|
||||
if err := section.SetValue(key, value); err != nil {
|
||||
return nil, confType, err
|
||||
}
|
||||
confValue, err := s.ValueOf(key)
|
||||
confValue, err := section.ValueOf(key)
|
||||
if err != nil {
|
||||
return nil, confType, err
|
||||
}
|
||||
@@ -93,9 +93,9 @@ func PurgeValue(conf *JobSearchConfig, key string) error {
|
||||
}
|
||||
out := viper.New()
|
||||
out.SetConfigFile(file.ConfigFileUsed())
|
||||
for _, k := range file.AllKeys() {
|
||||
if k != key {
|
||||
out.Set(k, file.Get(k))
|
||||
for _, fileKey := range file.AllKeys() {
|
||||
if fileKey != key {
|
||||
out.Set(fileKey, file.Get(fileKey))
|
||||
}
|
||||
}
|
||||
return out.WriteConfig()
|
||||
|
||||
@@ -11,18 +11,18 @@ import (
|
||||
)
|
||||
|
||||
// PrintTerminal prints the configuration to stdout as String formats it.
|
||||
func (conf JobSearchConfig) PrintTerminal() {
|
||||
fmt.Print(conf.String() + "\n")
|
||||
func (c JobSearchConfig) PrintTerminal() {
|
||||
fmt.Print(c.String() + "\n")
|
||||
}
|
||||
|
||||
// PrintJson prints the configuration to stdout as JSON.
|
||||
func (conf JobSearchConfig) PrintJson() {
|
||||
fmt.Print(conf.JSON() + "\n")
|
||||
func (c JobSearchConfig) PrintJson() {
|
||||
fmt.Print(c.JSON() + "\n")
|
||||
}
|
||||
|
||||
// PrintYaml prints the configuration to stdout as YAML.
|
||||
func (conf JobSearchConfig) PrintYaml() {
|
||||
fmt.Print(conf.YAML() + "\n")
|
||||
func (c JobSearchConfig) PrintYaml() {
|
||||
fmt.Print(c.YAML() + "\n")
|
||||
}
|
||||
|
||||
// PrintLog logs each configuration key and value, sorted by key, at atLevel:
|
||||
@@ -55,4 +55,4 @@ func (conf JobSearchConfig) PrintLog(atLevel slog.Level) {
|
||||
for _, key := range keys {
|
||||
logger.Log(ctx, atLevel, "config value", "key", key, "value", flat[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
+197
-179
@@ -26,7 +26,8 @@ import (
|
||||
//
|
||||
// time.Duration fields report reflect.String from TypeOf and accept either a
|
||||
// time.Duration or a duration string such as "90s" in SetValue.
|
||||
// util.ByteSize fields work the same way, with size strings such as "32KiB".
|
||||
// util.ByteSize fields work the same way, with size strings such as "32KiB"
|
||||
// (or "-1" for no limit).
|
||||
//
|
||||
// []string fields report reflect.Slice and accept either a []string or a
|
||||
// comma-separated string such as "a, b" in SetValue (and so in default tags).
|
||||
@@ -42,16 +43,25 @@ import (
|
||||
//
|
||||
// A section can check values before they are stored by implementing
|
||||
// validator on its pointer type.
|
||||
// The struct tags section.go reads.
|
||||
const (
|
||||
keyTag = "cfg"
|
||||
// keyTag holds a field's configuration key.
|
||||
keyTag = "cfg"
|
||||
// defaultTag holds a field's default value.
|
||||
defaultTag = "default"
|
||||
)
|
||||
|
||||
// The unknown key error and the field types section.go handles specially.
|
||||
var (
|
||||
errUnknownKey = errors.New("unknown configuration key")
|
||||
durationType = reflect.TypeOf(time.Duration(0))
|
||||
byteSizeType = reflect.TypeOf(util.ByteSize(0))
|
||||
timeType = reflect.TypeOf(time.Time{})
|
||||
// errUnknownKey is returned for a key that names no field.
|
||||
errUnknownKey = errors.New("unknown configuration key")
|
||||
// durationType is time.Duration, read and given out as a string.
|
||||
durationType = reflect.TypeOf(time.Duration(0))
|
||||
// byteSizeType is util.ByteSize, read and given out as a string.
|
||||
byteSizeType = reflect.TypeOf(util.ByteSize(0))
|
||||
// timeType is time.Time, a struct that is a value, not a section.
|
||||
timeType = reflect.TypeOf(time.Time{})
|
||||
// stringSliceType is []string, set from a comma-separated list.
|
||||
stringSliceType = reflect.TypeOf([]string{})
|
||||
)
|
||||
|
||||
@@ -100,67 +110,67 @@ type field struct {
|
||||
hasDefault bool
|
||||
}
|
||||
|
||||
// isSectionList reports whether t is a list of sections, []S for a section
|
||||
// struct S.
|
||||
func isSectionList(t reflect.Type) bool {
|
||||
return t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Struct &&
|
||||
t.Elem() != timeType
|
||||
// isSectionList reports whether listType is a list of sections, []S for a
|
||||
// section struct S.
|
||||
func isSectionList(listType reflect.Type) bool {
|
||||
return listType.Kind() == reflect.Slice && listType.Elem().Kind() == reflect.Struct &&
|
||||
listType.Elem() != timeType
|
||||
}
|
||||
|
||||
// fieldsOf lists the configuration fields of a section type in declaration
|
||||
// order, with the fields of embedded structs promoted into it.
|
||||
func fieldsOf(t reflect.Type) []field {
|
||||
func fieldsOf(structType reflect.Type) []field {
|
||||
var fields []field
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
sf := t.Field(i)
|
||||
if sf.Anonymous && sf.Type.Kind() == reflect.Struct {
|
||||
for _, f := range fieldsOf(sf.Type) {
|
||||
f.index = append([]int{i}, f.index...)
|
||||
fields = append(fields, f)
|
||||
for i := 0; i < structType.NumField(); i++ {
|
||||
structField := structType.Field(i)
|
||||
if structField.Anonymous && structField.Type.Kind() == reflect.Struct {
|
||||
for _, embeddedField := range fieldsOf(structField.Type) {
|
||||
embeddedField.index = append([]int{i}, embeddedField.index...)
|
||||
fields = append(fields, embeddedField)
|
||||
}
|
||||
continue
|
||||
}
|
||||
key, ok := sf.Tag.Lookup(keyTag)
|
||||
if !ok || key == "" || key == "-" || !sf.IsExported() {
|
||||
key, ok := structField.Tag.Lookup(keyTag)
|
||||
if !ok || key == "" || key == "-" || !structField.IsExported() {
|
||||
continue
|
||||
}
|
||||
def, hasDefault := sf.Tag.Lookup(defaultTag)
|
||||
defaultValue, hasDefault := structField.Tag.Lookup(defaultTag)
|
||||
fields = append(fields, field{
|
||||
key: key,
|
||||
index: []int{i},
|
||||
typ: sf.Type,
|
||||
section: sf.Type.Kind() == reflect.Struct && sf.Type != timeType,
|
||||
def: def,
|
||||
typ: structField.Type,
|
||||
section: structField.Type.Kind() == reflect.Struct && structField.Type != timeType,
|
||||
def: defaultValue,
|
||||
hasDefault: hasDefault,
|
||||
})
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
// structValue dereferences v down to the section struct it holds.
|
||||
func structValue(v any) reflect.Value {
|
||||
rv := reflect.ValueOf(v)
|
||||
for rv.Kind() == reflect.Pointer {
|
||||
rv = rv.Elem()
|
||||
// structValue dereferences value down to the section struct it holds.
|
||||
func structValue(value any) reflect.Value {
|
||||
reflected := reflect.ValueOf(value)
|
||||
for reflected.Kind() == reflect.Pointer {
|
||||
reflected = reflected.Elem()
|
||||
}
|
||||
if rv.Kind() != reflect.Struct {
|
||||
panic(fmt.Sprintf("config: %T is not a configuration section", v))
|
||||
if reflected.Kind() != reflect.Struct {
|
||||
panic(fmt.Sprintf("config: %T is not a configuration section", value))
|
||||
}
|
||||
return rv
|
||||
return reflected
|
||||
}
|
||||
|
||||
// lookup resolves a dotted key to the leaf field and the section holding it.
|
||||
func lookup(section reflect.Value, key string) (reflect.Value, field, error) {
|
||||
head, rest, nested := strings.Cut(key, ".")
|
||||
for _, f := range fieldsOf(section.Type()) {
|
||||
if f.key != head {
|
||||
for _, configField := range fieldsOf(section.Type()) {
|
||||
if configField.key != head {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case f.section && nested:
|
||||
return lookup(section.FieldByIndex(f.index), rest)
|
||||
case !f.section && !nested:
|
||||
return section, f, nil
|
||||
case configField.section && nested:
|
||||
return lookup(section.FieldByIndex(configField.index), rest)
|
||||
case !configField.section && !nested:
|
||||
return section, configField, nil
|
||||
}
|
||||
// Key matched but the shape didn't: a section with no sub-key, or a
|
||||
// value with one.
|
||||
@@ -177,52 +187,52 @@ func joinKey(prefix, key string) string {
|
||||
}
|
||||
|
||||
// keysOf returns the keys directly in a section.
|
||||
func keysOf(v any) []string {
|
||||
fields := fieldsOf(structValue(v).Type())
|
||||
func keysOf(value any) []string {
|
||||
fields := fieldsOf(structValue(value).Type())
|
||||
keys := make([]string, 0, len(fields))
|
||||
for _, f := range fields {
|
||||
keys = append(keys, f.key)
|
||||
for _, configField := range fields {
|
||||
keys = append(keys, configField.key)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// mapOf converts a section to a map, with nested sections as nested maps.
|
||||
func mapOf(v any) map[string]any {
|
||||
return mapOfValue(structValue(v))
|
||||
func mapOf(value any) map[string]any {
|
||||
return mapOfValue(structValue(value))
|
||||
}
|
||||
|
||||
// mapOfValue is mapOf for a section struct value.
|
||||
func mapOfValue(section reflect.Value) map[string]any {
|
||||
m := map[string]any{}
|
||||
for _, f := range fieldsOf(section.Type()) {
|
||||
fv := section.FieldByIndex(f.index)
|
||||
if f.section {
|
||||
m[f.key] = mapOfValue(fv)
|
||||
values := map[string]any{}
|
||||
for _, configField := range fieldsOf(section.Type()) {
|
||||
fieldValue := section.FieldByIndex(configField.index)
|
||||
if configField.section {
|
||||
values[configField.key] = mapOfValue(fieldValue)
|
||||
} else {
|
||||
m[f.key] = exportValue(fv)
|
||||
values[configField.key] = exportValue(fieldValue)
|
||||
}
|
||||
}
|
||||
return m
|
||||
return values
|
||||
}
|
||||
|
||||
// flattenOf converts a section to a flat map of dotted keys, each prefixed
|
||||
// with prefix (if non-empty).
|
||||
func flattenOf(prefix string, v any) map[string]any {
|
||||
m := map[string]any{}
|
||||
flattenInto(m, prefix, structValue(v))
|
||||
return m
|
||||
func flattenOf(prefix string, value any) map[string]any {
|
||||
flat := map[string]any{}
|
||||
flattenInto(flat, prefix, structValue(value))
|
||||
return flat
|
||||
}
|
||||
|
||||
// flattenInto adds section's keys to m, dotted and prefixed with prefix (if
|
||||
// non-empty), descending into nested sections.
|
||||
func flattenInto(m map[string]any, prefix string, section reflect.Value) {
|
||||
for _, f := range fieldsOf(section.Type()) {
|
||||
fv := section.FieldByIndex(f.index)
|
||||
key := joinKey(prefix, f.key)
|
||||
if f.section {
|
||||
flattenInto(m, key, fv)
|
||||
func flattenInto(flat map[string]any, prefix string, section reflect.Value) {
|
||||
for _, configField := range fieldsOf(section.Type()) {
|
||||
fieldValue := section.FieldByIndex(configField.index)
|
||||
key := joinKey(prefix, configField.key)
|
||||
if configField.section {
|
||||
flattenInto(flat, key, fieldValue)
|
||||
} else {
|
||||
m[key] = exportValue(fv)
|
||||
flat[key] = exportValue(fieldValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,33 +242,33 @@ func flattenInto(m map[string]any, prefix string, section reflect.Value) {
|
||||
// so values agree with kindOf and can be passed back to SetValue; []string is
|
||||
// copied; a list of sections becomes a []map[string]any (nil if empty);
|
||||
// everything else is unchanged.
|
||||
func exportValue(fv reflect.Value) any {
|
||||
switch fv.Type() {
|
||||
func exportValue(fieldValue reflect.Value) any {
|
||||
switch fieldValue.Type() {
|
||||
case durationType:
|
||||
return time.Duration(fv.Int()).String()
|
||||
return time.Duration(fieldValue.Int()).String()
|
||||
case byteSizeType:
|
||||
return util.ByteSize(fv.Int()).String()
|
||||
return util.ByteSize(fieldValue.Int()).String()
|
||||
}
|
||||
if isSectionList(fv.Type()) {
|
||||
if isSectionList(fieldValue.Type()) {
|
||||
var items []map[string]any
|
||||
for i := range fv.Len() {
|
||||
items = append(items, mapOfValue(fv.Index(i)))
|
||||
for i := range fieldValue.Len() {
|
||||
items = append(items, mapOfValue(fieldValue.Index(i)))
|
||||
}
|
||||
return items
|
||||
}
|
||||
v := fv.Interface()
|
||||
if s, ok := v.([]string); ok {
|
||||
value := fieldValue.Interface()
|
||||
if list, ok := value.([]string); ok {
|
||||
// A copy, so changing it doesn't change the configuration.
|
||||
return slices.Clone(s)
|
||||
return slices.Clone(list)
|
||||
}
|
||||
return v
|
||||
return value
|
||||
}
|
||||
|
||||
// splitList splits a comma-separated list, trimming spaces around each item
|
||||
// and dropping empty ones, so "" is an empty list.
|
||||
func splitList(s string) []string {
|
||||
func splitList(list string) []string {
|
||||
var items []string
|
||||
for item := range strings.SplitSeq(s, ",") {
|
||||
for item := range strings.SplitSeq(list, ",") {
|
||||
if item = strings.TrimSpace(item); item != "" {
|
||||
items = append(items, item)
|
||||
}
|
||||
@@ -268,115 +278,120 @@ func splitList(s string) []string {
|
||||
|
||||
// kindOf is the kind reported by TypeOf, i.e. how the value is read from
|
||||
// Viper. Durations and byte sizes are read as strings and parsed by SetValue.
|
||||
func kindOf(t reflect.Type) reflect.Kind {
|
||||
if t == durationType || t == byteSizeType {
|
||||
func kindOf(fieldType reflect.Type) reflect.Kind {
|
||||
if fieldType == durationType || fieldType == byteSizeType {
|
||||
return reflect.String
|
||||
}
|
||||
return t.Kind()
|
||||
return fieldType.Kind()
|
||||
}
|
||||
|
||||
// typeOf returns the kind of the dotted key in section v (see kindOf), or
|
||||
// errUnknownKey.
|
||||
func typeOf(v any, key string) (reflect.Kind, error) {
|
||||
_, f, err := lookup(structValue(v), key)
|
||||
// typeOf returns the kind of the dotted key in the section value (see
|
||||
// kindOf), or errUnknownKey.
|
||||
func typeOf(value any, key string) (reflect.Kind, error) {
|
||||
_, configField, err := lookup(structValue(value), key)
|
||||
if err != nil {
|
||||
return reflect.Invalid, err
|
||||
}
|
||||
return kindOf(f.typ), nil
|
||||
return kindOf(configField.typ), nil
|
||||
}
|
||||
|
||||
// valueOf returns the value of the dotted key in section v, or errUnknownKey.
|
||||
func valueOf(v any, key string) (any, error) {
|
||||
section, f, err := lookup(structValue(v), key)
|
||||
// valueOf returns the value of the dotted key in the section value, or
|
||||
// errUnknownKey.
|
||||
func valueOf(value any, key string) (any, error) {
|
||||
section, configField, err := lookup(structValue(value), key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return exportValue(section.FieldByIndex(f.index)), nil
|
||||
return exportValue(section.FieldByIndex(configField.index)), nil
|
||||
}
|
||||
|
||||
// setValue sets a dotted key on the section that ptr points to.
|
||||
func setValue(ptr any, key string, value any) error {
|
||||
section, f, err := lookup(structValue(ptr), key)
|
||||
section, configField, err := lookup(structValue(ptr), key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return setField(section, f, value)
|
||||
return setField(section, configField, value)
|
||||
}
|
||||
|
||||
// setField converts value to the field's type and, if the section has a
|
||||
// validator, checks it, before setting the field. On error the field is left
|
||||
// unchanged.
|
||||
func setField(section reflect.Value, f field, value any) error {
|
||||
newValue, err := convert(value, f.typ)
|
||||
func setField(section reflect.Value, configField field, value any) error {
|
||||
newValue, err := convert(value, configField.typ)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if v, ok := section.Addr().Interface().(validator); ok {
|
||||
if err := v.validate(f.key, newValue.Interface()); err != nil {
|
||||
if checker, ok := section.Addr().Interface().(validator); ok {
|
||||
if err := checker.validate(configField.key, newValue.Interface()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
section.FieldByIndex(f.index).Set(newValue)
|
||||
section.FieldByIndex(configField.index).Set(newValue)
|
||||
return nil
|
||||
}
|
||||
|
||||
// convert turns value into a value of type t. Values already of the right
|
||||
// convert turns value into a value of type targetType. Values already of the right
|
||||
// type are used as-is, except that an empty []string becomes nil, as an empty
|
||||
// comma-separated list does (see splitList); strings are parsed. Lists of
|
||||
// sections are converted by convertSectionList.
|
||||
func convert(value any, t reflect.Type) (reflect.Value, error) {
|
||||
if isSectionList(t) {
|
||||
return convertSectionList(value, t)
|
||||
func convert(value any, targetType reflect.Type) (reflect.Value, error) {
|
||||
if isSectionList(targetType) {
|
||||
return convertSectionList(value, targetType)
|
||||
}
|
||||
if value != nil {
|
||||
rv := reflect.ValueOf(value)
|
||||
if rv.Type().AssignableTo(t) {
|
||||
if t == stringSliceType && rv.Len() == 0 {
|
||||
return reflect.Zero(t), nil
|
||||
reflected := reflect.ValueOf(value)
|
||||
if reflected.Type().AssignableTo(targetType) {
|
||||
if targetType == stringSliceType && reflected.Len() == 0 {
|
||||
return reflect.Zero(targetType), nil
|
||||
}
|
||||
return rv, nil
|
||||
return reflected, nil
|
||||
}
|
||||
if s, ok := value.(string); ok {
|
||||
return parseString(s, t)
|
||||
if text, ok := value.(string); ok {
|
||||
return parseString(text, targetType)
|
||||
}
|
||||
}
|
||||
return reflect.Value{}, fmt.Errorf("unsupported type %T", value)
|
||||
}
|
||||
|
||||
// convertSectionList turns value into a list of sections of type t: a value
|
||||
// of type t, a list of maps of keys to values, or a JSON array of objects.
|
||||
// convertSectionList turns value into a list of sections of type listType: a
|
||||
// value of type listType, a list of maps of keys to values, or a JSON array of objects.
|
||||
// nil and empty lists become a nil list.
|
||||
func convertSectionList(value any, t reflect.Type) (reflect.Value, error) {
|
||||
if s, ok := value.(string); ok {
|
||||
func convertSectionList(value any, listType reflect.Type) (reflect.Value, error) {
|
||||
if text, ok := value.(string); ok {
|
||||
var items []map[string]any
|
||||
if strings.TrimSpace(s) != "" {
|
||||
if err := json.Unmarshal([]byte(s), &items); err != nil {
|
||||
return reflect.Value{}, fmt.Errorf("unable to convert to a list of %s: %w", t.Elem(), err)
|
||||
if strings.TrimSpace(text) != "" {
|
||||
if err := json.Unmarshal([]byte(text), &items); err != nil {
|
||||
return reflect.Value{}, fmt.Errorf(
|
||||
"unable to convert to a list of %s: %w",
|
||||
listType.Elem(),
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
value = items
|
||||
}
|
||||
if value == nil {
|
||||
return reflect.Zero(t), nil
|
||||
return reflect.Zero(listType), nil
|
||||
}
|
||||
rv := reflect.ValueOf(value)
|
||||
if rv.Kind() != reflect.Slice {
|
||||
reflected := reflect.ValueOf(value)
|
||||
if reflected.Kind() != reflect.Slice {
|
||||
return reflect.Value{}, fmt.Errorf("unsupported type %T", value)
|
||||
}
|
||||
if rv.Len() == 0 {
|
||||
return reflect.Zero(t), nil
|
||||
if reflected.Len() == 0 {
|
||||
return reflect.Zero(listType), nil
|
||||
}
|
||||
if rv.Type().AssignableTo(t) {
|
||||
for i := range rv.Len() {
|
||||
if err := validateSection(rv.Index(i)); err != nil {
|
||||
if reflected.Type().AssignableTo(listType) {
|
||||
for i := range reflected.Len() {
|
||||
if err := validateSection(reflected.Index(i)); err != nil {
|
||||
return reflect.Value{}, fmt.Errorf("item %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
return rv, nil
|
||||
return reflected, nil
|
||||
}
|
||||
list := reflect.MakeSlice(t, 0, rv.Len())
|
||||
for i := range rv.Len() {
|
||||
item, err := sectionFromMap(rv.Index(i).Interface(), t.Elem())
|
||||
list := reflect.MakeSlice(listType, 0, reflected.Len())
|
||||
for i := range reflected.Len() {
|
||||
item, err := sectionFromMap(reflected.Index(i).Interface(), listType.Elem())
|
||||
if err != nil {
|
||||
return reflect.Value{}, fmt.Errorf("item %d: %w", i, err)
|
||||
}
|
||||
@@ -385,31 +400,31 @@ func convertSectionList(value any, t reflect.Type) (reflect.Value, error) {
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// sectionFromMap builds a section of type t from a map of its keys to values,
|
||||
// starting from its defaults. Values that are neither strings nor of the
|
||||
// field's type (e.g. numbers read from YAML) are formatted and parsed. The
|
||||
// finished section is validated as a whole (see validateSection), so a key
|
||||
// left out of the map can't skip its validation.
|
||||
func sectionFromMap(value any, t reflect.Type) (reflect.Value, error) {
|
||||
// sectionFromMap builds a section of type structType from value, a map of its
|
||||
// keys to values, starting from its defaults. Values that are neither strings
|
||||
// nor of the field's type (e.g. numbers read from YAML) are formatted and
|
||||
// parsed. The finished section is validated as a whole (see
|
||||
// validateSection), so a key left out of the map can't skip its validation.
|
||||
func sectionFromMap(value any, structType reflect.Type) (reflect.Value, error) {
|
||||
item, ok := value.(map[string]any)
|
||||
if !ok {
|
||||
return reflect.Value{}, fmt.Errorf("unsupported type %T", value)
|
||||
}
|
||||
section := reflect.New(t).Elem()
|
||||
section := reflect.New(structType).Elem()
|
||||
if err := applyDefaultsTo("", section); err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
for k, v := range item {
|
||||
s, f, err := lookup(section, k)
|
||||
for itemKey, itemValue := range item {
|
||||
holder, configField, err := lookup(section, itemKey)
|
||||
if err != nil {
|
||||
return reflect.Value{}, fmt.Errorf("%w %q", err, k)
|
||||
return reflect.Value{}, fmt.Errorf("%w %q", err, itemKey)
|
||||
}
|
||||
if _, isString := v.(string); v != nil && !isString &&
|
||||
!reflect.TypeOf(v).AssignableTo(f.typ) {
|
||||
v = fmt.Sprint(v)
|
||||
if _, isString := itemValue.(string); itemValue != nil && !isString &&
|
||||
!reflect.TypeOf(itemValue).AssignableTo(configField.typ) {
|
||||
itemValue = fmt.Sprint(itemValue)
|
||||
}
|
||||
if err := setField(s, f, v); err != nil {
|
||||
return reflect.Value{}, fmt.Errorf("%s: %w", k, err)
|
||||
if err := setField(holder, configField, itemValue); err != nil {
|
||||
return reflect.Value{}, fmt.Errorf("%s: %w", itemKey, err)
|
||||
}
|
||||
}
|
||||
if err := validateSection(section); err != nil {
|
||||
@@ -422,73 +437,76 @@ func sectionFromMap(value any, t reflect.Type) (reflect.Value, error) {
|
||||
// field directly in it, so values that were never set (defaults or zero
|
||||
// values) are checked too.
|
||||
func validateSection(section reflect.Value) error {
|
||||
v, ok := reflect.New(section.Type()).Interface().(validator)
|
||||
checker, ok := reflect.New(section.Type()).Interface().(validator)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
for _, f := range fieldsOf(section.Type()) {
|
||||
if f.section {
|
||||
for _, configField := range fieldsOf(section.Type()) {
|
||||
if configField.section {
|
||||
continue
|
||||
}
|
||||
if err := v.validate(f.key, section.FieldByIndex(f.index).Interface()); err != nil {
|
||||
return fmt.Errorf("%s: %w", f.key, err)
|
||||
if err := checker.validate(
|
||||
configField.key,
|
||||
section.FieldByIndex(configField.index).Interface(),
|
||||
); err != nil {
|
||||
return fmt.Errorf("%s: %w", configField.key, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseString parses s as a value of type t. It supports strings, bools,
|
||||
// parseString parses text as a value of type targetType. It supports strings, bools,
|
||||
// signed and unsigned integers, floats, time.Duration, ByteSize and []string
|
||||
// (from a comma-separated list, see splitList).
|
||||
func parseString(s string, t reflect.Type) (reflect.Value, error) {
|
||||
v := reflect.New(t).Elem()
|
||||
switch t {
|
||||
func parseString(text string, targetType reflect.Type) (reflect.Value, error) {
|
||||
parsed := reflect.New(targetType).Elem()
|
||||
switch targetType {
|
||||
case durationType:
|
||||
d, err := time.ParseDuration(s)
|
||||
duration, err := time.ParseDuration(text)
|
||||
if err != nil {
|
||||
return reflect.Value{}, fmt.Errorf("unable to convert to duration: %w", err)
|
||||
}
|
||||
v.SetInt(int64(d))
|
||||
return v, nil
|
||||
parsed.SetInt(int64(duration))
|
||||
return parsed, nil
|
||||
case byteSizeType:
|
||||
b, err := util.ParseByteSize(s)
|
||||
size, err := util.ParseByteSize(text)
|
||||
if err != nil {
|
||||
return reflect.Value{}, fmt.Errorf("unable to convert to byte size: %w", err)
|
||||
}
|
||||
v.SetInt(int64(b))
|
||||
return v, nil
|
||||
parsed.SetInt(int64(size))
|
||||
return parsed, nil
|
||||
case stringSliceType:
|
||||
v.Set(reflect.ValueOf(splitList(s)))
|
||||
return v, nil
|
||||
parsed.Set(reflect.ValueOf(splitList(text)))
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
switch t.Kind() {
|
||||
switch targetType.Kind() {
|
||||
case reflect.String:
|
||||
v.SetString(s)
|
||||
parsed.SetString(text)
|
||||
case reflect.Bool:
|
||||
var b bool
|
||||
b, err = strconv.ParseBool(s)
|
||||
v.SetBool(b)
|
||||
var flag bool
|
||||
flag, err = strconv.ParseBool(text)
|
||||
parsed.SetBool(flag)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
var n int64
|
||||
n, err = strconv.ParseInt(s, 10, t.Bits())
|
||||
v.SetInt(n)
|
||||
var number int64
|
||||
number, err = strconv.ParseInt(text, 10, targetType.Bits())
|
||||
parsed.SetInt(number)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
var n uint64
|
||||
n, err = strconv.ParseUint(s, 10, t.Bits())
|
||||
v.SetUint(n)
|
||||
var number uint64
|
||||
number, err = strconv.ParseUint(text, 10, targetType.Bits())
|
||||
parsed.SetUint(number)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
var n float64
|
||||
n, err = strconv.ParseFloat(s, t.Bits())
|
||||
v.SetFloat(n)
|
||||
var number float64
|
||||
number, err = strconv.ParseFloat(text, targetType.Bits())
|
||||
parsed.SetFloat(number)
|
||||
default:
|
||||
return reflect.Value{}, fmt.Errorf("unsupported type string for %s", t)
|
||||
return reflect.Value{}, fmt.Errorf("unsupported type string for %s", targetType)
|
||||
}
|
||||
if err != nil {
|
||||
return reflect.Value{}, fmt.Errorf("unable to convert %q to %s: %w", s, t, err)
|
||||
return reflect.Value{}, fmt.Errorf("unable to convert %q to %s: %w", text, targetType, err)
|
||||
}
|
||||
return v, nil
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
// applyDefaults sets every field that has a default tag, recursing into
|
||||
@@ -501,18 +519,18 @@ func applyDefaults(ptr any) error {
|
||||
// recursing into nested sections. prefix is the section's dotted key, used in
|
||||
// error messages.
|
||||
func applyDefaultsTo(prefix string, section reflect.Value) error {
|
||||
for _, f := range fieldsOf(section.Type()) {
|
||||
key := joinKey(prefix, f.key)
|
||||
if f.section {
|
||||
if err := applyDefaultsTo(key, section.FieldByIndex(f.index)); err != nil {
|
||||
for _, configField := range fieldsOf(section.Type()) {
|
||||
key := joinKey(prefix, configField.key)
|
||||
if configField.section {
|
||||
if err := applyDefaultsTo(key, section.FieldByIndex(configField.index)); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !f.hasDefault {
|
||||
if !configField.hasDefault {
|
||||
continue
|
||||
}
|
||||
if err := setField(section, f, f.def); err != nil {
|
||||
if err := setField(section, configField, configField.def); err != nil {
|
||||
return fmt.Errorf("invalid default for %s: %w", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,9 @@ func TestMapOf(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFlattenOfPrefix(t *testing.T) {
|
||||
m := flattenOf("p", testSection{Dur: 90 * time.Second})
|
||||
m := flattenOf("p", testSection{
|
||||
Dur: 90 * time.Second,
|
||||
})
|
||||
for _, k := range []string{"p.str", "p.on", "p.inner.name"} {
|
||||
if _, ok := m[k]; !ok {
|
||||
t.Errorf("flattenOf missing %q in %v", k, m)
|
||||
@@ -145,14 +147,18 @@ func TestApplyDefaults(t *testing.T) {
|
||||
t.Fatalf("applyDefaults: %v", err)
|
||||
}
|
||||
want := testSection{
|
||||
testEmbedded: testEmbedded{On: true},
|
||||
Str: "hello",
|
||||
Int: -42,
|
||||
Uint: 7,
|
||||
Float: 1.5,
|
||||
Dur: 90 * time.Second,
|
||||
Size: 32 << 10,
|
||||
Inner: testInner{Name: "inner"},
|
||||
testEmbedded: testEmbedded{
|
||||
On: true,
|
||||
},
|
||||
Str: "hello",
|
||||
Int: -42,
|
||||
Uint: 7,
|
||||
Float: 1.5,
|
||||
Dur: 90 * time.Second,
|
||||
Size: 32 << 10,
|
||||
Inner: testInner{
|
||||
Name: "inner",
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(s, want) {
|
||||
t.Errorf("applyDefaults = %+v; want %+v", s, want)
|
||||
@@ -247,6 +253,7 @@ func TestSetValue(t *testing.T) {
|
||||
{"size", "1 mb", "1MB"}, // byte sizes are given out as strings
|
||||
{"size", "1.5KiB", "1536B"},
|
||||
{"size", "4096", "4KiB"},
|
||||
{"size", "-1", "-1"}, // no limit
|
||||
{"slice", " a, b ,, c ", []string{"a", "b", "c"}},
|
||||
{"slice", "", []string(nil)},
|
||||
{"inner.name", "nested", "nested"},
|
||||
@@ -286,8 +293,14 @@ func TestSetValue(t *testing.T) {
|
||||
}
|
||||
got, err := valueOf(s, tt.key)
|
||||
if err != nil || !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("after setValue(%q, %#v): valueOf = %#v, %v; want %#v",
|
||||
tt.key, tt.value, got, err, tt.want)
|
||||
t.Errorf(
|
||||
"after setValue(%q, %#v): valueOf = %#v, %v; want %#v",
|
||||
tt.key,
|
||||
tt.value,
|
||||
got,
|
||||
err,
|
||||
tt.want,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -322,8 +335,13 @@ func TestSetValueErrors(t *testing.T) {
|
||||
before := s
|
||||
err := setValue(&s, tt.key, tt.value)
|
||||
if err == nil || !strings.Contains(err.Error(), tt.want) {
|
||||
t.Errorf("setValue(%q, %#v) = %v; want error containing %q",
|
||||
tt.key, tt.value, err, tt.want)
|
||||
t.Errorf(
|
||||
"setValue(%q, %#v) = %v; want error containing %q",
|
||||
tt.key,
|
||||
tt.value,
|
||||
err,
|
||||
tt.want,
|
||||
)
|
||||
}
|
||||
if !reflect.DeepEqual(s, before) {
|
||||
t.Errorf("setValue(%q, %#v) modified the section on error", tt.key, tt.value)
|
||||
@@ -334,7 +352,9 @@ func TestSetValueErrors(t *testing.T) {
|
||||
// A []string is given out as a copy, so changing it leaves the section as it
|
||||
// was.
|
||||
func TestValueOfSliceIsACopy(t *testing.T) {
|
||||
s := testSection{Slice: []string{"a", "b"}}
|
||||
s := testSection{
|
||||
Slice: []string{"a", "b"},
|
||||
}
|
||||
got, err := valueOf(s, "slice")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
Reference in new issue
Block a user