Skip to content

Commit

Permalink
Feature user types (#39)
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Demin <[email protected]>
  • Loading branch information
Oberonus authored Feb 3, 2020
1 parent 5dd031c commit 573ad8b
Show file tree
Hide file tree
Showing 7 changed files with 282 additions and 48 deletions.
65 changes: 21 additions & 44 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"reflect"
"strconv"

"github.com/beatlabs/harvester/log"
)
Expand All @@ -25,25 +24,29 @@ const (

var sourceTags = [...]Source{SourceSeed, SourceEnv, SourceConsul, SourceFlag}

// CfgType represents an interface which any config field type must implement.
type CfgType interface {
fmt.Stringer
SetString(string) error
}

// Field definition of a config value that can change.
type Field struct {
name string
tp string
version uint64
setter reflect.Value
printer reflect.Value
sources map[Source]string
name string
tp string
version uint64
structField CfgType
sources map[Source]string
}

// newField constructor.
func newField(prefix string, fld reflect.StructField, val reflect.Value) *Field {
f := &Field{
name: prefix + fld.Name,
tp: fld.Type.Name(),
version: 0,
setter: val.Addr().MethodByName("Set"),
printer: val.Addr().MethodByName("String"),
sources: make(map[Source]string),
name: prefix + fld.Name,
tp: fld.Type.Name(),
version: 0,
structField: val.Addr().Interface().(CfgType),
sources: make(map[Source]string),
}

for _, tag := range sourceTags {
Expand Down Expand Up @@ -73,11 +76,7 @@ func (f *Field) Sources() map[Source]string {

// String returns string representation of field's value.
func (f *Field) String() string {
vv := f.printer.Call([]reflect.Value{})
if len(vv) > 0 {
return vv[0].String()
}
return ""
return f.structField.String()
}

// Set the value of the field.
Expand All @@ -86,33 +85,11 @@ func (f *Field) Set(value string, version uint64) error {
log.Warnf("version %d is older or same as the field's %s", version, f.name)
return nil
}
var arg interface{}
switch f.tp {
case "Bool":
v, err := strconv.ParseBool(value)
if err != nil {
return err
}
arg = v
case "String", "Secret":
arg = value
case "Int64":
v, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return err
}
arg = v
case "Float64":
v, err := strconv.ParseFloat(value, 64)
if err != nil {
return err
}
arg = v
}
rr := f.setter.Call([]reflect.Value{reflect.ValueOf(arg)})
if len(rr) > 0 {
return fmt.Errorf("the set call returned %d values: %v", len(rr), rr)

if err := f.structField.SetString(value); err != nil {
return err
}

f.version = version
log.Infof("field %s updated with value %v, version: %d", f.name, f, version)
return nil
Expand Down
65 changes: 65 additions & 0 deletions config/custom_type_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package config_test

import (
"fmt"
"sync"
"testing"

"github.com/beatlabs/harvester/config"
stdTypes "github.com/beatlabs/harvester/sync"
"github.com/stretchr/testify/assert"
)

func TestCustomField(t *testing.T) {
c := &testConfig{}
cfg, err := config.New(c)
assert.NoError(t, err)
err = cfg.Fields[0].Set("expected", 1)
assert.NoError(t, err)
err = cfg.Fields[1].Set("bar", 1)
assert.NoError(t, err)
assert.Equal(t, "expected", c.CustomValue.Get())
assert.Equal(t, "bar", c.SomeString.Get())
}

func TestErrorValidationOnCustomField(t *testing.T) {
c := &testConfig{}
cfg, err := config.New(c)
assert.NoError(t, err)
err = cfg.Fields[0].Set("not_expected", 1)
assert.Error(t, err)
}

type testConcreteValue struct {
m sync.Mutex
value string
}

func (f *testConcreteValue) Set(value string) {
f.m.Lock()
defer f.m.Unlock()
f.value = value
}

func (f *testConcreteValue) Get() string {
f.m.Lock()
defer f.m.Unlock()
return f.value
}

func (f *testConcreteValue) String() string {
return f.Get()
}

func (f *testConcreteValue) SetString(value string) error {
if value != "expected" {
return fmt.Errorf("unable to store provided value")
}
f.Set(value)
return nil
}

type testConfig struct {
CustomValue testConcreteValue `seed:"expected"`
SomeString stdTypes.String `seed:"foo"`
}
10 changes: 6 additions & 4 deletions config/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (p *parser) getFields(prefix string, tp reflect.Type, val reflect.Value) ([
for i := 0; i < tp.NumField(); i++ {
f := tp.Field(i)

typ, err := p.getStructFieldType(f)
typ, err := p.getStructFieldType(f, val.Field(i))
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -86,16 +86,18 @@ func (p *parser) isKeyValueDuplicate(src Source, value string) bool {
return false
}

func (p *parser) getStructFieldType(f reflect.StructField) (structFieldType, error) {
func (p *parser) getStructFieldType(f reflect.StructField, val reflect.Value) (structFieldType, error) {
t := f.Type
if t.Kind() != reflect.Struct {
return typeInvalid, fmt.Errorf("only struct type supported for %s", f.Name)
}

cfgType := reflect.TypeOf((*CfgType)(nil)).Elem()

for _, tag := range sourceTags {
if _, ok := f.Tag.Lookup(string(tag)); ok {
if t.PkgPath() != "github.com/beatlabs/harvester/sync" {
return typeInvalid, fmt.Errorf("field %s is not supported (only types from the sync package of harvester)", f.Name)
if !val.Addr().Type().Implements(cfgType) {
return typeInvalid, fmt.Errorf("field %s must implement CfgType interface", f.Name)
}
return typeField, nil
}
Expand Down
102 changes: 102 additions & 0 deletions examples/05_custom_types/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package main

import (
"context"
"fmt"
"log"
"os"
"regexp"
"strings"

gosync "sync"

"github.com/beatlabs/harvester"
"github.com/beatlabs/harvester/sync"
)

type config struct {
IndexName sync.String `seed:"customers-v1"`
Email Email `seed:"[email protected]" env:"ENV_EMAIL"`
}

func main() {
ctx, cnl := context.WithCancel(context.Background())
defer cnl()

err := os.Setenv("ENV_EMAIL", "[email protected]")
if err != nil {
log.Fatalf("failed to set env var: %v", err)
}

cfg := config{}

h, err := harvester.New(&cfg).Create()
if err != nil {
log.Fatalf("failed to create harvester: %v", err)
}

err = h.Harvest(ctx)
if err != nil {
log.Fatalf("failed to harvest configuration: %v", err)
}

log.Printf("Config : IndexName: %s, Email: %s, Email.Name: %s, Email.Domain: %s\n", cfg.IndexName.Get(), cfg.Email.Get(), cfg.Email.GetName(), cfg.Email.GetDomain())
}

// regex to validate an email value.
const emailPattern = "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"

// Email represents a custom config structure.
type Email struct {
m gosync.RWMutex
v string
name string
domain string
}

// SetString performs basic validation and sets a config value from string typed value.
func (t *Email) SetString(v string) error {
re := regexp.MustCompile(emailPattern)
if !re.MatchString(v) {
return fmt.Errorf("%s is not a valid email address", v)
}

t.m.Lock()
defer t.m.Unlock()

t.v = v
parts := strings.Split(v, "@")
t.name = parts[0]
t.domain = parts[1]

return nil
}

// Get returns the stored value.
func (t *Email) Get() string {
t.m.RLock()
defer t.m.RUnlock()

return t.v
}

// GetName returns name part of the stored email.
func (t *Email) GetName() string {
t.m.RLock()
defer t.m.RUnlock()

return t.name
}

// GetDomain returns domain part of the stored email.
func (t *Email) GetDomain() string {
t.m.RLock()
defer t.m.RUnlock()

return t.domain
}

// String represents golang Stringer interface.
func (t *Email) String() string {
return t.Get()
}
12 changes: 12 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,15 @@ A fast way to get consul is the following:
2019/09/24 16:40:14 WARN: version 135 is older or same as the field's AccessToken
2019/09/24 16:40:15 INFO: field AccessToken updated with value ***, version: 136
2019/09/24 16:40:16 Config: IndexName: customers-v1, CacheRetention: 86400, LogLevel: DEBUG, AccessToken: newaccesstoken

## 05 Custom config types with complex structure and validation

go run examples/05_custom_types/main.go

2020/01/21 13:39:34 INFO: field IndexName updated with value customers-v1, version: 0
2020/01/21 13:39:34 INFO: seed value customers-v1 applied on field IndexName
2020/01/21 13:39:34 INFO: field EMail updated with value [email protected], version: 0
2020/01/21 13:39:34 INFO: seed value [email protected] applied on field EMail
2020/01/21 13:39:34 INFO: field EMail updated with value [email protected], version: 0
2020/01/21 13:39:34 INFO: env var value [email protected] applied on field EMail
2020/01/21 13:39:34 Config : IndexName: customers-v1, EMail: [email protected], EMail.Name: bar, EMail.Domain: example.com
43 changes: 43 additions & 0 deletions sync/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package sync

import (
"fmt"
"strconv"
"sync"
)

Expand Down Expand Up @@ -35,6 +36,16 @@ func (b *Bool) String() string {
return "false"
}

// SetString parses and sets a value from string type.
func (b *Bool) SetString(val string) error {
v, err := strconv.ParseBool(val)
if err != nil {
return err
}
b.Set(v)
return nil
}

// Int64 type with concurrent access support.
type Int64 struct {
rw sync.RWMutex
Expand Down Expand Up @@ -62,6 +73,16 @@ func (i *Int64) String() string {
return fmt.Sprintf("%d", i.value)
}

// SetString parses and sets a value from string type.
func (i *Int64) SetString(val string) error {
v, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return err
}
i.Set(v)
return nil
}

// Float64 type with concurrent access support.
type Float64 struct {
rw sync.RWMutex
Expand Down Expand Up @@ -89,6 +110,16 @@ func (f *Float64) String() string {
return fmt.Sprintf("%f", f.value)
}

// SetString parses and sets a value from string type.
func (f *Float64) SetString(val string) error {
v, err := strconv.ParseFloat(val, 64)
if err != nil {
return err
}
f.Set(v)
return nil
}

// String type with concurrent access support.
type String struct {
rw sync.RWMutex
Expand Down Expand Up @@ -116,6 +147,12 @@ func (s *String) String() string {
return s.value
}

// SetString parses and sets a value from string type.
func (s *String) SetString(val string) error {
s.Set(val)
return nil
}

// Secret string type for secrets with concurrent access support.
type Secret struct {
rw sync.RWMutex
Expand All @@ -140,3 +177,9 @@ func (s *Secret) Set(value string) {
func (s *Secret) String() string {
return "***"
}

// SetString parses and sets a value from string type.
func (s *Secret) SetString(val string) error {
s.Set(val)
return nil
}
Loading

0 comments on commit 573ad8b

Please sign in to comment.