mirror of
https://github.com/Luzifer/go_helpers.git
synced 2024-12-24 13:01: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
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
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")
|
||||
)
|
||||
|
||||
type FieldCollection struct {
|
||||
data map[string]interface{}
|
||||
lock sync.RWMutex
|
||||
}
|
||||
type (
|
||||
// FieldCollection holds a map with integrated locking and can
|
||||
// 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
|
||||
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
|
||||
func FieldCollectionFromData(data map[string]interface{}) *FieldCollection {
|
||||
//
|
||||
//revive:disable-next-line:exported
|
||||
func FieldCollectionFromData(data map[string]any) *FieldCollection {
|
||||
o := NewFieldCollection()
|
||||
o.SetFromData(data)
|
||||
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())
|
||||
func (f *FieldCollection) Clone() *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
|
||||
func (f *FieldCollection) Data() map[string]interface{} {
|
||||
func (f *FieldCollection) Data() map[string]any {
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
|
@ -73,7 +55,7 @@ func (f *FieldCollection) Data() map[string]interface{} {
|
|||
f.lock.RLock()
|
||||
defer f.lock.RUnlock()
|
||||
|
||||
out := make(map[string]interface{})
|
||||
out := make(map[string]any)
|
||||
for k := range f.data {
|
||||
out[k] = f.data[k]
|
||||
}
|
||||
|
@ -114,186 +96,9 @@ func (f *FieldCollection) HasAll(keys ...string) bool {
|
|||
return f.Expect(keys...) == 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
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// Get retrieves the value of a key as "any" type or returns an error
|
||||
// in case the field is not set
|
||||
func (f *FieldCollection) Get(name string) (any, error) {
|
||||
if f == nil || f.data == nil {
|
||||
return nil, errors.New("uninitialized field collection")
|
||||
}
|
||||
|
@ -306,62 +111,43 @@ func (f *FieldCollection) StringSlice(name string) ([]string, error) {
|
|||
return nil, ErrValueNotSet
|
||||
}
|
||||
|
||||
switch v := v.(type) {
|
||||
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
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Implement JSON marshalling to plain underlying map[string]interface{}
|
||||
|
||||
func (f *FieldCollection) MarshalJSON() ([]byte, error) {
|
||||
if f == nil || f.data == nil {
|
||||
return []byte("{}"), nil
|
||||
}
|
||||
|
||||
// Keys returns a list of all known keys
|
||||
func (f *FieldCollection) Keys() (keys []string) {
|
||||
f.lock.RLock()
|
||||
defer f.lock.RUnlock()
|
||||
|
||||
return json.Marshal(f.data)
|
||||
}
|
||||
|
||||
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")
|
||||
for k := range f.data {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
f.SetFromData(data)
|
||||
return nil
|
||||
return keys
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return f.Data(), nil
|
||||
}
|
||||
|
||||
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")
|
||||
if f.data == nil {
|
||||
f.data = make(map[string]any)
|
||||
}
|
||||
|
||||
f.SetFromData(data)
|
||||
return nil
|
||||
f.data[key] = value
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFieldCollectionJSONMarshal(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) {
|
||||
func TestExpect(t *testing.T) {
|
||||
var f *FieldCollection
|
||||
|
||||
f.Set("foo", "bar")
|
||||
|
||||
f = nil
|
||||
f.SetFromData(map[string]interface{}{"foo": "bar"})
|
||||
assert.NoError(t, f.Expect())
|
||||
assert.Error(t, f.Expect("foo"))
|
||||
}
|
||||
|
||||
func TestFieldCollectionNilClone(t *testing.T) {
|
||||
func TestFieldCollectionNilClone(*testing.T) {
|
||||
var f *FieldCollection
|
||||
|
||||
f.Clone()
|
||||
}
|
||||
|
||||
|
@ -68,13 +21,38 @@ func TestFieldCollectionNilDataGet(t *testing.T) {
|
|||
var f *FieldCollection
|
||||
|
||||
for name, fn := range map[string]func(name string) bool{
|
||||
"bool": f.CanBool,
|
||||
"duration": f.CanDuration,
|
||||
"int64": f.CanInt64,
|
||||
"string": f.CanString,
|
||||
"bool": f.CanBool,
|
||||
"duration": f.CanDuration,
|
||||
"int64": f.CanInt64,
|
||||
"string": f.CanString,
|
||||
"stringSlice": f.CanStringSlice,
|
||||
} {
|
||||
if fn("foo") {
|
||||
t.Errorf("%s key is available", name)
|
||||
}
|
||||
assert.False(t, fn("foo"), "%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/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.8.4
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
|
@ -17,5 +17,4 @@ require (
|
|||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // 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 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
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
|
||||
|
||||
import (
|
||||
|
@ -6,7 +7,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// 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{}
|
||||
|
||||
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)
|
||||
|
||||
var buf = new(bytes.Buffer)
|
||||
buf := new(bytes.Buffer)
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue