From 8de34dfda1a0300a2a08cde0effa49732cc0eec7 Mon Sep 17 00:00:00 2001 From: adman Date: Mon, 20 Apr 2026 23:16:01 +0200 Subject: [PATCH] mapstructure --- mapstructure/decode.go | 179 +++++++++ mapstructure/encode.go | 92 +++++ mapstructure/format.go | 110 ++++++ mapstructure/mapstructure_test.go | 595 ++++++++++++++++++++++++++++++ mapstructure/tags.go | 28 ++ text/text.go | 5 +- 6 files changed, 1005 insertions(+), 4 deletions(-) create mode 100644 mapstructure/decode.go create mode 100644 mapstructure/encode.go create mode 100644 mapstructure/format.go create mode 100644 mapstructure/mapstructure_test.go create mode 100644 mapstructure/tags.go diff --git a/mapstructure/decode.go b/mapstructure/decode.go new file mode 100644 index 0000000..9c9688b --- /dev/null +++ b/mapstructure/decode.go @@ -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 +} diff --git a/mapstructure/encode.go b/mapstructure/encode.go new file mode 100644 index 0000000..13767c3 --- /dev/null +++ b/mapstructure/encode.go @@ -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 + } +} diff --git a/mapstructure/format.go b/mapstructure/format.go new file mode 100644 index 0000000..480d527 --- /dev/null +++ b/mapstructure/format.go @@ -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) +} diff --git a/mapstructure/mapstructure_test.go b/mapstructure/mapstructure_test.go new file mode 100644 index 0000000..f65b3a5 --- /dev/null +++ b/mapstructure/mapstructure_test.go @@ -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") + } +} diff --git a/mapstructure/tags.go b/mapstructure/tags.go new file mode 100644 index 0000000..c6baabb --- /dev/null +++ b/mapstructure/tags.go @@ -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 +} diff --git a/text/text.go b/text/text.go index 637df05..17bf036 100644 --- a/text/text.go +++ b/text/text.go @@ -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++ {