Skip to content

Commit

Permalink
Merge pull request #209 from jxsl13/fix-issue-200
Browse files Browse the repository at this point in the history
Closes #200
  • Loading branch information
knadh committed Mar 20, 2023
2 parents 94f17ab + aed6c61 commit c77dab6
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 8 deletions.
9 changes: 2 additions & 7 deletions getters.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,10 +491,7 @@ func (ko *Koanf) StringsMap(path string) map[string][]string {
case map[string][]string:
out = make(map[string][]string, len(mp))
for k, v := range mp {
out[k] = make([]string, 0, len(v))
for _, s := range v {
out[k] = append(out[k], s)
}
out[k] = append(out[k], v...)
}
case map[string][]interface{}:
out = make(map[string][]string, len(mp))
Expand All @@ -513,9 +510,7 @@ func (ko *Koanf) StringsMap(path string) map[string][]string {
for k, v := range mp {
switch s := v.(type) {
case []string:
for _, v := range s {
out[k] = append(out[k], v)
}
out[k] = append(out[k], s...)
case []interface{}:
for _, v := range s {
switch sv := v.(type) {
Expand Down
66 changes: 65 additions & 1 deletion koanf.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package koanf

import (
"bytes"
"encoding"
"fmt"
"reflect"
"sort"
"strconv"

Expand Down Expand Up @@ -251,7 +253,7 @@ func (ko *Koanf) UnmarshalWithConf(path string, o interface{}, c UnmarshalConf)
DecodeHook: mapstructure.ComposeDecodeHookFunc(
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","),
mapstructure.TextUnmarshallerHookFunc()),
textUnmarshalerHookFunc()),
Metadata: nil,
Result: o,
WeaklyTypedInput: true,
Expand Down Expand Up @@ -511,3 +513,65 @@ func populateKeyParts(m KeyMap, delim string) KeyMap {
}
return out
}

// textUnmarshalerHookFunc is a fixed version of mapstructure.TextUnmarshallerHookFunc.
// This hook allows to additionally unmarshal text into custom string types that implement the encoding.(Un)TextMarshaler interface(s)
func textUnmarshalerHookFunc() mapstructure.DecodeHookFuncType {
return func(
f reflect.Type,
t reflect.Type,
data interface{}) (interface{}, error) {
if f.Kind() != reflect.String {
return data, nil
}
result := reflect.New(t).Interface()
unmarshaller, ok := result.(encoding.TextUnmarshaler)
if !ok {
return data, nil
}

// default text representaion is the actual value of the `from` string
var (
dataVal = reflect.ValueOf(data)
text = []byte(dataVal.String())
)
if f.Kind() == t.Kind() {
// source and target are of underlying type string
var (
err error
ptrVal = reflect.New(dataVal.Type())
)
if !ptrVal.Elem().CanSet() {
// cannot set, skip, this should not happen
if err := unmarshaller.UnmarshalText(text); err != nil {
return nil, err
}
return result, nil
}
ptrVal.Elem().Set(dataVal)

// We need to assert that both, the value type and the pointer type
// do (not) implement the TextMarshaler interface before proceeding and simmply
// using the the string value of the string type.
// it might be the case that the internal string representation differs from
// the (un)marshalled string.

for _, v := range []reflect.Value{dataVal, ptrVal} {
if marshaller, ok := v.Interface().(encoding.TextMarshaler); ok {
text, err = marshaller.MarshalText()
if err != nil {
return nil, err
}
break
}
}
}

// text is either the source string's value or the source string type's marshaled value
// which may differ fromit internal string value.
if err := unmarshaller.UnmarshalText(text); err != nil {
return nil, err
}
return result, nil
}
}
3 changes: 3 additions & 0 deletions tests/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ replace (
github.com/knadh/koanf/providers/fs => ../providers/fs
github.com/knadh/koanf/providers/posflag => ../providers/posflag
github.com/knadh/koanf/providers/rawbytes => ../providers/rawbytes
github.com/knadh/koanf/providers/structs => ../providers/structs
github.com/knadh/koanf/v2 => ../
)

Expand All @@ -35,13 +36,15 @@ require (
github.com/knadh/koanf/providers/fs v0.0.0-00010101000000-000000000000
github.com/knadh/koanf/providers/posflag v0.0.0-00010101000000-000000000000
github.com/knadh/koanf/providers/rawbytes v0.0.0-00010101000000-000000000000
github.com/knadh/koanf/providers/structs v0.0.0-00010101000000-000000000000
github.com/knadh/koanf/v2 v2.0.0-00010101000000-000000000000
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.1
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hjson/hjson-go/v4 v4.3.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions tests/go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
Expand Down
107 changes: 107 additions & 0 deletions tests/textmarshal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package koanf_test

import (
"encoding"
"fmt"
"strings"
"testing"

"github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/structs"
"github.com/knadh/koanf/v2"
"github.com/stretchr/testify/assert"
)

func TestTextUnmarshalStringFixed(t *testing.T) {
defer func() {
assert.Nil(t, recover())
}()

type targetStruct struct {
LogFormatPointer LogFormatPointer // default should map to json
LogFormatValue LogFormatValue // default should map to json
}

target := &targetStruct{"text_custom", "text_custom"}
before := *target

var (
bptr interface{} = &(target.LogFormatPointer)
cptr interface{} = target.LogFormatValue
)
_, ok := (bptr).(encoding.TextMarshaler)
assert.True(t, ok)

_, ok = (cptr).(encoding.TextMarshaler)
assert.True(t, ok)

k := koanf.New(".")
k.Load(structs.Provider(target, "koanf"), nil)

k.Load(env.Provider("", ".", func(s string) string {
return strings.Replace(strings.ToLower(s), "_", ".", -1)
}), nil)

// default values
err := k.Unmarshal("", &target)
assert.NoError(t, err)
assert.Equal(t, &before, target)
}

// LogFormatValue is a custom string type that implements the TextUnmarshaler interface
// Additionally it implements the TextMarshaler interface (value receiver)
type LogFormatValue string

// pointer receiver
func (c *LogFormatValue) UnmarshalText(data []byte) error {
switch strings.ToLower(string(data)) {
case "", "json":
*c = "json_custom"
case "text":
*c = "text_custom"
default:
return fmt.Errorf("invalid log format: %s", string(data))
}
return nil
}

// value receiver
func (c LogFormatValue) MarshalText() ([]byte, error) {
//overcomplicated custom internal string representation
switch c {
case "", "json_custom":
return []byte("json"), nil
case "text_custom":
return []byte("text"), nil
}
return nil, fmt.Errorf("invalid internal string representation: %q", c)
}

// LogFormatPointer is a custom string type that implements the TextUnmarshaler interface
// Additionally it implements the TextMarshaler interface (pointer receiver)
type LogFormatPointer string

// pointer receiver
func (c *LogFormatPointer) UnmarshalText(data []byte) error {
switch strings.ToLower(string(data)) {
case "", "json":
*c = "json_custom"
case "text":
*c = "text_custom"
default:
return fmt.Errorf("invalid log format: %s", string(data))
}
return nil
}

// also pointer receiver
func (c *LogFormatPointer) MarshalText() ([]byte, error) {
//overcomplicated custom internal string representation
switch *c {
case "", "json_custom":
return []byte("json"), nil
case "text_custom":
return []byte("text"), nil
}
return nil, fmt.Errorf("invalid internal string representation: %q", *c)
}

0 comments on commit c77dab6

Please sign in to comment.