mapstructure
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
package mapstructure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func (f *Format) decode(m map[string]any, target reflect.Value) error {
|
||||
t := target.Type()
|
||||
usedKeys := map[string]bool{}
|
||||
|
||||
for i := range t.NumField() {
|
||||
field := t.Field(i)
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
|
||||
tag := parseTag(field.Tag.Get(f.Name))
|
||||
if tag.Skip {
|
||||
continue
|
||||
}
|
||||
|
||||
key := tag.Name
|
||||
if key == "" {
|
||||
key = field.Name
|
||||
}
|
||||
|
||||
val, exists := m[key]
|
||||
if !exists {
|
||||
if tag.Required || (f.requireAll && !tag.OmitEmpty) {
|
||||
return fmt.Errorf("%w: %s", ErrMissingRequired, key)
|
||||
}
|
||||
continue
|
||||
}
|
||||
usedKeys[key] = true
|
||||
|
||||
fieldVal := target.Field(i)
|
||||
if err := f.setField(fieldVal, val, tag); err != nil {
|
||||
return fmt.Errorf("field %s: %w", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
if f.errorOnExtra {
|
||||
for key := range m {
|
||||
if !usedKeys[key] {
|
||||
return fmt.Errorf("%w: %s", ErrExtraKey, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Format) setField(target reflect.Value, value any, opts tagOptions) error {
|
||||
if value == nil {
|
||||
if opts.OmitEmpty {
|
||||
return nil
|
||||
}
|
||||
target.Set(reflect.Zero(target.Type()))
|
||||
return nil
|
||||
}
|
||||
|
||||
targetType := target.Type()
|
||||
|
||||
if codec, ok := f.decoders[targetType]; ok {
|
||||
result, err := codec(value, opts.Raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
target.Set(reflect.ValueOf(result))
|
||||
return nil
|
||||
}
|
||||
|
||||
src := reflect.ValueOf(value)
|
||||
|
||||
if src.Type().AssignableTo(targetType) {
|
||||
target.Set(src)
|
||||
return nil
|
||||
}
|
||||
|
||||
if src.Type().ConvertibleTo(targetType) {
|
||||
target.Set(src.Convert(targetType))
|
||||
return nil
|
||||
}
|
||||
|
||||
if targetType.Kind() == reflect.Struct {
|
||||
nested, ok := value.(map[string]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot map %T to struct %s", value, targetType.Name())
|
||||
}
|
||||
nestedTarget := reflect.New(targetType).Elem()
|
||||
if err := f.decode(nested, nestedTarget); err != nil {
|
||||
return err
|
||||
}
|
||||
target.Set(nestedTarget)
|
||||
return nil
|
||||
}
|
||||
|
||||
if targetType.Kind() == reflect.Slice {
|
||||
return f.setSlice(target, value)
|
||||
}
|
||||
|
||||
if targetType.Kind() == reflect.Map {
|
||||
return f.setMap(target, value)
|
||||
}
|
||||
|
||||
if targetType.Kind() == reflect.Bool {
|
||||
return f.setBool(target, value)
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot set %s from %T", targetType, value)
|
||||
}
|
||||
|
||||
func (f *Format) setBool(target reflect.Value, value any) error {
|
||||
switch v := value.(type) {
|
||||
case int8:
|
||||
target.SetBool(v != 0)
|
||||
case int16:
|
||||
target.SetBool(v != 0)
|
||||
case int32:
|
||||
target.SetBool(v != 0)
|
||||
case int64:
|
||||
target.SetBool(v != 0)
|
||||
default:
|
||||
return fmt.Errorf("cannot convert %T to bool", value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Format) setSlice(target reflect.Value, value any) error {
|
||||
src, ok := value.([]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot map %T to slice", value)
|
||||
}
|
||||
|
||||
elemType := target.Type().Elem()
|
||||
slice := reflect.MakeSlice(target.Type(), 0, len(src))
|
||||
|
||||
for _, item := range src {
|
||||
elem := reflect.New(elemType).Elem()
|
||||
if err := f.setField(elem, item, tagOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
slice = reflect.Append(slice, elem)
|
||||
}
|
||||
|
||||
target.Set(slice)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Format) setMap(target reflect.Value, value any) error {
|
||||
src, ok := value.(map[string]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot map %T to map", value)
|
||||
}
|
||||
|
||||
mapType := target.Type()
|
||||
keyType := mapType.Key()
|
||||
elemType := mapType.Elem()
|
||||
|
||||
result := reflect.MakeMap(mapType)
|
||||
|
||||
for k, v := range src {
|
||||
keyVal := reflect.ValueOf(k)
|
||||
if !keyVal.Type().AssignableTo(keyType) {
|
||||
keyVal = keyVal.Convert(keyType)
|
||||
}
|
||||
|
||||
elemVal := reflect.New(elemType).Elem()
|
||||
if err := f.setField(elemVal, v, tagOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result.SetMapIndex(keyVal, elemVal)
|
||||
}
|
||||
|
||||
target.Set(result)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package mapstructure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func (f *Format) encode(v reflect.Value) (map[string]any, error) {
|
||||
t := v.Type()
|
||||
result := map[string]any{}
|
||||
|
||||
for i := range t.NumField() {
|
||||
field := t.Field(i)
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
|
||||
tag := parseTag(field.Tag.Get(f.Name))
|
||||
if tag.Skip {
|
||||
continue
|
||||
}
|
||||
|
||||
key := tag.Name
|
||||
if key == "" {
|
||||
key = field.Name
|
||||
}
|
||||
|
||||
fieldVal := v.Field(i)
|
||||
if tag.OmitEmpty && fieldVal.IsZero() {
|
||||
continue
|
||||
}
|
||||
|
||||
encoded, err := f.encodeField(fieldVal, tag.Raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("field %s: %w", key, err)
|
||||
}
|
||||
result[key] = encoded
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (f *Format) encodeField(v reflect.Value, tagStr string) (any, error) {
|
||||
if codec, ok := f.encoders[v.Type()]; ok {
|
||||
return codec(v.Interface(), tagStr)
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Struct:
|
||||
return f.encode(v)
|
||||
case reflect.Slice:
|
||||
if v.IsNil() {
|
||||
return []any{}, nil
|
||||
}
|
||||
slice := make([]any, v.Len())
|
||||
for i := range v.Len() {
|
||||
encoded, err := f.encodeField(v.Index(i), tagStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
slice[i] = encoded
|
||||
}
|
||||
return slice, nil
|
||||
case reflect.Map:
|
||||
if v.IsNil() {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
m := make(map[string]any, v.Len())
|
||||
iter := v.MapRange()
|
||||
for iter.Next() {
|
||||
key := iter.Key().String()
|
||||
encoded, err := f.encodeField(iter.Value(), tagStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m[key] = encoded
|
||||
}
|
||||
return m, nil
|
||||
case reflect.Pointer:
|
||||
if v.IsNil() {
|
||||
return nil, nil
|
||||
}
|
||||
return f.encodeField(v.Elem(), tagStr)
|
||||
case reflect.Bool:
|
||||
if v.Bool() {
|
||||
return int8(1), nil
|
||||
}
|
||||
return int8(0), nil
|
||||
default:
|
||||
return v.Interface(), nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package mapstructure
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotPointer = errors.New("target must be a pointer")
|
||||
ErrNilPointer = errors.New("target must not be a nil pointer")
|
||||
ErrNotStruct = errors.New("target must be a pointer to a struct")
|
||||
ErrNotMap = errors.New("data must be a map[string]any")
|
||||
ErrMissingRequired = errors.New("missing required field")
|
||||
ErrExtraKey = errors.New("map contains key with no matching struct field")
|
||||
)
|
||||
|
||||
type Format struct {
|
||||
Name string
|
||||
requireAll bool
|
||||
errorOnExtra bool
|
||||
decoders map[reflect.Type]func(any, string) (any, error)
|
||||
encoders map[reflect.Type]func(any, string) (any, error)
|
||||
}
|
||||
|
||||
func NewFormat(name string, opts ...Option) *Format {
|
||||
f := &Format{
|
||||
Name: name,
|
||||
decoders: make(map[reflect.Type]func(any, string) (any, error)),
|
||||
encoders: make(map[reflect.Type]func(any, string) (any, error)),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(f)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
type Option func(*Format)
|
||||
|
||||
func WithRequireAll(require bool) Option {
|
||||
return func(f *Format) {
|
||||
f.requireAll = require
|
||||
}
|
||||
}
|
||||
|
||||
func WithErrorOnExtra(errorOnExtra bool) Option {
|
||||
return func(f *Format) {
|
||||
f.errorOnExtra = errorOnExtra
|
||||
}
|
||||
}
|
||||
|
||||
func WithTypeCodec(decodeFn, encodeFn any) Option {
|
||||
dt := reflect.TypeOf(decodeFn)
|
||||
et := reflect.TypeOf(encodeFn)
|
||||
|
||||
if dt.Kind() != reflect.Func || dt.NumIn() != 2 || dt.NumOut() != 2 {
|
||||
panic("decode function must have signature func(any, string) (T, error)")
|
||||
}
|
||||
if et.Kind() != reflect.Func || et.NumIn() != 2 || et.NumOut() != 2 {
|
||||
panic("encode function must have signature func(T, string) (any, error)")
|
||||
}
|
||||
|
||||
targetType := dt.Out(0)
|
||||
sourceType := et.In(0)
|
||||
|
||||
return func(f *Format) {
|
||||
dv := reflect.ValueOf(decodeFn)
|
||||
f.decoders[targetType] = func(v any, tag string) (any, error) {
|
||||
results := dv.Call([]reflect.Value{reflect.ValueOf(v), reflect.ValueOf(tag)})
|
||||
err, _ := results[1].Interface().(error)
|
||||
return results[0].Interface(), err
|
||||
}
|
||||
|
||||
ev := reflect.ValueOf(encodeFn)
|
||||
f.encoders[sourceType] = func(v any, tag string) (any, error) {
|
||||
results := ev.Call([]reflect.Value{reflect.ValueOf(v), reflect.ValueOf(tag)})
|
||||
err, _ := results[1].Interface().(error)
|
||||
return results[0].Interface(), err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Format) Decode(data any, target any) error {
|
||||
v := reflect.ValueOf(target)
|
||||
if v.Kind() != reflect.Pointer || v.IsNil() {
|
||||
return ErrNotPointer
|
||||
}
|
||||
v = v.Elem()
|
||||
if v.Kind() != reflect.Struct {
|
||||
return ErrNotStruct
|
||||
}
|
||||
m, ok := data.(map[string]any)
|
||||
if !ok {
|
||||
return ErrNotMap
|
||||
}
|
||||
return f.decode(m, v)
|
||||
}
|
||||
|
||||
func (f *Format) Encode(source any) (map[string]any, error) {
|
||||
v := reflect.ValueOf(source)
|
||||
if v.Kind() == reflect.Pointer {
|
||||
if v.IsNil() {
|
||||
return nil, ErrNilPointer
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
if v.Kind() != reflect.Struct {
|
||||
return nil, ErrNotStruct
|
||||
}
|
||||
return f.encode(v)
|
||||
}
|
||||
@@ -0,0 +1,595 @@
|
||||
package mapstructure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
type Inner struct {
|
||||
Value int32 `nbt:"Value"`
|
||||
}
|
||||
|
||||
type Example struct {
|
||||
Name string `nbt:"Name"`
|
||||
Age int32 `nbt:"Age,omitempty"`
|
||||
Secret string `nbt:"Secret,required"`
|
||||
Skipped string `nbt:"-"`
|
||||
NoTag string
|
||||
Nested Inner `nbt:"Nested"`
|
||||
Flag bool `nbt:"Flag"`
|
||||
}
|
||||
|
||||
func TestBasicDecode(t *testing.T) {
|
||||
f := NewFormat("nbt")
|
||||
data := map[string]any{
|
||||
"Name": "test",
|
||||
"Secret": "shh",
|
||||
"NoTag": "fallback",
|
||||
"Nested": map[string]any{"Value": int32(42)},
|
||||
"Flag": int8(1),
|
||||
}
|
||||
var got Example
|
||||
if err := f.Decode(data, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := Example{Name: "test", Secret: "shh", NoTag: "fallback", Nested: Inner{Value: 42}, Flag: true}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingRequired(t *testing.T) {
|
||||
f := NewFormat("nbt")
|
||||
data := map[string]any{"Name": "test"}
|
||||
var got Example
|
||||
if err := f.Decode(data, &got); err == nil {
|
||||
t.Fatal("expected error for missing required field")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOmitEmpty(t *testing.T) {
|
||||
f := NewFormat("nbt")
|
||||
data := map[string]any{"Secret": "shh"}
|
||||
var got Example
|
||||
if err := f.Decode(data, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Age != 0 {
|
||||
t.Fatal("omitempty field should stay zero when absent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAll(t *testing.T) {
|
||||
type Simple struct {
|
||||
A int32 `nbt:"A"`
|
||||
B int32 `nbt:"B,omitempty"`
|
||||
C int32 `nbt:"C"`
|
||||
}
|
||||
f := NewFormat("nbt", WithRequireAll(true))
|
||||
data := map[string]any{"A": int32(1), "C": int32(3)}
|
||||
var got Simple
|
||||
if err := f.Decode(data, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.A != 1 || got.C != 3 {
|
||||
t.Fatal("unexpected values")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAllMissing(t *testing.T) {
|
||||
type Simple struct {
|
||||
A int32 `nbt:"A"`
|
||||
B int32 `nbt:"B"`
|
||||
}
|
||||
f := NewFormat("nbt", WithRequireAll(true))
|
||||
data := map[string]any{"A": int32(1)}
|
||||
var got Simple
|
||||
if err := f.Decode(data, &got); err == nil {
|
||||
t.Fatal("expected error for missing field with RequireAll")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAllOmitEmptyWins(t *testing.T) {
|
||||
type Simple struct {
|
||||
A int32 `nbt:"A"`
|
||||
B int32 `nbt:"B,omitempty"`
|
||||
}
|
||||
f := NewFormat("nbt", WithRequireAll(true))
|
||||
data := map[string]any{"A": int32(1)}
|
||||
var got Simple
|
||||
if err := f.Decode(data, &got); err != nil {
|
||||
t.Fatal("omitempty should override RequireAll")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkipTag(t *testing.T) {
|
||||
f := NewFormat("nbt")
|
||||
data := map[string]any{"Name": "test", "Secret": "shh", "Skipped": "should not appear"}
|
||||
var got Example
|
||||
if err := f.Decode(data, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Skipped != "" {
|
||||
t.Fatal("skipped field should not be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorOnExtra(t *testing.T) {
|
||||
f := NewFormat("nbt", WithErrorOnExtra(true))
|
||||
data := map[string]any{"Name": "test", "Secret": "shh", "Unknown": "extra"}
|
||||
var got Example
|
||||
if err := f.Decode(data, &got); err == nil {
|
||||
t.Fatal("expected error for extra key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoErrorOnExtraByDefault(t *testing.T) {
|
||||
f := NewFormat("nbt")
|
||||
data := map[string]any{"Name": "test", "Secret": "shh", "Unknown": "extra"}
|
||||
var got Example
|
||||
if err := f.Decode(data, &got); err != nil {
|
||||
t.Fatal("extra keys should be ignored by default")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotPointer(t *testing.T) {
|
||||
f := NewFormat("nbt")
|
||||
data := map[string]any{}
|
||||
var got Example
|
||||
if err := f.Decode(data, got); err == nil {
|
||||
t.Fatal("expected error for non-pointer target")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotStruct(t *testing.T) {
|
||||
f := NewFormat("nbt")
|
||||
data := map[string]any{}
|
||||
var got int32
|
||||
if err := f.Decode(data, &got); err == nil {
|
||||
t.Fatal("expected error for non-struct target")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotMap(t *testing.T) {
|
||||
f := NewFormat("nbt")
|
||||
var got Example
|
||||
if err := f.Decode("not a map", &got); err == nil {
|
||||
t.Fatal("expected error for non-map data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagMapping(t *testing.T) {
|
||||
type Renamed struct {
|
||||
GoName string `nbt:"MapKey"`
|
||||
}
|
||||
f := NewFormat("nbt")
|
||||
data := map[string]any{"MapKey": "hello"}
|
||||
var got Renamed
|
||||
if err := f.Decode(data, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.GoName != "hello" {
|
||||
t.Fatal("tag-mapped field not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeConversion(t *testing.T) {
|
||||
type Conv struct {
|
||||
I8 int8 `nbt:"I8"`
|
||||
I16 int16 `nbt:"I16"`
|
||||
I32 int32 `nbt:"I32"`
|
||||
I64 int64 `nbt:"I64"`
|
||||
F32 float32 `nbt:"F32"`
|
||||
F64 float64 `nbt:"F64"`
|
||||
Bool bool `nbt:"Bool"`
|
||||
}
|
||||
f := NewFormat("nbt")
|
||||
data := map[string]any{
|
||||
"I8": int8(1),
|
||||
"I16": int16(2),
|
||||
"I32": int32(3),
|
||||
"I64": int64(4),
|
||||
"F32": float32(5.5),
|
||||
"F64": float64(6.6),
|
||||
"Bool": int8(1),
|
||||
}
|
||||
var got Conv
|
||||
if err := f.Decode(data, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := Conv{I8: 1, I16: 2, I32: 3, I64: 4, F32: 5.5, F64: 6.6, Bool: true}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeConversionWidening(t *testing.T) {
|
||||
type Wide struct {
|
||||
I32FromI8 int32 `nbt:"A"`
|
||||
I64FromI16 int64 `nbt:"B"`
|
||||
}
|
||||
f := NewFormat("nbt")
|
||||
data := map[string]any{
|
||||
"A": int8(10),
|
||||
"B": int16(20),
|
||||
}
|
||||
var got Wide
|
||||
if err := f.Decode(data, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := Wide{I32FromI8: 10, I64FromI16: 20}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSliceDecode(t *testing.T) {
|
||||
type WithSlice struct {
|
||||
Items []int32 `nbt:"Items"`
|
||||
}
|
||||
f := NewFormat("nbt")
|
||||
data := map[string]any{
|
||||
"Items": []any{int32(1), int32(2), int32(3)},
|
||||
}
|
||||
var got WithSlice
|
||||
if err := f.Decode(data, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := WithSlice{Items: []int32{1, 2, 3}}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNestedStructDecode(t *testing.T) {
|
||||
type Outer struct {
|
||||
Inner Inner `nbt:"Inner"`
|
||||
}
|
||||
f := NewFormat("nbt")
|
||||
data := map[string]any{
|
||||
"Inner": map[string]any{"Value": int32(99)},
|
||||
}
|
||||
var got Outer
|
||||
if err := f.Decode(data, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := Outer{Inner: Inner{Value: 99}}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncode(t *testing.T) {
|
||||
f := NewFormat("nbt")
|
||||
s := Example{Name: "test", Secret: "shh", NoTag: "fallback", Nested: Inner{Value: 42}, Flag: true}
|
||||
got, err := f.Encode(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := map[string]any{
|
||||
"Name": "test",
|
||||
"Secret": "shh",
|
||||
"NoTag": "fallback",
|
||||
"Nested": map[string]any{"Value": int32(42)},
|
||||
"Flag": int8(1),
|
||||
}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeOmitEmpty(t *testing.T) {
|
||||
f := NewFormat("nbt")
|
||||
s := Example{Name: "test", Secret: "shh"}
|
||||
got, err := f.Encode(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, exists := got["Age"]; exists {
|
||||
t.Fatal("omitempty zero field should not appear in encoded map")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeSkipTag(t *testing.T) {
|
||||
f := NewFormat("nbt")
|
||||
s := Example{Name: "test", Secret: "shh", Skipped: "hidden"}
|
||||
got, err := f.Encode(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, exists := got["Skipped"]; exists {
|
||||
t.Fatal("skipped field should not appear in encoded map")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodePointer(t *testing.T) {
|
||||
f := NewFormat("nbt")
|
||||
s := &Example{Name: "test", Secret: "shh"}
|
||||
got, err := f.Encode(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got["Name"] != "test" {
|
||||
t.Fatal("encode from pointer should work")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeSlice(t *testing.T) {
|
||||
type WithSlice struct {
|
||||
Items []int32 `nbt:"Items"`
|
||||
}
|
||||
f := NewFormat("nbt")
|
||||
s := WithSlice{Items: []int32{1, 2, 3}}
|
||||
got, err := f.Encode(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
items, ok := got["Items"].([]any)
|
||||
if !ok {
|
||||
t.Fatal("slice not encoded correctly")
|
||||
}
|
||||
if len(items) != 3 || items[0].(int32) != 1 {
|
||||
t.Fatal("slice values not correct")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoundTrip(t *testing.T) {
|
||||
f := NewFormat("nbt")
|
||||
original := Example{Name: "hello", Secret: "world", Nested: Inner{Value: 99}, Flag: true}
|
||||
encoded, err := f.Encode(original)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var decoded Example
|
||||
if err := f.Decode(encoded, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if diff := cmp.Diff(original, decoded); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeCodecDecode(t *testing.T) {
|
||||
type GameMode int32
|
||||
|
||||
type Player struct {
|
||||
Mode GameMode `nbt:"Mode"`
|
||||
}
|
||||
|
||||
f := NewFormat("nbt",
|
||||
WithTypeCodec(
|
||||
func(v any, tag string) (GameMode, error) {
|
||||
i, ok := v.(int32)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("expected int32, got %T", v)
|
||||
}
|
||||
return GameMode(i), nil
|
||||
},
|
||||
func(v GameMode, tag string) (any, error) {
|
||||
return int32(v), nil
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
data := map[string]any{"Mode": int32(1)}
|
||||
var got Player
|
||||
if err := f.Decode(data, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Mode != GameMode(1) {
|
||||
t.Fatal("type codec decode did not work")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeCodecEncode(t *testing.T) {
|
||||
type GameMode int32
|
||||
|
||||
type Player struct {
|
||||
Mode GameMode `nbt:"Mode"`
|
||||
}
|
||||
|
||||
f := NewFormat("nbt",
|
||||
WithTypeCodec(
|
||||
func(v any, tag string) (GameMode, error) {
|
||||
i, ok := v.(int32)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("expected int32, got %T", v)
|
||||
}
|
||||
return GameMode(i), nil
|
||||
},
|
||||
func(v GameMode, tag string) (any, error) {
|
||||
return int32(v), nil
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
s := Player{Mode: GameMode(2)}
|
||||
got, err := f.Encode(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got["Mode"].(int32) != 2 {
|
||||
t.Fatal("type codec encode did not work")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeCodecRoundTrip(t *testing.T) {
|
||||
type GameMode int32
|
||||
|
||||
type Player struct {
|
||||
Mode GameMode `nbt:"Mode"`
|
||||
}
|
||||
|
||||
f := NewFormat("nbt",
|
||||
WithTypeCodec(
|
||||
func(v any, tag string) (GameMode, error) {
|
||||
i, ok := v.(int32)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("expected int32, got %T", v)
|
||||
}
|
||||
return GameMode(i), nil
|
||||
},
|
||||
func(v GameMode, tag string) (any, error) {
|
||||
return int32(v), nil
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
original := Player{Mode: GameMode(3)}
|
||||
encoded, err := f.Encode(original)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var decoded Player
|
||||
if err := f.Decode(encoded, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if diff := cmp.Diff(original, decoded); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeCodecReceivesTag(t *testing.T) {
|
||||
type GameMode int32
|
||||
|
||||
type Player struct {
|
||||
Mode GameMode `nbt:"Mode,strict"`
|
||||
Friends GameMode `nbt:"Friends"`
|
||||
}
|
||||
|
||||
strictModes := map[string]bool{}
|
||||
|
||||
f := NewFormat("nbt",
|
||||
WithTypeCodec(
|
||||
func(v any, tag string) (GameMode, error) {
|
||||
strictModes[tag] = true
|
||||
i, ok := v.(int32)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("expected int32, got %T", v)
|
||||
}
|
||||
return GameMode(i), nil
|
||||
},
|
||||
func(v GameMode, tag string) (any, error) {
|
||||
return int32(v), nil
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
data := map[string]any{"Mode": int32(1), "Friends": int32(2)}
|
||||
var got Player
|
||||
if err := f.Decode(data, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strictModes["Mode,strict"] {
|
||||
t.Fatalf("expected tag 'Mode,strict' to be passed to codec, got keys: %v", strictModes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleTypeCodecs(t *testing.T) {
|
||||
type GameMode int32
|
||||
type Dimension int32
|
||||
|
||||
type Player struct {
|
||||
Mode GameMode `nbt:"Mode"`
|
||||
Dim Dimension `nbt:"Dim"`
|
||||
}
|
||||
|
||||
f := NewFormat("nbt",
|
||||
WithTypeCodec(
|
||||
func(v any, tag string) (GameMode, error) {
|
||||
i, _ := v.(int32)
|
||||
return GameMode(i), nil
|
||||
},
|
||||
func(v GameMode, tag string) (any, error) {
|
||||
return int32(v), nil
|
||||
},
|
||||
),
|
||||
WithTypeCodec(
|
||||
func(v any, tag string) (Dimension, error) {
|
||||
i, _ := v.(int32)
|
||||
return Dimension(i), nil
|
||||
},
|
||||
func(v Dimension, tag string) (any, error) {
|
||||
return int32(v), nil
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
data := map[string]any{"Mode": int32(1), "Dim": int32(2)}
|
||||
var got Player
|
||||
if err := f.Decode(data, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Mode != GameMode(1) || got.Dim != Dimension(2) {
|
||||
t.Fatal("multiple type codecs did not work")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnexportedFieldsIgnored(t *testing.T) {
|
||||
type WithUnexported struct {
|
||||
Exported string `nbt:"Exported"`
|
||||
unexported string
|
||||
}
|
||||
f := NewFormat("nbt")
|
||||
data := map[string]any{"Exported": "yes", "unexported": "no"}
|
||||
var got WithUnexported
|
||||
if err := f.Decode(data, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.unexported != "" {
|
||||
t.Fatal("unexported fields should be ignored")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeMapField(t *testing.T) {
|
||||
type WithMap struct {
|
||||
Props map[string]string `nbt:"Props"`
|
||||
}
|
||||
f := NewFormat("nbt")
|
||||
s := WithMap{Props: map[string]string{"key": "val"}}
|
||||
got, err := f.Encode(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
props, ok := got["Props"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("map field not encoded correctly")
|
||||
}
|
||||
if props["key"] != "val" {
|
||||
t.Fatal("map field value incorrect")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeMapField(t *testing.T) {
|
||||
type WithMap struct {
|
||||
Props map[string]string `nbt:"Props"`
|
||||
}
|
||||
f := NewFormat("nbt")
|
||||
data := map[string]any{
|
||||
"Props": map[string]any{"key": "val"},
|
||||
}
|
||||
var got WithMap
|
||||
if err := f.Decode(data, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Props["key"] != "val" {
|
||||
t.Fatal("map field value incorrect")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeBoolFromInt32(t *testing.T) {
|
||||
type WithBool struct {
|
||||
Flag bool `nbt:"Flag"`
|
||||
}
|
||||
f := NewFormat("nbt")
|
||||
data := map[string]any{"Flag": int32(1)}
|
||||
var got WithBool
|
||||
if err := f.Decode(data, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !got.Flag {
|
||||
t.Fatal("bool from int32 should be true")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package mapstructure
|
||||
|
||||
import "strings"
|
||||
|
||||
type tagOptions struct {
|
||||
Name string
|
||||
OmitEmpty bool
|
||||
Required bool
|
||||
Skip bool
|
||||
Raw string
|
||||
}
|
||||
|
||||
func parseTag(raw string) tagOptions {
|
||||
if raw == "-" {
|
||||
return tagOptions{Skip: true, Raw: raw}
|
||||
}
|
||||
parts := strings.Split(raw, ",")
|
||||
opts := tagOptions{Name: parts[0], Raw: raw}
|
||||
for _, part := range parts[1:] {
|
||||
switch part {
|
||||
case "omitempty":
|
||||
opts.OmitEmpty = true
|
||||
case "required":
|
||||
opts.Required = true
|
||||
}
|
||||
}
|
||||
return opts
|
||||
}
|
||||
+1
-4
@@ -415,10 +415,7 @@ func ptr[T any](v T) *T {
|
||||
func Pretty(s string) *Component {
|
||||
parts := strings.Split(s, "§")
|
||||
|
||||
root := Component{}
|
||||
if parts[0] != "" {
|
||||
root.Text = parts[0]
|
||||
}
|
||||
root := Component{Text: parts[0]}
|
||||
|
||||
var current = &root
|
||||
for i := 1; i < len(parts); i++ {
|
||||
|
||||
Reference in New Issue
Block a user