Skip to content

Commit

Permalink
Parse fields that implement TextUnmarshaller interface (#148)
Browse files Browse the repository at this point in the history
Right now we can parse only bools, ints and strings. This change allows to convert strings to types that implement encoding.TextUnmarshaller interface, e.g. time.Time or math/big.Int
  • Loading branch information
alsamylkin committed Jan 4, 2017
1 parent cbef20b commit f82f109
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 8 deletions.
17 changes: 17 additions & 0 deletions config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,20 @@ fmt.Println(m.World)

Note that any fields you wish to deserialize into must be exported, just like
`json.Unmarshal` and friends.


### Benchmarks

Current performance benchmark data:

```
BenchmarkYAMLCreateSingleFile-8 50000 31317 ns/op 10832 B/op 121 allocs/op
BenchmarkYAMLCreateMultiFile-8 30000 51650 ns/op 19840 B/op 207 allocs/op
BenchmarkYAMLSimpleGetLevel1-8 50000000 26.7 ns/op 0 B/op 0 allocs/op
BenchmarkYAMLSimpleGetLevel3-8 50000000 26.4 ns/op 0 B/op 0 allocs/op
BenchmarkYAMLSimpleGetLevel7-8 50000000 26.0 ns/op 0 B/op 0 allocs/op
BenchmarkYAMLPopulateStruct-8 2000000 860 ns/op 192 B/op 10 allocs/op
BenchmarkYAMLPopulateStructNested-8 500000 2615 ns/op 632 B/op 34 allocs/op
BenchmarkYAMLPopulateStructNestedMultipleFiles-8 500000 3585 ns/op 816 B/op 45 allocs/op
BenchmarkYAMLPopulateNestedTextUnmarshaler-8 100000 17016 ns/op 3217 B/op 209 allocs/op
```
3 changes: 2 additions & 1 deletion config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,8 @@ func TestNestedStructs(t *testing.T) {
v := provider.Get(Root)

assert.True(t, v.HasValue())
v.PopulateStruct(str)
err := v.PopulateStruct(str)
assert.Nil(t, err)

assert.Equal(t, 1234, str.ID)
assert.Equal(t, 999, str.NestedPtr.ID1)
Expand Down
14 changes: 14 additions & 0 deletions config/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,5 +192,19 @@
// Note that any fields you wish to deserialize into must be exported, just like
// json.Unmarshal and friends.
//
// Benchmarks
//
// Current performance benchmark data:
//
// BenchmarkYAMLCreateSingleFile-8 50000 31317 ns/op 10832 B/op 121 allocs/op
// BenchmarkYAMLCreateMultiFile-8 30000 51650 ns/op 19840 B/op 207 allocs/op
// BenchmarkYAMLSimpleGetLevel1-8 50000000 26.7 ns/op 0 B/op 0 allocs/op
// BenchmarkYAMLSimpleGetLevel3-8 50000000 26.4 ns/op 0 B/op 0 allocs/op
// BenchmarkYAMLSimpleGetLevel7-8 50000000 26.0 ns/op 0 B/op 0 allocs/op
// BenchmarkYAMLPopulateStruct-8 2000000 860 ns/op 192 B/op 10 allocs/op
// BenchmarkYAMLPopulateStructNested-8 500000 2615 ns/op 632 B/op 34 allocs/op
// BenchmarkYAMLPopulateStructNestedMultipleFiles-8 500000 3585 ns/op 816 B/op 45 allocs/op
// BenchmarkYAMLPopulateNestedTextUnmarshaler-8 100000 17016 ns/op 3217 B/op 209 allocs/op
//
//
package config
13 changes: 13 additions & 0 deletions config/testdata/textUnmarshaller.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
episodes:
- hero: Scrooge
- hero: LaunchpadMcQuack
- hero: LaunchpadMcQuack
- hero: LaunchpadMcQuack
- hero: Scrooge
- hero: Scrooge
- hero: Scrooge
- hero: LaunchpadMcQuack
- hero: LaunchpadMcQuack
- hero: LaunchpadMcQuack
- hero: Scrooge
- hero: Scrooge
15 changes: 11 additions & 4 deletions config/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
package config

import (
"encoding"
"fmt"
"reflect"
"strconv"
Expand Down Expand Up @@ -337,11 +338,17 @@ func convertValue(value interface{}, targetType reflect.Type) (interface{}, erro
}
switch v := value.(type) {
case string:
switch targetType.Name() {
case "int":
target := reflect.New(targetType).Interface()
switch t := target.(type) {
case *int:
return strconv.Atoi(v)
case "bool":
return v == "True" || v == "true", nil
case *bool:
return strconv.ParseBool(v)
case encoding.TextUnmarshaler:
err := t.UnmarshalText([]byte(v))

// target should have a pointer receiver to be able to change itself based on text
return reflect.ValueOf(target).Elem().Interface(), err
}
}

Expand Down
22 changes: 21 additions & 1 deletion config/yaml_benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@

package config

import "testing"
import (
"testing"
)

func BenchmarkYAMLCreateSingleFile(b *testing.B) {
for n := 0; n < b.N; n++ {
Expand Down Expand Up @@ -129,6 +131,24 @@ func BenchmarkYAMLPopulateStructNestedMultipleFiles(b *testing.B) {
}
}

func BenchmarkYAMLPopulateNestedTextUnmarshaler(b *testing.B) {
type protagonist struct {
Hero duckTaleCharacter
}

type series struct {
Episodes []protagonist
}

p := NewYAMLProviderFromFiles(true, NewRelativeResolver("./testdata"), "textUnmarshaller.yaml")
s := &series{}
b.ResetTimer()

for n := 0; n < b.N; n++ {
p.Get(Root).PopulateStruct(s)
}
}

func providerOneFile() Provider {
return NewYAMLProviderFromFiles(false, NewRelativeResolver("./testdata"), "benchmark1.yaml")
}
Expand Down
19 changes: 18 additions & 1 deletion config/yaml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,6 @@ func TestDurationParsing(t *testing.T) {
ds := durationStruct{}
err := provider.Get("durationStruct").PopulateStruct(&ds)
assert.NoError(t, err)
assert.NoError(t, err)
assert.Equal(t, 10*time.Second, ds.Seconds)
assert.Equal(t, 20*time.Minute, ds.Minutes)
assert.Equal(t, 30*time.Hour, ds.Hours)
Expand All @@ -241,3 +240,21 @@ func TestTypeOfTypes(t *testing.T) {
assert.Equal(t, userDefinedTypeFloat(123.456), *tts.TypeStruct.TestFloat)
})
}

func TestHappyTextUnMarshallerParsing(t *testing.T) {
withYamlBytes(t, happyTextUnmarshallerYaml, func(provider Provider) {
ds := duckTales{}
err := provider.Get("duckTales").PopulateStruct(&ds)
assert.NoError(t, err)
assert.Equal(t, scrooge, ds.Protagonist)
assert.Equal(t, launchpadMcQuack, ds.Pilot)
})
}

func TestGrumpyTextUnMarshallerParsing(t *testing.T) {
withYamlBytes(t, grumpyTextUnmarshallerYaml, func(provider Provider) {
ds := duckTales{}
err := provider.Get("darkwingDuck").PopulateStruct(&ds)
assert.EqualError(t, err, "Unknown character: DarkwingDuck")
})
}
41 changes: 40 additions & 1 deletion config/yaml_test_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@

package config

import "time"
import (
"errors"
"time"
)

type emptystruct struct {
Slice []string
Expand Down Expand Up @@ -142,3 +145,39 @@ typeStruct:
testUInt: 456
testFloat: 123.456
`)

var happyTextUnmarshallerYaml = []byte(`
duckTales:
protagonist: Scrooge
pilot: LaunchpadMcQuack
`)

var grumpyTextUnmarshallerYaml = []byte(`
darkwingDuck:
protagonist: DarkwingDuck
`)

type duckTaleCharacter int

func (d *duckTaleCharacter) UnmarshalText(text []byte) error {
switch string(text) {
case "Scrooge":
*d = scrooge
return nil
case "LaunchpadMcQuack":
*d = launchpadMcQuack
return nil
}

return errors.New("Unknown character: " + string(text))
}

const (
scrooge duckTaleCharacter = iota
launchpadMcQuack
)

type duckTales struct {
Protagonist duckTaleCharacter
Pilot duckTaleCharacter
}

0 comments on commit f82f109

Please sign in to comment.