Skip to content

Commit

Permalink
Merge pull request #27 from antonmashko/external-source-refactoring
Browse files Browse the repository at this point in the history
flexible logic for external source
  • Loading branch information
antonmashko authored Sep 14, 2023
2 parents 522b0f0 + bcb6b65 commit 1e18219
Show file tree
Hide file tree
Showing 9 changed files with 365 additions and 129 deletions.
115 changes: 102 additions & 13 deletions external.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,113 @@
package envconf

import "reflect"

// DEPRECATED: on next changes this interface will be removed
type Value interface {
Owner() Value
Name() string
Tag() reflect.StructField
}
import (
"fmt"
"reflect"
"strings"
"unicode"
)

// External config source
type External interface {
// Get string value from values chain(from parent to child)
Get(...Value) (interface{}, bool)
//
TagName() string
Unmarshal(interface{}) error
}

type emptyExt struct{}

func (c *emptyExt) Get(v ...Value) (interface{}, bool) { return nil, false }
func (emptyExt) TagName() string {
return ""
}

func (emptyExt) Unmarshal(v interface{}) error { return nil }

type externalConfig struct {
s *structType
ext External
data map[string]interface{}
}

func newExternalConfig(ext External) *externalConfig {
return &externalConfig{
ext: ext,
data: make(map[string]interface{}),
}
}

func (c *externalConfig) Unmarshal(v interface{}) error {
if c.ext == (emptyExt{}) {
return nil
}
mp := make(map[string]interface{})
err := c.ext.Unmarshal(&mp)
if err != nil {
return err
}
c.data = make(map[string]interface{})
if err = c.fillMap(c.s, mp); err != nil {
return err
}
return nil
}

func (c *externalConfig) setParentStruct(s *structType) {
c.s = s
}

func (c *externalConfig) get(f field) (interface{}, bool) {
v, ok := c.data[fullname(f)]
return v, ok
}

func (c *emptyExt) Unmarshal(v interface{}) error { return nil }
func (c *externalConfig) fillMap(s *structType, src map[string]interface{}) error {
for k, v := range src {
f, ok := c.findField(k, s)
if !ok {
continue
}

mp, ok := v.(map[string]interface{})
if ok && f.structField().Type.Kind() != reflect.Map {
st, ok := f.(*structType)
if !ok {
return &Error{
Message: fmt.Sprintf("unable to cast %s to struct", f.structField().Type),
FieldName: fullname(f),
}
}
c.fillMap(st, mp)
continue
}

c.data[fullname(f)] = v
}
return nil
}

func (c *externalConfig) findField(key string, s *structType) (field, bool) {
var fr rune
for _, r := range key {
fr = r
break
}
lc := unicode.IsLower(fr)
for _, f := range s.fields {
// if annotation exists matching only by it
sf := f.structField()
extName := sf.Tag.Get(c.ext.TagName())
if extName != "" && key == extName {
return f, true
}

// unexportable field. looking for any first match with EqualFold
if lc && strings.EqualFold(key, sf.Name) {
return f, true
}

if key == sf.Name {
return f, true
}
}

return nil, false
}
32 changes: 32 additions & 0 deletions external_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package envconf

import "testing"

func TestExternal_EmpExt_Ok(t *testing.T) {
e := emptyExt{}
if e.TagName() != "" || e.Unmarshal(nil) != nil {
t.Error("unexpected result")
}
}

func TestExternal_newExternalConfig_Ok(t *testing.T) {
ext := newExternalConfig(emptyExt{})
if ext == nil {
t.Fail()
}
if ext.Unmarshal(nil) != nil {
t.Error("unexpected result")
}
}

func TestExternal_InvalidJson_Err(t *testing.T) {
jsonConf := NewJsonConfig()
jsonConf.Read([]byte("<test></test>"))
ext := newExternalConfig(jsonConf)
tc := struct {
Foo int
}{}
if ext.Unmarshal(&tc) == nil {
t.Error("unexpected error got nil")
}
}
5 changes: 5 additions & 0 deletions field.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type field interface {
init() error
define() error
isSet() bool
structField() reflect.StructField
}

type requiredField interface {
Expand Down Expand Up @@ -42,6 +43,10 @@ func (emptyField) name() string {
return ""
}

func (emptyField) structField() reflect.StructField {
return reflect.StructField{}
}

func createFieldFromValue(v reflect.Value, p *structType, t reflect.StructField) field {
switch v.Kind() {
case reflect.Struct:
Expand Down
49 changes: 5 additions & 44 deletions json.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,66 +2,27 @@ package envconf

import (
"encoding/json"
"strings"
)

type JsonConfig struct {
m map[string]interface{}
data []byte
}

// DEPRECATED: Do not use this external type
// It will be moved to the separate package
func NewJsonConfig() *JsonConfig {
return &JsonConfig{
m: make(map[string]interface{}),
}
return &JsonConfig{}
}

func (j *JsonConfig) Read(data []byte) {
j.data = data
func (j *JsonConfig) TagName() string {
return "json"
}

func (j *JsonConfig) Get(values ...Value) (interface{}, bool) {
const tagName = "json"
mp := map[string]interface{}(j.m)
for _, v := range values {
name := v.Tag().Tag.Get(tagName)
if name == "" {
name = v.Name()
}
name = strings.ToLower(name)
tmp, ok := mp[name]
if !ok {
// lookup with ignore case
for k, v := range mp {
if strings.ToLower(k) == name {
tmp = v
}
}
if tmp == nil {
// NOTE: not found
return nil, false
}
}
switch tmp.(type) {
case map[string]interface{}:
mp = tmp.(map[string]interface{})
break
default:
return tmp, true
}
}
return nil, false
func (j *JsonConfig) Read(data []byte) {
j.data = data
}

func (j *JsonConfig) Unmarshal(v interface{}) error {
if j.data == nil {
return nil
}
err := json.Unmarshal(j.data, &j.m)
if err != nil {
return err
}
return json.Unmarshal(j.data, v)
}
Loading

0 comments on commit 1e18219

Please sign in to comment.