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 {
|
func Pretty(s string) *Component {
|
||||||
parts := strings.Split(s, "§")
|
parts := strings.Split(s, "§")
|
||||||
|
|
||||||
root := Component{}
|
root := Component{Text: parts[0]}
|
||||||
if parts[0] != "" {
|
|
||||||
root.Text = parts[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
var current = &root
|
var current = &root
|
||||||
for i := 1; i < len(parts); i++ {
|
for i := 1; i < len(parts); i++ {
|
||||||
|
|||||||
Reference in New Issue
Block a user