mirror of
https://github.com/Luzifer/go_helpers.git
synced 2024-12-25 13:31:21 +00:00
Add validation to fieldcollection
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
e407587b52
commit
6efbc2dd11
21 changed files with 1000 additions and 330 deletions
|
@ -1,62 +1,44 @@
|
||||||
|
// Package fieldcollection contains a map[string]any with accessor
|
||||||
|
// methods to derive them into different formats
|
||||||
package fieldcollection
|
package fieldcollection
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrValueNotSet = errors.New("specified value not found")
|
// ErrValueNotSet signalizes the value does not exist in the map
|
||||||
|
ErrValueNotSet = errors.New("specified value not found")
|
||||||
|
// ErrValueMismatch signalizes the value has a different data type
|
||||||
ErrValueMismatch = errors.New("specified value has different format")
|
ErrValueMismatch = errors.New("specified value has different format")
|
||||||
)
|
)
|
||||||
|
|
||||||
type FieldCollection struct {
|
type (
|
||||||
data map[string]interface{}
|
// FieldCollection holds a map with integrated locking and can
|
||||||
lock sync.RWMutex
|
// therefore used in multiple Go-routines concurrently
|
||||||
}
|
FieldCollection struct {
|
||||||
|
data map[string]any
|
||||||
|
lock sync.RWMutex
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// NewFieldCollection creates a new FieldCollection with empty data store
|
// NewFieldCollection creates a new FieldCollection with empty data store
|
||||||
func NewFieldCollection() *FieldCollection {
|
func NewFieldCollection() *FieldCollection {
|
||||||
return &FieldCollection{data: make(map[string]interface{})}
|
return &FieldCollection{data: make(map[string]any)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FieldCollectionFromData is a wrapper around NewFieldCollection and SetFromData
|
// FieldCollectionFromData is a wrapper around NewFieldCollection and SetFromData
|
||||||
func FieldCollectionFromData(data map[string]interface{}) *FieldCollection {
|
//
|
||||||
|
//revive:disable-next-line:exported
|
||||||
|
func FieldCollectionFromData(data map[string]any) *FieldCollection {
|
||||||
o := NewFieldCollection()
|
o := NewFieldCollection()
|
||||||
o.SetFromData(data)
|
o.SetFromData(data)
|
||||||
return o
|
return o
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanBool tries to read key name as bool and checks whether error is nil
|
|
||||||
func (f *FieldCollection) CanBool(name string) bool {
|
|
||||||
_, err := f.Bool(name)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CanDuration tries to read key name as time.Duration and checks whether error is nil
|
|
||||||
func (f *FieldCollection) CanDuration(name string) bool {
|
|
||||||
_, err := f.Duration(name)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CanInt64 tries to read key name as int64 and checks whether error is nil
|
|
||||||
func (f *FieldCollection) CanInt64(name string) bool {
|
|
||||||
_, err := f.Int64(name)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CanString tries to read key name as string and checks whether error is nil
|
|
||||||
func (f *FieldCollection) CanString(name string) bool {
|
|
||||||
_, err := f.String(name)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clone is a wrapper around n.SetFromData(o.Data())
|
// Clone is a wrapper around n.SetFromData(o.Data())
|
||||||
func (f *FieldCollection) Clone() *FieldCollection {
|
func (f *FieldCollection) Clone() *FieldCollection {
|
||||||
out := new(FieldCollection)
|
out := new(FieldCollection)
|
||||||
|
@ -65,7 +47,7 @@ func (f *FieldCollection) Clone() *FieldCollection {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data creates a map-copy of the data stored inside the FieldCollection
|
// Data creates a map-copy of the data stored inside the FieldCollection
|
||||||
func (f *FieldCollection) Data() map[string]interface{} {
|
func (f *FieldCollection) Data() map[string]any {
|
||||||
if f == nil {
|
if f == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -73,7 +55,7 @@ func (f *FieldCollection) Data() map[string]interface{} {
|
||||||
f.lock.RLock()
|
f.lock.RLock()
|
||||||
defer f.lock.RUnlock()
|
defer f.lock.RUnlock()
|
||||||
|
|
||||||
out := make(map[string]interface{})
|
out := make(map[string]any)
|
||||||
for k := range f.data {
|
for k := range f.data {
|
||||||
out[k] = f.data[k]
|
out[k] = f.data[k]
|
||||||
}
|
}
|
||||||
|
@ -114,186 +96,9 @@ func (f *FieldCollection) HasAll(keys ...string) bool {
|
||||||
return f.Expect(keys...) == nil
|
return f.Expect(keys...) == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MustBool is a wrapper around Bool and panics if an error was returned
|
// Get retrieves the value of a key as "any" type or returns an error
|
||||||
func (f *FieldCollection) MustBool(name string, defVal *bool) bool {
|
// in case the field is not set
|
||||||
v, err := f.Bool(name)
|
func (f *FieldCollection) Get(name string) (any, error) {
|
||||||
if err != nil {
|
|
||||||
if defVal != nil {
|
|
||||||
return *defVal
|
|
||||||
}
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustDuration is a wrapper around Duration and panics if an error was returned
|
|
||||||
func (f *FieldCollection) MustDuration(name string, defVal *time.Duration) time.Duration {
|
|
||||||
v, err := f.Duration(name)
|
|
||||||
if err != nil {
|
|
||||||
if defVal != nil {
|
|
||||||
return *defVal
|
|
||||||
}
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustInt64 is a wrapper around Int64 and panics if an error was returned
|
|
||||||
func (f *FieldCollection) MustInt64(name string, defVal *int64) int64 {
|
|
||||||
v, err := f.Int64(name)
|
|
||||||
if err != nil {
|
|
||||||
if defVal != nil {
|
|
||||||
return *defVal
|
|
||||||
}
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustString is a wrapper around String and panics if an error was returned
|
|
||||||
func (f *FieldCollection) MustString(name string, defVal *string) string {
|
|
||||||
v, err := f.String(name)
|
|
||||||
if err != nil {
|
|
||||||
if defVal != nil {
|
|
||||||
return *defVal
|
|
||||||
}
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bool tries to read key name as bool
|
|
||||||
func (f *FieldCollection) Bool(name string) (bool, error) {
|
|
||||||
if f == nil || f.data == nil {
|
|
||||||
return false, errors.New("uninitialized field collection")
|
|
||||||
}
|
|
||||||
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
v, ok := f.data[name]
|
|
||||||
if !ok {
|
|
||||||
return false, ErrValueNotSet
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v := v.(type) {
|
|
||||||
case bool:
|
|
||||||
return v, nil
|
|
||||||
case string:
|
|
||||||
bv, err := strconv.ParseBool(v)
|
|
||||||
return bv, errors.Wrap(err, "parsing string to bool")
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, ErrValueMismatch
|
|
||||||
}
|
|
||||||
|
|
||||||
// Duration tries to read key name as time.Duration
|
|
||||||
func (f *FieldCollection) Duration(name string) (time.Duration, error) {
|
|
||||||
if f == nil || f.data == nil {
|
|
||||||
return 0, errors.New("uninitialized field collection")
|
|
||||||
}
|
|
||||||
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
v, err := f.String(name)
|
|
||||||
if err != nil {
|
|
||||||
return 0, errors.Wrap(err, "getting string value")
|
|
||||||
}
|
|
||||||
|
|
||||||
d, err := time.ParseDuration(v)
|
|
||||||
return d, errors.Wrap(err, "parsing value")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Int64 tries to read key name as int64
|
|
||||||
func (f *FieldCollection) Int64(name string) (int64, error) {
|
|
||||||
if f == nil || f.data == nil {
|
|
||||||
return 0, errors.New("uninitialized field collection")
|
|
||||||
}
|
|
||||||
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
v, ok := f.data[name]
|
|
||||||
if !ok {
|
|
||||||
return 0, ErrValueNotSet
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v := v.(type) {
|
|
||||||
case int:
|
|
||||||
return int64(v), nil
|
|
||||||
case int16:
|
|
||||||
return int64(v), nil
|
|
||||||
case int32:
|
|
||||||
return int64(v), nil
|
|
||||||
case int64:
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, ErrValueMismatch
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set sets a single key to specified value
|
|
||||||
func (f *FieldCollection) Set(key string, value interface{}) {
|
|
||||||
if f == nil {
|
|
||||||
f = NewFieldCollection()
|
|
||||||
}
|
|
||||||
|
|
||||||
f.lock.Lock()
|
|
||||||
defer f.lock.Unlock()
|
|
||||||
|
|
||||||
if f.data == nil {
|
|
||||||
f.data = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
f.data[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFromData takes a map of data and copies all data into the FieldCollection
|
|
||||||
func (f *FieldCollection) SetFromData(data map[string]interface{}) {
|
|
||||||
if f == nil {
|
|
||||||
f = NewFieldCollection()
|
|
||||||
}
|
|
||||||
|
|
||||||
f.lock.Lock()
|
|
||||||
defer f.lock.Unlock()
|
|
||||||
|
|
||||||
if f.data == nil {
|
|
||||||
f.data = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, value := range data {
|
|
||||||
f.data[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String tries to read key name as string
|
|
||||||
func (f *FieldCollection) String(name string) (string, error) {
|
|
||||||
if f == nil || f.data == nil {
|
|
||||||
return "", errors.New("uninitialized field collection")
|
|
||||||
}
|
|
||||||
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
v, ok := f.data[name]
|
|
||||||
if !ok {
|
|
||||||
return "", ErrValueNotSet
|
|
||||||
}
|
|
||||||
|
|
||||||
if sv, ok := v.(string); ok {
|
|
||||||
return sv, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if iv, ok := v.(fmt.Stringer); ok {
|
|
||||||
return iv.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", ErrValueMismatch
|
|
||||||
}
|
|
||||||
|
|
||||||
// StringSlice tries to read key name as []string
|
|
||||||
func (f *FieldCollection) StringSlice(name string) ([]string, error) {
|
|
||||||
if f == nil || f.data == nil {
|
if f == nil || f.data == nil {
|
||||||
return nil, errors.New("uninitialized field collection")
|
return nil, errors.New("uninitialized field collection")
|
||||||
}
|
}
|
||||||
|
@ -306,62 +111,43 @@ func (f *FieldCollection) StringSlice(name string) ([]string, error) {
|
||||||
return nil, ErrValueNotSet
|
return nil, ErrValueNotSet
|
||||||
}
|
}
|
||||||
|
|
||||||
switch v := v.(type) {
|
return v, nil
|
||||||
case []string:
|
|
||||||
return v, nil
|
|
||||||
|
|
||||||
case []interface{}:
|
|
||||||
var out []string
|
|
||||||
|
|
||||||
for _, iv := range v {
|
|
||||||
sv, ok := iv.(string)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("value in slice was not string")
|
|
||||||
}
|
|
||||||
out = append(out, sv)
|
|
||||||
}
|
|
||||||
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, ErrValueMismatch
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement JSON marshalling to plain underlying map[string]interface{}
|
// Keys returns a list of all known keys
|
||||||
|
func (f *FieldCollection) Keys() (keys []string) {
|
||||||
func (f *FieldCollection) MarshalJSON() ([]byte, error) {
|
|
||||||
if f == nil || f.data == nil {
|
|
||||||
return []byte("{}"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
f.lock.RLock()
|
f.lock.RLock()
|
||||||
defer f.lock.RUnlock()
|
defer f.lock.RUnlock()
|
||||||
|
|
||||||
return json.Marshal(f.data)
|
for k := range f.data {
|
||||||
}
|
keys = append(keys, k)
|
||||||
|
|
||||||
func (f *FieldCollection) UnmarshalJSON(raw []byte) error {
|
|
||||||
data := make(map[string]interface{})
|
|
||||||
if err := json.Unmarshal(raw, &data); err != nil {
|
|
||||||
return errors.Wrap(err, "unmarshalling from JSON")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
f.SetFromData(data)
|
return keys
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement YAML marshalling to plain underlying map[string]interface{}
|
// Set sets a single key to specified value
|
||||||
|
func (f *FieldCollection) Set(key string, value any) {
|
||||||
|
f.lock.Lock()
|
||||||
|
defer f.lock.Unlock()
|
||||||
|
|
||||||
func (f *FieldCollection) MarshalYAML() (interface{}, error) {
|
if f.data == nil {
|
||||||
return f.Data(), nil
|
f.data = make(map[string]any)
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FieldCollection) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
||||||
data := make(map[string]interface{})
|
|
||||||
if err := unmarshal(&data); err != nil {
|
|
||||||
return errors.Wrap(err, "unmarshalling from YAML")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
f.SetFromData(data)
|
f.data[key] = value
|
||||||
return nil
|
}
|
||||||
|
|
||||||
|
// SetFromData takes a map of data and copies all data into the FieldCollection
|
||||||
|
func (f *FieldCollection) SetFromData(data map[string]any) {
|
||||||
|
f.lock.Lock()
|
||||||
|
defer f.lock.Unlock()
|
||||||
|
|
||||||
|
if f.data == nil {
|
||||||
|
f.data = make(map[string]any)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range data {
|
||||||
|
f.data[key] = value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,66 +1,19 @@
|
||||||
package fieldcollection
|
package fieldcollection
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFieldCollectionJSONMarshal(t *testing.T) {
|
func TestExpect(t *testing.T) {
|
||||||
var (
|
|
||||||
buf = new(bytes.Buffer)
|
|
||||||
raw = `{"key1":"test1","key2":"test2"}`
|
|
||||||
f = NewFieldCollection()
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := json.NewDecoder(strings.NewReader(raw)).Decode(f); err != nil {
|
|
||||||
t.Fatalf("Unable to unmarshal: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewEncoder(buf).Encode(f); err != nil {
|
|
||||||
t.Fatalf("Unable to marshal: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if raw != strings.TrimSpace(buf.String()) {
|
|
||||||
t.Errorf("Marshalled JSON does not match expectation: res=%s exp=%s", buf.String(), raw)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFieldCollectionYAMLMarshal(t *testing.T) {
|
|
||||||
var (
|
|
||||||
buf = new(bytes.Buffer)
|
|
||||||
raw = "key1: test1\nkey2: test2"
|
|
||||||
f = NewFieldCollection()
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := yaml.NewDecoder(strings.NewReader(raw)).Decode(f); err != nil {
|
|
||||||
t.Fatalf("Unable to unmarshal: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := yaml.NewEncoder(buf).Encode(f); err != nil {
|
|
||||||
t.Fatalf("Unable to marshal: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if raw != strings.TrimSpace(buf.String()) {
|
|
||||||
t.Errorf("Marshalled YAML does not match expectation: res=%s exp=%s", buf.String(), raw)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFieldCollectionNilModify(t *testing.T) {
|
|
||||||
var f *FieldCollection
|
var f *FieldCollection
|
||||||
|
assert.NoError(t, f.Expect())
|
||||||
f.Set("foo", "bar")
|
assert.Error(t, f.Expect("foo"))
|
||||||
|
|
||||||
f = nil
|
|
||||||
f.SetFromData(map[string]interface{}{"foo": "bar"})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFieldCollectionNilClone(t *testing.T) {
|
func TestFieldCollectionNilClone(*testing.T) {
|
||||||
var f *FieldCollection
|
var f *FieldCollection
|
||||||
|
|
||||||
f.Clone()
|
f.Clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,13 +21,38 @@ func TestFieldCollectionNilDataGet(t *testing.T) {
|
||||||
var f *FieldCollection
|
var f *FieldCollection
|
||||||
|
|
||||||
for name, fn := range map[string]func(name string) bool{
|
for name, fn := range map[string]func(name string) bool{
|
||||||
"bool": f.CanBool,
|
"bool": f.CanBool,
|
||||||
"duration": f.CanDuration,
|
"duration": f.CanDuration,
|
||||||
"int64": f.CanInt64,
|
"int64": f.CanInt64,
|
||||||
"string": f.CanString,
|
"string": f.CanString,
|
||||||
|
"stringSlice": f.CanStringSlice,
|
||||||
} {
|
} {
|
||||||
if fn("foo") {
|
assert.False(t, fn("foo"), "%s key is available", name)
|
||||||
t.Errorf("%s key is available", name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGet(t *testing.T) {
|
||||||
|
f := &FieldCollection{}
|
||||||
|
_, err := f.Get("foo")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
f.Set("foo", "bar")
|
||||||
|
_, err = f.Get("bar")
|
||||||
|
assert.ErrorIs(t, err, ErrValueNotSet)
|
||||||
|
|
||||||
|
v, err := f.Get("foo")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "bar", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKeys(t *testing.T) {
|
||||||
|
f := FieldCollectionFromData(map[string]any{
|
||||||
|
"foo": "bar",
|
||||||
|
})
|
||||||
|
assert.Equal(t, []string{"foo"}, f.Keys())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetOnNew(*testing.T) {
|
||||||
|
f := new(FieldCollection)
|
||||||
|
f.Set("foo", "bar")
|
||||||
|
}
|
||||||
|
|
38
fieldcollection/marshalJSON.go
Normal file
38
fieldcollection/marshalJSON.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package fieldcollection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Implement JSON marshalling to plain underlying map[string]any
|
||||||
|
|
||||||
|
// MarshalJSON implements json.Marshaller interface
|
||||||
|
func (f *FieldCollection) MarshalJSON() ([]byte, error) {
|
||||||
|
if f == nil || f.data == nil {
|
||||||
|
return []byte("{}"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f.lock.RLock()
|
||||||
|
defer f.lock.RUnlock()
|
||||||
|
|
||||||
|
data, err := json.Marshal(f.data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshalling to JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON implements json.Unmarshaller interface
|
||||||
|
func (f *FieldCollection) UnmarshalJSON(raw []byte) error {
|
||||||
|
data := make(map[string]any)
|
||||||
|
if err := json.Unmarshal(raw, &data); err != nil {
|
||||||
|
return errors.Wrap(err, "unmarshalling from JSON")
|
||||||
|
}
|
||||||
|
|
||||||
|
f.SetFromData(data)
|
||||||
|
return nil
|
||||||
|
}
|
23
fieldcollection/marshalJSON_test.go
Normal file
23
fieldcollection/marshalJSON_test.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package fieldcollection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFieldCollectionJSONMarshal(t *testing.T) {
|
||||||
|
var (
|
||||||
|
buf = new(bytes.Buffer)
|
||||||
|
raw = `{"key1":"test1","key2":"test2"}`
|
||||||
|
f = NewFieldCollection()
|
||||||
|
)
|
||||||
|
|
||||||
|
require.NoError(t, json.NewDecoder(strings.NewReader(raw)).Decode(f))
|
||||||
|
require.NoError(t, json.NewEncoder(buf).Encode(f))
|
||||||
|
assert.Equal(t, raw, strings.TrimSpace(buf.String()))
|
||||||
|
}
|
21
fieldcollection/marshalYAML.go
Normal file
21
fieldcollection/marshalYAML.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package fieldcollection
|
||||||
|
|
||||||
|
import "github.com/pkg/errors"
|
||||||
|
|
||||||
|
// Implement YAML marshalling to plain underlying map[string]any
|
||||||
|
|
||||||
|
// MarshalYAML implements yaml.Marshaller interface
|
||||||
|
func (f *FieldCollection) MarshalYAML() (any, error) {
|
||||||
|
return f.Data(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML implements yaml.Unmarshaller interface
|
||||||
|
func (f *FieldCollection) UnmarshalYAML(unmarshal func(any) error) error {
|
||||||
|
data := make(map[string]any)
|
||||||
|
if err := unmarshal(&data); err != nil {
|
||||||
|
return errors.Wrap(err, "unmarshalling from YAML")
|
||||||
|
}
|
||||||
|
|
||||||
|
f.SetFromData(data)
|
||||||
|
return nil
|
||||||
|
}
|
23
fieldcollection/marshalYAML_test.go
Normal file
23
fieldcollection/marshalYAML_test.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package fieldcollection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFieldCollectionYAMLMarshal(t *testing.T) {
|
||||||
|
var (
|
||||||
|
buf = new(bytes.Buffer)
|
||||||
|
raw = "key1: test1\nkey2: test2"
|
||||||
|
f = NewFieldCollection()
|
||||||
|
)
|
||||||
|
|
||||||
|
require.NoError(t, yaml.NewDecoder(strings.NewReader(raw)).Decode(f))
|
||||||
|
require.NoError(t, yaml.NewEncoder(buf).Encode(f))
|
||||||
|
assert.Equal(t, raw, strings.TrimSpace(buf.String()))
|
||||||
|
}
|
176
fieldcollection/schema.go
Normal file
176
fieldcollection/schema.go
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
package fieldcollection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/str"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
knownFields = "knownFields"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// SchemaField defines how a field is expected to be
|
||||||
|
SchemaField struct {
|
||||||
|
// Name of the field to validate
|
||||||
|
Name string
|
||||||
|
// If set to true the field must i.e. not be "" for a string field
|
||||||
|
NonEmpty bool
|
||||||
|
// The expected type of the field
|
||||||
|
Type SchemaFieldType
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchemaFieldType is a collection of known field types for which
|
||||||
|
// can be checked
|
||||||
|
SchemaFieldType uint64
|
||||||
|
|
||||||
|
// ValidateOpt is a validation function to be executed during the
|
||||||
|
// validation call
|
||||||
|
ValidateOpt func(f, validateStore *FieldCollection) error
|
||||||
|
)
|
||||||
|
|
||||||
|
// Collection of known field types for which can be checked
|
||||||
|
const (
|
||||||
|
SchemaFieldTypeAny SchemaFieldType = iota
|
||||||
|
SchemaFieldTypeBool
|
||||||
|
SchemaFieldTypeDuration
|
||||||
|
SchemaFieldTypeInt64
|
||||||
|
SchemaFieldTypeString
|
||||||
|
SchemaFieldTypeStringSlice
|
||||||
|
)
|
||||||
|
|
||||||
|
// CanHaveField validates the type of the field if it exists and puts
|
||||||
|
// the field to the allow-list for MustHaveNoUnknowFields
|
||||||
|
func CanHaveField(field SchemaField) ValidateOpt {
|
||||||
|
return func(f, validateStore *FieldCollection) error {
|
||||||
|
validateStore.Set(knownFields, append(validateStore.MustStringSlice(knownFields, nil), field.Name))
|
||||||
|
|
||||||
|
if !f.HasAll(field.Name) {
|
||||||
|
// It is allowed to not exist, and if it does not we don't need
|
||||||
|
// to type-check it
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return validateFieldType(f, field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustHaveField validates the type of the field and puts the field to
|
||||||
|
// the allow-list for MustHaveNoUnknowFields
|
||||||
|
func MustHaveField(field SchemaField) ValidateOpt {
|
||||||
|
return func(f, validateStore *FieldCollection) error {
|
||||||
|
validateStore.Set(knownFields, append(validateStore.MustStringSlice(knownFields, nil), field.Name))
|
||||||
|
|
||||||
|
if !f.HasAll(field.Name) {
|
||||||
|
// It must exist and does not
|
||||||
|
return fmt.Errorf("field %s does not exist", field.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return validateFieldType(f, field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustHaveNoUnknowFields validates no fields are present which are
|
||||||
|
// not previously allow-listed through CanHaveField or MustHaveField
|
||||||
|
// and therefore should be put as the last ValidateOpt
|
||||||
|
func MustHaveNoUnknowFields(f *FieldCollection, validateStore *FieldCollection) error {
|
||||||
|
var unexpected []string
|
||||||
|
|
||||||
|
for _, k := range f.Keys() {
|
||||||
|
if !str.StringInSlice(k, validateStore.MustStringSlice(knownFields, nil)) {
|
||||||
|
unexpected = append(unexpected, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(unexpected)
|
||||||
|
|
||||||
|
if len(unexpected) > 0 {
|
||||||
|
return fmt.Errorf("found unexpected fields: %s", strings.Join(unexpected, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateSchema can be used to validate the contents of the
|
||||||
|
// FieldCollection by passing in field definitions which may be there
|
||||||
|
// or must be there and to check whether there are no surplus fields
|
||||||
|
func (f *FieldCollection) ValidateSchema(opts ...ValidateOpt) error {
|
||||||
|
validateStore := NewFieldCollection()
|
||||||
|
validateStore.Set(knownFields, []string{})
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
if err := opt(f, validateStore); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:gocyclo // These are quite simple checks
|
||||||
|
func validateFieldType(f *FieldCollection, field SchemaField) (err error) {
|
||||||
|
switch field.Type {
|
||||||
|
case SchemaFieldTypeAny:
|
||||||
|
v, err := f.Get(field.Name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting field %s: %w", field.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.NonEmpty && v == nil {
|
||||||
|
return fmt.Errorf("field %s is empty", field.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
case SchemaFieldTypeBool:
|
||||||
|
if !f.CanBool(field.Name) {
|
||||||
|
return fmt.Errorf("field %s is not of type bool", field.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
case SchemaFieldTypeDuration:
|
||||||
|
v, err := f.Duration(field.Name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("field %s is not of type time.Duration: %w", field.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.NonEmpty && v == 0 {
|
||||||
|
return fmt.Errorf("field %s is empty", field.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
case SchemaFieldTypeInt64:
|
||||||
|
v, err := f.Int64(field.Name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("field %s is not of type int64: %w", field.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.NonEmpty && v == 0 {
|
||||||
|
return fmt.Errorf("field %s is empty", field.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
case SchemaFieldTypeString:
|
||||||
|
v, err := f.String(field.Name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("field %s is not of type string: %w", field.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.NonEmpty && v == "" {
|
||||||
|
return fmt.Errorf("field %s is empty", field.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
case SchemaFieldTypeStringSlice:
|
||||||
|
v, err := f.StringSlice(field.Name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("field %s is not of type []string: %w", field.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.NonEmpty && len(v) == 0 {
|
||||||
|
return fmt.Errorf("field %s is empty", field.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown field type specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
116
fieldcollection/schema_test.go
Normal file
116
fieldcollection/schema_test.go
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
package fieldcollection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
//nolint:funlen
|
||||||
|
func TestSchemaValidation(t *testing.T) {
|
||||||
|
fc := FieldCollectionFromData(map[string]any{
|
||||||
|
"anyZero": nil,
|
||||||
|
"bool": true,
|
||||||
|
"duration": time.Second,
|
||||||
|
"durationZero": time.Duration(0),
|
||||||
|
"int64": int64(12),
|
||||||
|
"int64Zero": int64(0),
|
||||||
|
"string": "ohai",
|
||||||
|
"stringZero": "",
|
||||||
|
"stringSlice": []string{"ohai"},
|
||||||
|
"stringSliceZero": []string{},
|
||||||
|
"stringSliceNil": nil,
|
||||||
|
})
|
||||||
|
|
||||||
|
// No validations
|
||||||
|
assert.NoError(t, fc.ValidateSchema())
|
||||||
|
|
||||||
|
// Non-existing field
|
||||||
|
assert.ErrorContains(t, fc.ValidateSchema(
|
||||||
|
MustHaveField(SchemaField{Name: "foo"}),
|
||||||
|
), "field foo does not exist")
|
||||||
|
|
||||||
|
// Non-existing field but can
|
||||||
|
assert.NoError(t, fc.ValidateSchema(
|
||||||
|
CanHaveField(SchemaField{Name: "foo"}),
|
||||||
|
))
|
||||||
|
|
||||||
|
// No unexpected fields (none given)
|
||||||
|
assert.ErrorContains(t, fc.ValidateSchema(
|
||||||
|
MustHaveNoUnknowFields,
|
||||||
|
), "found unexpected fields: anyZero, bool, duration, durationZero, int64, int64Zero, string, stringSlice, stringSliceNil, stringSliceZero, stringZero")
|
||||||
|
|
||||||
|
// No unexpected fields (all given)
|
||||||
|
assert.NoError(t, fc.ValidateSchema(
|
||||||
|
CanHaveField(SchemaField{Name: "anyZero"}),
|
||||||
|
CanHaveField(SchemaField{Name: "bool"}),
|
||||||
|
CanHaveField(SchemaField{Name: "duration"}),
|
||||||
|
CanHaveField(SchemaField{Name: "durationZero"}),
|
||||||
|
CanHaveField(SchemaField{Name: "int64"}),
|
||||||
|
CanHaveField(SchemaField{Name: "int64Zero"}),
|
||||||
|
CanHaveField(SchemaField{Name: "string"}),
|
||||||
|
CanHaveField(SchemaField{Name: "stringSlice"}),
|
||||||
|
CanHaveField(SchemaField{Name: "stringSliceNil"}),
|
||||||
|
CanHaveField(SchemaField{Name: "stringSliceZero"}),
|
||||||
|
CanHaveField(SchemaField{Name: "stringZero"}),
|
||||||
|
MustHaveNoUnknowFields,
|
||||||
|
))
|
||||||
|
|
||||||
|
// Field must exist in any type and not be zero
|
||||||
|
assert.NoError(t, fc.ValidateSchema(
|
||||||
|
MustHaveField(SchemaField{Name: "string", NonEmpty: true}),
|
||||||
|
))
|
||||||
|
|
||||||
|
// Field must exist in any type and not be zero but is zero
|
||||||
|
assert.ErrorContains(t, fc.ValidateSchema(
|
||||||
|
MustHaveField(SchemaField{Name: "anyZero", NonEmpty: true}),
|
||||||
|
), "field anyZero is empty")
|
||||||
|
|
||||||
|
// Fields must exist and be of correct type
|
||||||
|
assert.NoError(t, fc.ValidateSchema(
|
||||||
|
MustHaveField(SchemaField{Name: "bool", Type: SchemaFieldTypeBool}),
|
||||||
|
MustHaveField(SchemaField{Name: "duration", Type: SchemaFieldTypeDuration}),
|
||||||
|
MustHaveField(SchemaField{Name: "int64", Type: SchemaFieldTypeInt64}),
|
||||||
|
MustHaveField(SchemaField{Name: "string", Type: SchemaFieldTypeString}),
|
||||||
|
MustHaveField(SchemaField{Name: "stringSlice", Type: SchemaFieldTypeStringSlice}),
|
||||||
|
))
|
||||||
|
assert.Error(t, fc.ValidateSchema(
|
||||||
|
MustHaveField(SchemaField{Name: "bool", Type: SchemaFieldTypeDuration}),
|
||||||
|
))
|
||||||
|
assert.Error(t, fc.ValidateSchema(
|
||||||
|
MustHaveField(SchemaField{Name: "duration", Type: SchemaFieldTypeBool}),
|
||||||
|
))
|
||||||
|
assert.Error(t, fc.ValidateSchema(
|
||||||
|
MustHaveField(SchemaField{Name: "int64", Type: SchemaFieldTypeStringSlice}),
|
||||||
|
))
|
||||||
|
assert.Error(t, fc.ValidateSchema(
|
||||||
|
MustHaveField(SchemaField{Name: "string", Type: SchemaFieldTypeInt64}),
|
||||||
|
))
|
||||||
|
assert.Error(t, fc.ValidateSchema(
|
||||||
|
MustHaveField(SchemaField{Name: "stringSlice", Type: SchemaFieldTypeString}),
|
||||||
|
))
|
||||||
|
|
||||||
|
// Fields must not be zero
|
||||||
|
assert.ErrorContains(t, fc.ValidateSchema(
|
||||||
|
MustHaveField(SchemaField{Name: "duration", NonEmpty: true, Type: SchemaFieldTypeDuration}),
|
||||||
|
MustHaveField(SchemaField{Name: "durationZero", NonEmpty: true, Type: SchemaFieldTypeDuration}),
|
||||||
|
), "field durationZero is empty")
|
||||||
|
assert.ErrorContains(t, fc.ValidateSchema(
|
||||||
|
MustHaveField(SchemaField{Name: "int64", NonEmpty: true, Type: SchemaFieldTypeInt64}),
|
||||||
|
MustHaveField(SchemaField{Name: "int64Zero", NonEmpty: true, Type: SchemaFieldTypeInt64}),
|
||||||
|
), "field int64Zero is empty")
|
||||||
|
assert.ErrorContains(t, fc.ValidateSchema(
|
||||||
|
MustHaveField(SchemaField{Name: "string", NonEmpty: true, Type: SchemaFieldTypeString}),
|
||||||
|
MustHaveField(SchemaField{Name: "stringZero", NonEmpty: true, Type: SchemaFieldTypeString}),
|
||||||
|
), "field stringZero is empty")
|
||||||
|
assert.ErrorContains(t, fc.ValidateSchema(
|
||||||
|
MustHaveField(SchemaField{Name: "stringSlice", NonEmpty: true, Type: SchemaFieldTypeStringSlice}),
|
||||||
|
MustHaveField(SchemaField{Name: "stringSliceZero", NonEmpty: true, Type: SchemaFieldTypeStringSlice}),
|
||||||
|
), "field stringSliceZero is empty")
|
||||||
|
|
||||||
|
// Invalid field type
|
||||||
|
assert.ErrorContains(t, fc.ValidateSchema(
|
||||||
|
MustHaveField(SchemaField{Name: "stringSlice", NonEmpty: true, Type: 99999}),
|
||||||
|
), "unknown field type specified")
|
||||||
|
}
|
50
fieldcollection/typeBool.go
Normal file
50
fieldcollection/typeBool.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package fieldcollection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bool tries to read key name as bool
|
||||||
|
func (f *FieldCollection) Bool(name string) (bool, error) {
|
||||||
|
if f == nil || f.data == nil {
|
||||||
|
return false, errors.New("uninitialized field collection")
|
||||||
|
}
|
||||||
|
|
||||||
|
f.lock.RLock()
|
||||||
|
defer f.lock.RUnlock()
|
||||||
|
|
||||||
|
v, ok := f.data[name]
|
||||||
|
if !ok {
|
||||||
|
return false, ErrValueNotSet
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := v.(type) {
|
||||||
|
case bool:
|
||||||
|
return v, nil
|
||||||
|
case string:
|
||||||
|
bv, err := strconv.ParseBool(v)
|
||||||
|
return bv, errors.Wrap(err, "parsing string to bool")
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, ErrValueMismatch
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanBool tries to read key name as bool and checks whether error is nil
|
||||||
|
func (f *FieldCollection) CanBool(name string) bool {
|
||||||
|
_, err := f.Bool(name)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustBool is a wrapper around Bool and panics if an error was returned
|
||||||
|
func (f *FieldCollection) MustBool(name string, defVal *bool) bool {
|
||||||
|
v, err := f.Bool(name)
|
||||||
|
if err != nil {
|
||||||
|
if defVal != nil {
|
||||||
|
return *defVal
|
||||||
|
}
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
46
fieldcollection/typeBool_test.go
Normal file
46
fieldcollection/typeBool_test.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package fieldcollection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBool(t *testing.T) {
|
||||||
|
fc := FieldCollectionFromData(map[string]any{
|
||||||
|
"int": 12,
|
||||||
|
"invalidBoolString": "I'm a string!",
|
||||||
|
"validBool": true,
|
||||||
|
"validBoolString": "true",
|
||||||
|
"validBoolStringFalse": "false",
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := fc.Bool("_")
|
||||||
|
assert.ErrorIs(t, err, ErrValueNotSet)
|
||||||
|
|
||||||
|
_, err = fc.Bool("int")
|
||||||
|
assert.ErrorIs(t, err, ErrValueMismatch)
|
||||||
|
|
||||||
|
_, err = fc.Bool("invalidBoolString")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
v, err := fc.Bool("validBool")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, v)
|
||||||
|
|
||||||
|
v, err = fc.Bool("validBoolString")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, v)
|
||||||
|
|
||||||
|
v, err = fc.Bool("validBoolStringFalse")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, v)
|
||||||
|
|
||||||
|
assert.True(t, fc.CanBool("validBool"))
|
||||||
|
assert.False(t, fc.CanBool("int"))
|
||||||
|
|
||||||
|
assert.NotPanics(t, func() { fc.MustBool("validBool", nil) })
|
||||||
|
assert.Panics(t, func() { fc.MustBool("int", nil) })
|
||||||
|
|
||||||
|
assert.True(t, fc.MustBool("_", func(v bool) *bool { return &v }(true)))
|
||||||
|
}
|
51
fieldcollection/typeDuration.go
Normal file
51
fieldcollection/typeDuration.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package fieldcollection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CanDuration tries to read key name as time.Duration and checks whether error is nil
|
||||||
|
func (f *FieldCollection) CanDuration(name string) bool {
|
||||||
|
_, err := f.Duration(name)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duration tries to read key name as time.Duration
|
||||||
|
func (f *FieldCollection) Duration(name string) (time.Duration, error) {
|
||||||
|
if f == nil || f.data == nil {
|
||||||
|
return 0, errors.New("uninitialized field collection")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case !f.HasAll(name):
|
||||||
|
return 0, ErrValueNotSet
|
||||||
|
|
||||||
|
case f.CanInt64(name):
|
||||||
|
return time.Duration(f.MustInt64(name, nil)), nil
|
||||||
|
|
||||||
|
case f.CanString(name):
|
||||||
|
v, err := time.ParseDuration(f.MustString(name, nil))
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("parsing value: %w", err)
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 0, ErrValueMismatch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustDuration is a wrapper around Duration and panics if an error was returned
|
||||||
|
func (f *FieldCollection) MustDuration(name string, defVal *time.Duration) time.Duration {
|
||||||
|
v, err := f.Duration(name)
|
||||||
|
if err != nil {
|
||||||
|
if defVal != nil {
|
||||||
|
return *defVal
|
||||||
|
}
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
47
fieldcollection/typeDuration_test.go
Normal file
47
fieldcollection/typeDuration_test.go
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
package fieldcollection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDuration(t *testing.T) {
|
||||||
|
fc := FieldCollectionFromData(map[string]any{
|
||||||
|
"int": 12,
|
||||||
|
"bool": true,
|
||||||
|
"invalidString": "I'm a string!",
|
||||||
|
"valid": time.Second,
|
||||||
|
"validString": "12m",
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := fc.Duration("_")
|
||||||
|
assert.ErrorIs(t, err, ErrValueNotSet)
|
||||||
|
|
||||||
|
_, err = fc.Duration("bool")
|
||||||
|
assert.ErrorIs(t, err, ErrValueMismatch)
|
||||||
|
|
||||||
|
_, err = fc.Duration("invalidString")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
v, err := fc.Duration("valid")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, time.Second, v)
|
||||||
|
|
||||||
|
v, err = fc.Duration("validString")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 12*time.Minute, v)
|
||||||
|
|
||||||
|
v, err = fc.Duration("int")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 12*time.Nanosecond, v)
|
||||||
|
|
||||||
|
assert.True(t, fc.CanDuration("valid"))
|
||||||
|
assert.False(t, fc.CanDuration("bool"))
|
||||||
|
|
||||||
|
assert.NotPanics(t, func() { fc.MustDuration("valid", nil) })
|
||||||
|
assert.Panics(t, func() { fc.MustDuration("bool", nil) })
|
||||||
|
|
||||||
|
assert.Equal(t, time.Second, fc.MustDuration("_", func(v time.Duration) *time.Duration { return &v }(time.Second)))
|
||||||
|
}
|
64
fieldcollection/typeInt64.go
Normal file
64
fieldcollection/typeInt64.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package fieldcollection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Int64 tries to read key name as int64
|
||||||
|
func (f *FieldCollection) Int64(name string) (int64, error) {
|
||||||
|
if f == nil || f.data == nil {
|
||||||
|
return 0, errors.New("uninitialized field collection")
|
||||||
|
}
|
||||||
|
|
||||||
|
f.lock.RLock()
|
||||||
|
defer f.lock.RUnlock()
|
||||||
|
|
||||||
|
v, ok := f.data[name]
|
||||||
|
if !ok {
|
||||||
|
return 0, ErrValueNotSet
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := v.(type) {
|
||||||
|
case int:
|
||||||
|
return int64(v), nil
|
||||||
|
|
||||||
|
case int16:
|
||||||
|
return int64(v), nil
|
||||||
|
|
||||||
|
case int32:
|
||||||
|
return int64(v), nil
|
||||||
|
|
||||||
|
case int64:
|
||||||
|
return v, nil
|
||||||
|
|
||||||
|
case string:
|
||||||
|
pv, err := strconv.ParseInt(v, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("parsing value: %w", err)
|
||||||
|
}
|
||||||
|
return pv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, ErrValueMismatch
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanInt64 tries to read key name as int64 and checks whether error is nil
|
||||||
|
func (f *FieldCollection) CanInt64(name string) bool {
|
||||||
|
_, err := f.Int64(name)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustInt64 is a wrapper around Int64 and panics if an error was returned
|
||||||
|
func (f *FieldCollection) MustInt64(name string, defVal *int64) int64 {
|
||||||
|
v, err := f.Int64(name)
|
||||||
|
if err != nil {
|
||||||
|
if defVal != nil {
|
||||||
|
return *defVal
|
||||||
|
}
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
56
fieldcollection/typeInt64_test.go
Normal file
56
fieldcollection/typeInt64_test.go
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
package fieldcollection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInt64(t *testing.T) {
|
||||||
|
fc := FieldCollectionFromData(map[string]any{
|
||||||
|
"int": int(12),
|
||||||
|
"int16": int16(12),
|
||||||
|
"int32": int32(12),
|
||||||
|
"int64": int64(12),
|
||||||
|
"bool": true,
|
||||||
|
"invalidString": "I'm a string!",
|
||||||
|
"validString": "12",
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := fc.Int64("_")
|
||||||
|
assert.ErrorIs(t, err, ErrValueNotSet)
|
||||||
|
|
||||||
|
_, err = fc.Int64("bool")
|
||||||
|
assert.ErrorIs(t, err, ErrValueMismatch)
|
||||||
|
|
||||||
|
_, err = fc.Int64("invalidString")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
v, err := fc.Int64("int")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(12), v)
|
||||||
|
|
||||||
|
v, err = fc.Int64("int16")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(12), v)
|
||||||
|
|
||||||
|
v, err = fc.Int64("int32")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(12), v)
|
||||||
|
|
||||||
|
v, err = fc.Int64("int64")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(12), v)
|
||||||
|
|
||||||
|
v, err = fc.Int64("validString")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(12), v)
|
||||||
|
|
||||||
|
assert.True(t, fc.CanInt64("int"))
|
||||||
|
assert.False(t, fc.CanInt64("bool"))
|
||||||
|
|
||||||
|
assert.NotPanics(t, func() { fc.MustInt64("int32", nil) })
|
||||||
|
assert.Panics(t, func() { fc.MustInt64("bool", nil) })
|
||||||
|
|
||||||
|
assert.Equal(t, int64(5), fc.MustInt64("_", func(v int64) *int64 { return &v }(5)))
|
||||||
|
}
|
50
fieldcollection/typeString.go
Normal file
50
fieldcollection/typeString.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package fieldcollection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CanString tries to read key name as string and checks whether error is nil
|
||||||
|
func (f *FieldCollection) CanString(name string) bool {
|
||||||
|
_, err := f.String(name)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustString is a wrapper around String and panics if an error was returned
|
||||||
|
func (f *FieldCollection) MustString(name string, defVal *string) string {
|
||||||
|
v, err := f.String(name)
|
||||||
|
if err != nil {
|
||||||
|
if defVal != nil {
|
||||||
|
return *defVal
|
||||||
|
}
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// String tries to read key name as string
|
||||||
|
func (f *FieldCollection) String(name string) (string, error) {
|
||||||
|
if f == nil || f.data == nil {
|
||||||
|
return "", errors.New("uninitialized field collection")
|
||||||
|
}
|
||||||
|
|
||||||
|
f.lock.RLock()
|
||||||
|
defer f.lock.RUnlock()
|
||||||
|
|
||||||
|
v, ok := f.data[name]
|
||||||
|
if !ok {
|
||||||
|
return "", ErrValueNotSet
|
||||||
|
}
|
||||||
|
|
||||||
|
if sv, ok := v.(string); ok {
|
||||||
|
return sv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if iv, ok := v.(fmt.Stringer); ok {
|
||||||
|
return iv.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", ErrValueMismatch
|
||||||
|
}
|
56
fieldcollection/typeStringSlice.go
Normal file
56
fieldcollection/typeStringSlice.go
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
package fieldcollection
|
||||||
|
|
||||||
|
import "github.com/pkg/errors"
|
||||||
|
|
||||||
|
// CanStringSlice tries to read key name as []string and checks whether error is nil
|
||||||
|
func (f *FieldCollection) CanStringSlice(name string) bool {
|
||||||
|
_, err := f.StringSlice(name)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustStringSlice is a wrapper around StringSlice and panics if an error was returned
|
||||||
|
func (f *FieldCollection) MustStringSlice(name string, defVal *[]string) []string {
|
||||||
|
v, err := f.StringSlice(name)
|
||||||
|
if err != nil {
|
||||||
|
if defVal != nil {
|
||||||
|
return *defVal
|
||||||
|
}
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringSlice tries to read key name as []string
|
||||||
|
func (f *FieldCollection) StringSlice(name string) ([]string, error) {
|
||||||
|
if f == nil || f.data == nil {
|
||||||
|
return nil, errors.New("uninitialized field collection")
|
||||||
|
}
|
||||||
|
|
||||||
|
f.lock.RLock()
|
||||||
|
defer f.lock.RUnlock()
|
||||||
|
|
||||||
|
v, ok := f.data[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrValueNotSet
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := v.(type) {
|
||||||
|
case []string:
|
||||||
|
return v, nil
|
||||||
|
|
||||||
|
case []any:
|
||||||
|
var out []string
|
||||||
|
|
||||||
|
for _, iv := range v {
|
||||||
|
sv, ok := iv.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("value in slice was not string")
|
||||||
|
}
|
||||||
|
out = append(out, sv)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrValueMismatch
|
||||||
|
}
|
45
fieldcollection/typeStringSlice_test.go
Normal file
45
fieldcollection/typeStringSlice_test.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package fieldcollection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStringSlice(t *testing.T) {
|
||||||
|
fc := FieldCollectionFromData(map[string]any{
|
||||||
|
"int": 12,
|
||||||
|
"valid": []string{"ohai"},
|
||||||
|
"invalidSlice": []int{12},
|
||||||
|
"mixed": []any{"ohai", 12},
|
||||||
|
"validAny": []any{"ohai"},
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := fc.StringSlice("_")
|
||||||
|
assert.ErrorIs(t, err, ErrValueNotSet)
|
||||||
|
|
||||||
|
_, err = fc.StringSlice("int")
|
||||||
|
assert.ErrorIs(t, err, ErrValueMismatch)
|
||||||
|
|
||||||
|
_, err = fc.StringSlice("invalidSlice")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
_, err = fc.StringSlice("mixed")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
v, err := fc.StringSlice("valid")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, []string{"ohai"}, v)
|
||||||
|
|
||||||
|
v, err = fc.StringSlice("validAny")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, []string{"ohai"}, v)
|
||||||
|
|
||||||
|
assert.True(t, fc.CanStringSlice("valid"))
|
||||||
|
assert.False(t, fc.CanStringSlice("bool"))
|
||||||
|
|
||||||
|
assert.NotPanics(t, func() { fc.MustStringSlice("valid", nil) })
|
||||||
|
assert.Panics(t, func() { fc.MustStringSlice("bool", nil) })
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"a"}, fc.MustStringSlice("_", func(v []string) *[]string { return &v }([]string{"a"})))
|
||||||
|
}
|
46
fieldcollection/typeString_test.go
Normal file
46
fieldcollection/typeString_test.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package fieldcollection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
testStringer struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (testStringer) String() string { return "ohai" }
|
||||||
|
|
||||||
|
func TestString(t *testing.T) {
|
||||||
|
fc := FieldCollectionFromData(map[string]any{
|
||||||
|
"int": 12,
|
||||||
|
"validString": "Ello!",
|
||||||
|
"stringer": testStringer{},
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := fc.String("_")
|
||||||
|
assert.ErrorIs(t, err, ErrValueNotSet)
|
||||||
|
|
||||||
|
_, err = fc.String("int")
|
||||||
|
assert.ErrorIs(t, err, ErrValueMismatch)
|
||||||
|
|
||||||
|
_, err = fc.String("invalidString")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
v, err := fc.String("validString")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "Ello!", v)
|
||||||
|
|
||||||
|
v, err = fc.String("stringer")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "ohai", v)
|
||||||
|
|
||||||
|
assert.True(t, fc.CanString("validString"))
|
||||||
|
assert.False(t, fc.CanString("bool"))
|
||||||
|
|
||||||
|
assert.NotPanics(t, func() { fc.MustString("validString", nil) })
|
||||||
|
assert.Panics(t, func() { fc.MustString("bool", nil) })
|
||||||
|
|
||||||
|
assert.Equal(t, "a", fc.MustString("_", func(v string) *string { return &v }("a")))
|
||||||
|
}
|
3
go.mod
3
go.mod
|
@ -8,7 +8,7 @@ require (
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
@ -17,5 +17,4 @@ require (
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
golang.org/x/sys v0.18.0 // indirect
|
golang.org/x/sys v0.18.0 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -28,8 +28,6 @@ golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// Package yaml contains a method to convert a YAML into a JSON object
|
||||||
package yaml
|
package yaml
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -6,7 +7,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ToJSON takes an io.Reader containing YAML source and converts it into
|
// ToJSON takes an io.Reader containing YAML source and converts it into
|
||||||
|
@ -15,14 +16,14 @@ func ToJSON(in io.Reader) (io.Reader, error) {
|
||||||
var body interface{}
|
var body interface{}
|
||||||
|
|
||||||
if err := yaml.NewDecoder(in).Decode(&body); err != nil {
|
if err := yaml.NewDecoder(in).Decode(&body); err != nil {
|
||||||
return nil, fmt.Errorf("Unable to unmarshal YAML: %s", err)
|
return nil, fmt.Errorf("unmarshaling YAML: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
body = convert(body)
|
body = convert(body)
|
||||||
|
|
||||||
var buf = new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
if err := json.NewEncoder(buf).Encode(body); err != nil {
|
if err := json.NewEncoder(buf).Encode(body); err != nil {
|
||||||
return nil, fmt.Errorf("Unable to marshal JSON: %s", err)
|
return nil, fmt.Errorf("marshaling JSON: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return buf, nil
|
return buf, nil
|
||||||
|
|
Loading…
Reference in a new issue