Refactored configuration to make sections less code intensive to add
This commit is contained in:
1 parent
8b0cecb51b
commit
ff33ef082a
4 files changed
+438
-94
No files matched your search
@@ -22,76 +22,48 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const JSCKLogging string = "logging"
|
||||||
|
|
||||||
type JobSearchConfigLogging struct {
|
type JobSearchConfigLogging struct {
|
||||||
Level string
|
Level string `cfg:"level" default:"warn"`
|
||||||
File string
|
File string `cfg:"file"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default configuration
|
/*
|
||||||
func defaultConfigLogging() JobSearchConfigLogging {
|
* General section functions
|
||||||
return JobSearchConfigLogging{
|
* See related methods in section.go
|
||||||
Level: "warn",
|
*/
|
||||||
}
|
func (c *JobSearchConfigLogging) Keys() []string { return keysOf(c) }
|
||||||
|
|
||||||
|
func (c JobSearchConfigLogging) Map() map[string]any { return mapOf(c) }
|
||||||
|
|
||||||
|
func (c JobSearchConfigLogging) Flatten() map[string]any {
|
||||||
|
return flattenOf(JSCKLogging, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converts configuration to a map
|
|
||||||
func (conf JobSearchConfigLogging) Map() map[string]any {
|
|
||||||
return map[string]any{
|
|
||||||
"level": conf.Level,
|
|
||||||
"file": conf.File,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Converts configuration to a flat dotted format
|
|
||||||
func (conf JobSearchConfigLogging) Flatten() map[string]any {
|
|
||||||
confMap := conf.Map()
|
|
||||||
retMap := map[string]any{}
|
|
||||||
for k, v := range confMap {
|
|
||||||
retMap["logging."+k] = v
|
|
||||||
}
|
|
||||||
return retMap
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the expected type of a configuration key value
|
|
||||||
func (c JobSearchConfigLogging) TypeOf(key string) (reflect.Kind, error) {
|
func (c JobSearchConfigLogging) TypeOf(key string) (reflect.Kind, error) {
|
||||||
switch key {
|
return typeOf(c, key)
|
||||||
case "level":
|
|
||||||
return reflect.String, nil
|
|
||||||
case "file":
|
|
||||||
return reflect.String, nil
|
|
||||||
default:
|
|
||||||
return reflect.Invalid, errors.New("unknown configuration key")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the value of a configuration key
|
|
||||||
func (c JobSearchConfigLogging) ValueOf(key string) (any, error) {
|
func (c JobSearchConfigLogging) ValueOf(key string) (any, error) {
|
||||||
switch key {
|
return valueOf(c, key)
|
||||||
case "level":
|
|
||||||
return c.Level, nil
|
|
||||||
case "file":
|
|
||||||
return c.File, nil
|
|
||||||
default:
|
|
||||||
return "", errors.New("unknown configuration key")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets the value of a configuration key
|
|
||||||
func (c *JobSearchConfigLogging) SetValue(key string, value any) error {
|
func (c *JobSearchConfigLogging) SetValue(key string, value any) error {
|
||||||
|
return setValue(c, key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section specific functions */
|
||||||
|
// Checks values before they are stored (see validator)
|
||||||
|
func (c *JobSearchConfigLogging) validate(key string, value any) error {
|
||||||
switch key {
|
switch key {
|
||||||
case "level":
|
case "level":
|
||||||
newLevel := value.(string)
|
switch value.(string) {
|
||||||
switch newLevel {
|
|
||||||
case "debug", "info", "warn", "error":
|
case "debug", "info", "warn", "error":
|
||||||
c.Level = value.(string)
|
|
||||||
default:
|
default:
|
||||||
return errors.New(
|
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")
|
||||||
}
|
}
|
||||||
case "file":
|
|
||||||
c.File = value.(string)
|
|
||||||
default:
|
|
||||||
return errors.New("unknown configuration key")
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-43
@@ -18,9 +18,8 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"log/slog"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -31,65 +30,37 @@ import (
|
|||||||
const EnvPrefix string = "JOBSEARCH"
|
const EnvPrefix string = "JOBSEARCH"
|
||||||
const EtcConfigPath string = "/etc/job-search"
|
const EtcConfigPath string = "/etc/job-search"
|
||||||
|
|
||||||
|
// Each field's cfg tag must match the section's JSCK* constant.
|
||||||
type JobSearchConfig struct {
|
type JobSearchConfig struct {
|
||||||
Logging JobSearchConfigLogging
|
Logging JobSearchConfigLogging `cfg:"logging"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default configuration
|
// Default configuration, taken from the default struct tags
|
||||||
func LoadDefaultConfig() {
|
func LoadDefaultConfig() {
|
||||||
conf := JobSearchConfig{
|
var conf JobSearchConfig
|
||||||
Logging: defaultConfigLogging(),
|
if err := applyDefaults(&conf); err != nil {
|
||||||
|
panic(fmt.Sprintf("config: %v", err))
|
||||||
}
|
}
|
||||||
LoadToViper(conf)
|
LoadToViper(conf)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converts configuration to a map
|
func (c JobSearchConfig) Map() map[string]any { return mapOf(c) }
|
||||||
func (conf JobSearchConfig) Map() map[string]any {
|
|
||||||
confMap := map[string]any{}
|
|
||||||
confMap["logging"] = conf.Logging.Map()
|
|
||||||
return confMap
|
|
||||||
}
|
|
||||||
|
|
||||||
// Converts configuration to a flat dotted format
|
func (c JobSearchConfig) Flatten() map[string]any { return flattenOf("", c) }
|
||||||
func (conf JobSearchConfig) Flatten() map[string]any {
|
|
||||||
confFlat := map[string]any{}
|
|
||||||
maps.Copy(confFlat, conf.Logging.Flatten())
|
|
||||||
return confFlat
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the expected type of a configuration key value
|
|
||||||
func (c JobSearchConfig) TypeOf(key string) (reflect.Kind, error) {
|
func (c JobSearchConfig) TypeOf(key string) (reflect.Kind, error) {
|
||||||
keyParts := strings.Split(key, ".")
|
return typeOf(c, key)
|
||||||
switch keyParts[0] {
|
|
||||||
case "logging":
|
|
||||||
return c.Logging.TypeOf(strings.Join(keyParts[1:], "."))
|
|
||||||
default:
|
|
||||||
return reflect.Invalid, errors.New("unknown configuration key")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the expected type of a configuration value
|
|
||||||
func (c JobSearchConfig) ValueOf(key string) (any, error) {
|
func (c JobSearchConfig) ValueOf(key string) (any, error) {
|
||||||
keyParts := strings.Split(key, ".")
|
return valueOf(c, key)
|
||||||
switch keyParts[0] {
|
|
||||||
case "logging":
|
|
||||||
return c.Logging.ValueOf(strings.Join(keyParts[1:], "."))
|
|
||||||
default:
|
|
||||||
return nil, errors.New("unknown configuration key")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the expected type of a configuration value
|
|
||||||
func (c *JobSearchConfig) SetValue(key string, value any) error {
|
func (c *JobSearchConfig) SetValue(key string, value any) error {
|
||||||
keyParts := strings.Split(key, ".")
|
return setValue(c, key, value)
|
||||||
switch keyParts[0] {
|
|
||||||
case "logging":
|
|
||||||
return c.Logging.SetValue(strings.Join(keyParts[1:], "."), value)
|
|
||||||
default:
|
|
||||||
return errors.New("unknown configuration key")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Utility method for loading a configuration from Viper
|
||||||
func LoadFromViper(conf *JobSearchConfig) {
|
func LoadFromViper(conf *JobSearchConfig) {
|
||||||
keys := viper.AllKeys()
|
keys := viper.AllKeys()
|
||||||
for idx := range keys {
|
for idx := range keys {
|
||||||
@@ -105,10 +76,13 @@ func LoadFromViper(conf *JobSearchConfig) {
|
|||||||
conf.SetValue(key, viper.GetInt(key))
|
conf.SetValue(key, viper.GetInt(key))
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
conf.SetValue(key, viper.GetBool(key))
|
conf.SetValue(key, viper.GetBool(key))
|
||||||
|
default:
|
||||||
|
slog.Error(fmt.Sprintf("unknown key type: %s", keyType))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Utility method for loading a configuration into Viper as defaults
|
||||||
func LoadToViper(conf JobSearchConfig) {
|
func LoadToViper(conf JobSearchConfig) {
|
||||||
flatMap := conf.Flatten()
|
flatMap := conf.Flatten()
|
||||||
for key := range flatMap {
|
for key := range flatMap {
|
||||||
@@ -121,7 +95,7 @@ func (conf JobSearchConfig) String() string {
|
|||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for k, v := range conf.Flatten() {
|
for k, v := range conf.Flatten() {
|
||||||
typ, _ := conf.TypeOf(k)
|
typ, _ := conf.TypeOf(k)
|
||||||
sb.WriteString(fmt.Sprint(k, " = ", v, " <", typ, ">"))
|
sb.WriteString(fmt.Sprint(k, " = ", v, " <", typ, ">\n"))
|
||||||
}
|
}
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2026 Craig Williams <craig@avsdev.uk>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Kinds that LoadFromViper knows how to read.
|
||||||
|
var viperKinds = map[reflect.Kind]bool{
|
||||||
|
reflect.String: true, reflect.Int: true, reflect.Bool: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every key round-trips through TypeOf/ValueOf/SetValue and has a type that
|
||||||
|
// LoadFromViper can read.
|
||||||
|
func TestKeysRoundTrip(t *testing.T) {
|
||||||
|
var conf JobSearchConfig
|
||||||
|
if err := applyDefaults(&conf); err != nil {
|
||||||
|
t.Fatalf("defaults: %v", err)
|
||||||
|
}
|
||||||
|
for key, want := range conf.Flatten() {
|
||||||
|
kind, err := conf.TypeOf(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("TypeOf(%q): %v", key, err)
|
||||||
|
} else if !viperKinds[kind] {
|
||||||
|
t.Errorf("TypeOf(%q) = %v, which LoadFromViper does not handle", key, kind)
|
||||||
|
}
|
||||||
|
got, err := conf.ValueOf(key)
|
||||||
|
if err != nil || !reflect.DeepEqual(got, want) {
|
||||||
|
t.Errorf("ValueOf(%q) = %#v, %v; want %#v", key, got, err, want)
|
||||||
|
}
|
||||||
|
if err := conf.SetValue(key, got); err != nil {
|
||||||
|
t.Errorf("SetValue(%q, %#v): %v", key, got, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every section has the full method set, and its Flatten keys (which use the
|
||||||
|
// section's JSCK* constant) line up with its cfg tag in the parent.
|
||||||
|
func TestSections(t *testing.T) {
|
||||||
|
var conf JobSearchConfig
|
||||||
|
all := conf.Flatten()
|
||||||
|
checkSections(t, all, "", reflect.ValueOf(&conf).Elem())
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkSections(t *testing.T, all map[string]any, prefix string, section reflect.Value) {
|
||||||
|
for _, f := range fieldsOf(section.Type()) {
|
||||||
|
if !f.section {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fv := section.FieldByIndex(f.index)
|
||||||
|
name := fv.Type().Name()
|
||||||
|
for _, m := range []string{"Map", "Flatten", "TypeOf", "ValueOf", "SetValue"} {
|
||||||
|
if !fv.Addr().MethodByName(m).IsValid() {
|
||||||
|
t.Errorf("%s is missing method %s", name, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if flatten := fv.MethodByName("Flatten"); flatten.IsValid() {
|
||||||
|
keys := flatten.Call(nil)[0].Interface().(map[string]any)
|
||||||
|
for k := range keys {
|
||||||
|
if _, ok := all[joinKey(prefix, k)]; !ok {
|
||||||
|
t.Errorf("%s.Flatten() key %q does not match its cfg tag %q",
|
||||||
|
name, k, joinKey(prefix, f.key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkSections(t, all, joinKey(prefix, f.key), fv)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2026 Craig Williams <craig@avsdev.uk>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Configuration sections are plain structs described with struct tags:
|
||||||
|
//
|
||||||
|
// cfg:"key" the field's configuration key. Fields without it are
|
||||||
|
// not part of the configuration.
|
||||||
|
// default:"value" the field's default, parsed the same way as a string
|
||||||
|
// passed to SetValue (e.g. "10m", "true", "42").
|
||||||
|
//
|
||||||
|
// A tagged field whose type is a struct (other than time.Time) is a nested
|
||||||
|
// section, addressed as "section.key". An embedded struct has its tagged
|
||||||
|
// fields promoted into the enclosing section, just as Go promotes fields.
|
||||||
|
//
|
||||||
|
// time.Duration fields report reflect.String from TypeOf and accept either a
|
||||||
|
// time.Duration or a duration string such as "90s" in SetValue.
|
||||||
|
//
|
||||||
|
// A section can check values before they are stored by implementing
|
||||||
|
// validator on its pointer type.
|
||||||
|
const (
|
||||||
|
keyTag = "cfg"
|
||||||
|
defaultTag = "default"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errUnknownKey = errors.New("unknown configuration key")
|
||||||
|
durationType = reflect.TypeOf(time.Duration(0))
|
||||||
|
timeType = reflect.TypeOf(time.Time{})
|
||||||
|
)
|
||||||
|
|
||||||
|
// validator is implemented by sections that need to reject some values.
|
||||||
|
// key is the field's own key within the section; value has already been
|
||||||
|
// converted to the field's type.
|
||||||
|
type validator interface {
|
||||||
|
validate(key string, value any) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type field struct {
|
||||||
|
key string
|
||||||
|
index []int // path for reflect.Value.FieldByIndex
|
||||||
|
typ reflect.Type
|
||||||
|
section bool
|
||||||
|
def string
|
||||||
|
hasDefault bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key, ok := sf.Tag.Lookup(keyTag)
|
||||||
|
if !ok || key == "" || key == "-" || !sf.IsExported() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
def, hasDefault := sf.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,
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
if rv.Kind() != reflect.Struct {
|
||||||
|
panic(fmt.Sprintf("config: %T is not a configuration section", v))
|
||||||
|
}
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case f.section && nested:
|
||||||
|
return lookup(section.FieldByIndex(f.index), rest)
|
||||||
|
case !f.section && !nested:
|
||||||
|
return section, f, nil
|
||||||
|
}
|
||||||
|
// Key matched but the shape didn't: a section with no sub-key, or a
|
||||||
|
// value with one.
|
||||||
|
return reflect.Value{}, field{}, errUnknownKey
|
||||||
|
}
|
||||||
|
return reflect.Value{}, field{}, errUnknownKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinKey(prefix, key string) string {
|
||||||
|
if prefix == "" {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
return prefix + "." + key
|
||||||
|
}
|
||||||
|
|
||||||
|
// keysOf returns the keys directly in a section.
|
||||||
|
func keysOf(v any) []string {
|
||||||
|
fields := fieldsOf(structValue(v).Type())
|
||||||
|
keys := make([]string, 0, len(fields))
|
||||||
|
for _, f := range fields {
|
||||||
|
keys = append(keys, f.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 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)
|
||||||
|
} else {
|
||||||
|
m[f.key] = fv.Interface()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 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)
|
||||||
|
} else {
|
||||||
|
m[key] = fv.Interface()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// kindOf is the kind reported by TypeOf, i.e. how the value is read from
|
||||||
|
// Viper. Durations are read as strings and parsed by SetValue.
|
||||||
|
func kindOf(t reflect.Type) reflect.Kind {
|
||||||
|
if t == durationType {
|
||||||
|
return reflect.String
|
||||||
|
}
|
||||||
|
return t.Kind()
|
||||||
|
}
|
||||||
|
|
||||||
|
func typeOf(v any, key string) (reflect.Kind, error) {
|
||||||
|
_, f, err := lookup(structValue(v), key)
|
||||||
|
if err != nil {
|
||||||
|
return reflect.Invalid, err
|
||||||
|
}
|
||||||
|
return kindOf(f.typ), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func valueOf(v any, key string) (any, error) {
|
||||||
|
section, f, err := lookup(structValue(v), key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return section.FieldByIndex(f.index).Interface(), 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)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return setField(section, f, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setField(section reflect.Value, f field, value any) error {
|
||||||
|
newValue, err := convert(value, f.typ)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if v, ok := section.Addr().Interface().(validator); ok {
|
||||||
|
if err := v.validate(f.key, newValue.Interface()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
section.FieldByIndex(f.index).Set(newValue)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert turns value into a value of type t. Values already of the right
|
||||||
|
// type are used as-is; strings are parsed.
|
||||||
|
func convert(value any, t reflect.Type) (reflect.Value, error) {
|
||||||
|
if value != nil {
|
||||||
|
rv := reflect.ValueOf(value)
|
||||||
|
if rv.Type().AssignableTo(t) {
|
||||||
|
return rv, nil
|
||||||
|
}
|
||||||
|
if s, ok := value.(string); ok {
|
||||||
|
return parseString(s, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reflect.Value{}, fmt.Errorf("unsupported type %T", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseString(s string, t reflect.Type) (reflect.Value, error) {
|
||||||
|
v := reflect.New(t).Elem()
|
||||||
|
if t == durationType {
|
||||||
|
d, err := time.ParseDuration(s)
|
||||||
|
if err != nil {
|
||||||
|
return reflect.Value{}, fmt.Errorf("unable to convert to duration: %w", err)
|
||||||
|
}
|
||||||
|
v.SetInt(int64(d))
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
v.SetString(s)
|
||||||
|
case reflect.Bool:
|
||||||
|
var b bool
|
||||||
|
b, err = strconv.ParseBool(s)
|
||||||
|
v.SetBool(b)
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
var n float64
|
||||||
|
n, err = strconv.ParseFloat(s, t.Bits())
|
||||||
|
v.SetFloat(n)
|
||||||
|
default:
|
||||||
|
return reflect.Value{}, fmt.Errorf("unsupported type string for %s", t)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return reflect.Value{}, fmt.Errorf("unable to convert %q to %s: %w", s, t, err)
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyDefaults sets every field that has a default tag, recursing into
|
||||||
|
// nested sections. ptr must point to a section struct.
|
||||||
|
func applyDefaults(ptr any) error {
|
||||||
|
return applyDefaultsTo("", structValue(ptr))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !f.hasDefault {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := setField(section, f, f.def); err != nil {
|
||||||
|
return fmt.Errorf("invalid default for %s: %w", key, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in new issue
Block a user