Skip to content

Commit

Permalink
improved collection parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
Anton Mashko authored and Anton Mashko committed Sep 26, 2023
1 parent f71e364 commit 6867d15
Show file tree
Hide file tree
Showing 15 changed files with 362 additions and 151 deletions.
13 changes: 12 additions & 1 deletion error.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,16 @@ type Error struct {
}

func (e *Error) Error() string {
return fmt.Sprintf("%s '%s'. %s", e.Message, e.FieldName, e.Inner)
msg := e.Message
if e.FieldName != "" {
msg = fmt.Sprintf("%s: %s", e.FieldName, msg)
}
if e.Inner != nil {
msg += " " + e.Inner.Error()
}
return msg
}

func (e *Error) Unwrap() error {
return e.Inner
}
88 changes: 64 additions & 24 deletions external.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package envconf
import (
"fmt"
"reflect"
"strconv"
"strings"
"unicode"
)
Expand Down Expand Up @@ -39,7 +40,7 @@ func newExternalConfig(ext External) *externalConfig {
}
}

func (c *externalConfig) unmarshal(rf reflect.Type, v interface{}) error {
func (c *externalConfig) unmarshal(v interface{}) error {
if c.ext == (emptyExt{}) {
return nil
}
Expand All @@ -52,13 +53,37 @@ func (c *externalConfig) unmarshal(rf reflect.Type, v interface{}) error {
if err != nil {
return err
}
c.data, err = c.normalizeMap(rf, mp)
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Pointer {

Check failure on line 57 in external.go

View workflow job for this annotation

GitHub Actions / build

undefined: reflect.Pointer

Check failure on line 57 in external.go

View workflow job for this annotation

GitHub Actions / build

undefined: reflect.Pointer
rv = rv.Elem()
}
c.data, err = c.normalizeMap(rv, mp)
if err != nil {
return err
}
return nil
}

func (c *externalConfig) readFrom(key string, ic interface{}) (interface{}, bool) {
switch vt := ic.(type) {
case map[string]interface{}:
var ok bool
ic, ok = vt[key]
return ic, ok
case []interface{}:
idx, err := strconv.Atoi(key)
if err != nil {
return nil, false
}
if idx >= len(vt) {
return nil, false
}
return vt[idx], true
default:
return nil, false
}
}

func (c *externalConfig) get(f field) (interface{}, bool) {
if c.ext == (emptyExt{}) {
return nil, false
Expand All @@ -71,18 +96,21 @@ func (c *externalConfig) get(f field) (interface{}, bool) {

// ignoring top level struct
path = path[1:]
var mp map[string]interface{} = c.data
if len(path) > 1 {
for i := 0; i < len(path)-1; i++ {
mp = mp[path[i].Name].(map[string]interface{})
var ic interface{} = c.data
var ok bool
for i := 0; i < len(path); i++ {
ic, ok = c.readFrom(path[i].Name, ic)
if !ok {
return nil, false
}
if ok && i == len(path)-1 {
return ic, true
}
}

v, ok := mp[path[len(path)-1].Name]
return v, ok
return nil, false
}

func (c *externalConfig) normalizeMap(rt reflect.Type, mp map[string]interface{}) (map[string]interface{}, error) {
func (c *externalConfig) normalizeMap(rv reflect.Value, mp map[string]interface{}) (map[string]interface{}, error) {
result := make(map[string]interface{})
for k, v := range mp {
var fr rune
Expand All @@ -92,25 +120,27 @@ func (c *externalConfig) normalizeMap(rt reflect.Type, mp map[string]interface{}
}
lc := unicode.IsLower(fr)
// normalizing names(keys) in map
for i := 0; i < rt.NumField(); i++ {
f := rt.Field(i)
if !c.equal(k, lc, f) {
rt := rv.Type()
for i := 0; i < rv.NumField(); i++ {
sf := rt.Field(i)
f := rv.Field(i)
if !c.equal(k, lc, sf) {
continue
}
val, err := c.normalize(f.Type, v)
val, err := c.normalize(f, v)
if err != nil {
return nil, err
}
result[f.Name] = val
result[sf.Name] = val
break
}
}
return result, nil
}

func (c *externalConfig) normalizeSlice(rt reflect.Type, sl []interface{}) ([]interface{}, error) {
func (c *externalConfig) normalizeSlice(rv reflect.Value, sl []interface{}) ([]interface{}, error) {
for i := range sl {
v, err := c.normalize(rt, sl[i])
v, err := c.normalize(rv.Index(i), sl[i])
if err != nil {
return nil, err
}
Expand All @@ -119,26 +149,36 @@ func (c *externalConfig) normalizeSlice(rt reflect.Type, sl []interface{}) ([]in
return sl, nil
}

func (c *externalConfig) normalize(rf reflect.Type, v interface{}) (interface{}, error) {
func (c *externalConfig) normalize(rv reflect.Value, v interface{}) (interface{}, error) {
switch vt := v.(type) {
case map[string]interface{}:
switch rf.Kind() {
switch rv.Kind() {
case reflect.Map:
return vt, nil
case reflect.Struct:
return c.normalizeMap(rf, vt)
return c.normalizeMap(rv, vt)
case reflect.Interface:
if rv.IsValid() && !rv.IsZero() {
return c.normalize(rv.Elem(), v)
}
return vt, nil
case reflect.Pointer:

Check failure on line 165 in external.go

View workflow job for this annotation

GitHub Actions / build

undefined: reflect.Pointer

Check failure on line 165 in external.go

View workflow job for this annotation

GitHub Actions / build

undefined: reflect.Pointer
if rv.IsValid() && !rv.IsZero() {
return c.normalize(rv.Elem(), v)
}
return vt, nil
default:
return nil, &Error{
Message: fmt.Sprint("unable to cast map[string]interface{} into ", rf.String()),
Message: fmt.Sprint("unable to cast map[string]interface{} into ", rv.Type().Name()),
}
}
case []interface{}:
switch rf.Kind() {
switch rv.Kind() {
case reflect.Slice, reflect.Array:
return c.normalizeSlice(rf.Elem(), vt)
return c.normalizeSlice(rv, vt)
default:
return nil, &Error{
Message: fmt.Sprint("unable to cast []interface{} into ", rf.String()),
Message: fmt.Sprint("unable to cast []interface{} into ", rv.Type().String()),
}
}
default:
Expand Down
10 changes: 10 additions & 0 deletions external/test/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module github.com/antonmashko/envconf/external/test

go 1.21.0

require (
github.com/antonmashko/envconf v1.3.3
github.com/antonmashko/envconf/external/yaml v0.0.0-20230925193500-f71e3643e0cb
)

require gopkg.in/yaml.v3 v3.0.1 // indirect
8 changes: 8 additions & 0 deletions external/test/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
github.com/antonmashko/envconf v1.3.3 h1:HPyAW3XWG88ldhIOCCKK8Y4mu8xFeiKPfpgPa4Y+1e4=
github.com/antonmashko/envconf v1.3.3/go.mod h1:BJSRjk4g3ERM7wGlWmVHYU5hxI4y8w0XdMjCa0/wwSc=
github.com/antonmashko/envconf/external/yaml v0.0.0-20230925193500-f71e3643e0cb h1:RuCp6SCRQiHoXSSffNioIKFgi3Nq5q4Ya7e/5mbh1l0=
github.com/antonmashko/envconf/external/yaml v0.0.0-20230925193500-f71e3643e0cb/go.mod h1:gspdgcVUFVPKgkdTzLVJkwgkhWLLzKix8sET9e5jc1k=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
8 changes: 8 additions & 0 deletions external/test/implementation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package test

import (
"github.com/antonmashko/envconf"
"github.com/antonmashko/envconf/external/yaml"
)

var _ envconf.External = (yaml.Yaml)([]byte{})
4 changes: 2 additions & 2 deletions external/yaml/yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import "gopkg.in/yaml.v3"

type Yaml []byte

func (y Yaml) TagName() string {
return "yaml"
func (y Yaml) TagName() []string {
return []string{"yaml"}
}

func (y Yaml) Unmarshal(v interface{}) error {
Expand Down
4 changes: 2 additions & 2 deletions external_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func TestExternal_newExternalConfig_Ok(t *testing.T) {
if ext == nil {
t.Fail()
}
if ext.unmarshal(nil, nil) != nil {
if ext.unmarshal(nil) != nil {
t.Error("unexpected result")
}
}
Expand All @@ -28,7 +28,7 @@ func TestExternal_InvalidJson_Err(t *testing.T) {
tc := struct {
Foo int
}{}
if ext.unmarshal(reflect.TypeOf(tc), &tc) == nil {
if ext.unmarshal(&tc) == nil {
t.Error("unexpected error got nil")
}
}
34 changes: 21 additions & 13 deletions field.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,31 +46,39 @@ func (emptyField) structField() reflect.StructField {
return reflect.StructField{}
}

func createFieldFromValue(v reflect.Value, p *structType, t reflect.StructField) field {
func createFieldFromValue(v reflect.Value, p field, t reflect.StructField, parser *EnvConf) field {
if v.Kind() == reflect.Pointer {

Check failure on line 50 in field.go

View workflow job for this annotation

GitHub Actions / build

undefined: reflect.Pointer

Check failure on line 50 in field.go

View workflow job for this annotation

GitHub Actions / build

undefined: reflect.Pointer
return newPtrType(v, p, t, parser)
}
// validate reflect value
if !v.CanInterface() {
return emptyField{}
}

// implementations check
implF := asImpl(v)
if implF != nil {
return newFieldType(v, p, t, parser.external, parser.PriorityOrder())
}

switch v.Kind() {
case reflect.Struct:
// implementations check
implF := asImpl(v)
if implF != nil {
return newFieldType(v, p, t)
}
return newStructType(v, p, t)
case reflect.Ptr:
return newPtrType(v, p, t)
return newStructType(v, p, t, parser)
case reflect.Interface:
if v.IsValid() && !v.IsZero() {
return createFieldFromValue(v.Elem(), p, t)
return newInterfaceType(v, p, t, parser)
case reflect.Array, reflect.Slice:
return &collectionSliceType{
collectionType: newCollectionType(v, p, t, parser),
}
case reflect.Map:
return &collectionMapType{
collectionType: newCollectionType(v, p, t, parser),
}
return emptyField{}
case reflect.Chan, reflect.Func, reflect.UnsafePointer, reflect.Uintptr:
// unsupported types
return emptyField{}
default:
return newFieldType(v, p, t)
return newFieldType(v, p, t, parser.external, parser.PriorityOrder())
}
}

Expand Down
2 changes: 1 addition & 1 deletion parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func (e *EnvConf) Parse(data interface{}, opts ...option.ClientOption) error {
return err
}
}
if err = e.external.unmarshal(p.t, data); err != nil {
if err = e.external.unmarshal(data); err != nil {
return err
}
return p.define()
Expand Down
5 changes: 0 additions & 5 deletions parser_collection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,6 @@ func TestParse_Array_ErrOutOfRange(t *testing.T) {
Field [2]int `env:"TEST_PARSE_ARRAY_OK"`
}{}
os.Setenv("TEST_PARSE_ARRAY_OK", "-2, -1,0, 1 ,2 ")
defer func() {
if e := recover(); e == nil {
t.Fatal("expected error but got nil")
}
}()
if err := envconf.Parse(&cfg); err == nil {
t.Fatal("expected error but got nil")
}
Expand Down
Loading

0 comments on commit 6867d15

Please sign in to comment.