Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

internal: interface support #37

Merged
merged 4 commits into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
fetch-depth: 2
- uses: actions/setup-go@v4
with:
go-version: '1.16'
go-version: '1.18'
- name: build
run: go build -race -o /dev/null
- name: Run coverage
Expand Down
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 {
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:
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 {
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 go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/antonmashko/envconf

go 1.16
go 1.18
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
Loading