From b528418bcfda45d100294a56ba8c9c5cd341cc81 Mon Sep 17 00:00:00 2001 From: Abdelrauf Date: Thu, 15 Jan 2026 22:11:51 +0400 Subject: [PATCH] added OrderedMap and other related methods, now SMap requires OrderedMap to properly encode and decode --- access/generic_decode.go | 141 +++++++++++-- access/get.go | 28 +++ access/get_test.go | 34 ++++ access/put.go | 27 +++ access/put_test.go | 69 +++++++ go.mod | 2 +- packable/pack_test.go | 85 ++++++++ packable/packable_mapPackables.go | 75 +++++++ scheme/scheme.go | 81 +++++--- scheme/scheme_test.go | 76 +++---- types/orderedmap.go | 328 ++++++++++++++++++++++++++++++ types/orderedmap_test.go | 208 +++++++++++++++++++ 12 files changed, 1073 insertions(+), 81 deletions(-) create mode 100644 types/orderedmap.go create mode 100644 types/orderedmap_test.go diff --git a/access/generic_decode.go b/access/generic_decode.go index 941e3f4..06526b9 100644 --- a/access/generic_decode.go +++ b/access/generic_decode.go @@ -62,11 +62,11 @@ func DecodePrimitive(typ types.Type, buf []byte) (interface{}, error) { } // DecodeTupleGeneric: decode a []any from the current position in a SeqGetAccess. -func DecodeTupleGeneric(seq *SeqGetAccess, root bool) ([]any, error) { +// If root is true, the caller already consumed the tuple header. +// If ordered is true, maps inside the tuple are decoded as *types.OrderedMapAny. +func DecodeTupleGeneric(seq *SeqGetAccess, root bool, ordered bool) ([]any, error) { nested := seq - pos := 0 if !root { - pos := seq.CurrentIndex() typ, width, err := seq.PeekTypeWidth() if err != nil { @@ -82,57 +82,66 @@ func DecodeTupleGeneric(seq *SeqGetAccess, root bool) ([]any, error) { } return nil, nil } - nested, err = seq.PeekNestedSeq() if err != nil { return nil, fmt.Errorf("DecodeTuple: nested peek failed at pos %d: %w", pos, err) } } + out := make([]any, 0, nested.ArgCount()) for i := 0; i < nested.ArgCount(); i++ { valTyp, _, err := nested.PeekTypeWidth() - if err != nil { - return nil, fmt.Errorf("DecodeMapAny: nested value decode error at %d: %w", i+1, err) - + return nil, fmt.Errorf("DecodeTuple: nested value peek error at %d: %w", i, err) } switch valTyp { case types.TypeMap: - v, err := DecodeMapAny(nested) // delegate + var v any + if ordered { + v, err = DecodeOrderedMapAny(nested) + } else { + v, err = DecodeMapAny(nested) + } if err != nil { - return nil, fmt.Errorf("DecodeMapAny: nested value decode error at %d: %w", i+1, err) + return nil, fmt.Errorf("DecodeTuple: nested map decode error at %d: %w", i, err) } out = append(out, v) + case types.TypeTuple: - v, err := DecodeTuple(nested) // delegate + v, err := DecodeTuple(nested) if err != nil { - return nil, fmt.Errorf("DecodeMapAny: nested value decode error at %d: %w", i+1, err) + return nil, fmt.Errorf("DecodeTuple: nested tuple decode error at %d: %w", i, err) } out = append(out, v) + default: valPayload, valTyp, err := nested.Next() if err != nil { - return nil, fmt.Errorf("DecodeMapAny: nested value decode error at %d: %w", i+1, err) + return nil, fmt.Errorf("DecodeTuple: nested value next error at %d: %w", i, err) } v, err := DecodePrimitive(valTyp, valPayload) - if err != nil { - return nil, fmt.Errorf("DecodeMapAny: nested value decode error at %d: %w", i+1, err) + return nil, fmt.Errorf("DecodeTuple: primitive decode error at %d: %w", i, err) } out = append(out, v) } - } + if !root { if err := seq.Advance(); err != nil { - return nil, fmt.Errorf("DecodeTuple: advance failed at pos %d: %w", pos, err) + return nil, fmt.Errorf("DecodeTuple: advance failed: %w", err) } } return out, nil } +// Convenience wrappers func DecodeTuple(seq *SeqGetAccess) ([]any, error) { - return DecodeTupleGeneric(seq, false) + return DecodeTupleGeneric(seq, false, false) +} + +func DecodeTupleOrdered(seq *SeqGetAccess) ([]any, error) { + return DecodeTupleGeneric(seq, false, true) } // DecodeMapAny: decode a map[string]any from the current position in a SeqGetAccess. @@ -208,15 +217,91 @@ func DecodeMapAny(seq *SeqGetAccess) (map[string]any, error) { return out, nil } +// DecodeOrderedMapAny decodes a map from the sequence into an OrderedMapAny, +// preserving insertion order of keys. +func DecodeOrderedMapAny(seq *SeqGetAccess) (*types.OrderedMapAny, error) { + pos := seq.CurrentIndex() + typ, width, err := seq.PeekTypeWidth() + if err != nil { + return nil, fmt.Errorf("DecodeOrderedMapAny: peek failed at pos %d: %w", pos, err) + } + if typ != types.TypeMap { + return nil, fmt.Errorf("DecodeOrderedMapAny: type mismatch at pos %d — expected %v, got %v", pos, types.TypeMap, typ) + } + if width == 0 { + // nil/empty map + if err := seq.Advance(); err != nil { + return nil, fmt.Errorf("DecodeOrderedMapAny: advance failed at pos %d: %w", pos, err) + } + return nil, nil + } + + nested, err := seq.PeekNestedSeq() + if err != nil { + return nil, fmt.Errorf("DecodeOrderedMapAny: nested peek failed at pos %d: %w", pos, err) + } + + out := types.NewOrderedMapAny() + for i := 0; i < nested.ArgCount(); i += 2 { + // key + keyPayload, keyTyp, err := nested.Next() + if err != nil { + return nil, fmt.Errorf("DecodeOrderedMapAny: key decode error at %d: %w", i, err) + } + if keyTyp != types.TypeString { + return nil, fmt.Errorf("DecodeOrderedMapAny: map key not string at %d, got %v", i, keyTyp) + } + key := string(keyPayload) + + valTyp, _, err := nested.PeekTypeWidth() + if err != nil { + return nil, fmt.Errorf("DecodeOrderedMapAny: nested value decode error at %d: %w", i+1, err) + } + + switch valTyp { + case types.TypeMap: + v, err := DecodeOrderedMapAny(nested) // delegate recursively + if err != nil { + return nil, fmt.Errorf("DecodeOrderedMapAny: nested value decode error at %d: %w", i+1, err) + } + out.Set(key, v) + + case types.TypeTuple: + v, err := DecodeTuple(nested) + if err != nil { + return nil, fmt.Errorf("DecodeOrderedMapAny: nested value decode error at %d: %w", i+1, err) + } + out.Set(key, v) + + default: + valPayload, valTyp, err := nested.Next() + if err != nil { + return nil, fmt.Errorf("DecodeOrderedMapAny: nested value decode error at %d: %w", i+1, err) + } + v, err := DecodePrimitive(valTyp, valPayload) + if err != nil { + return nil, fmt.Errorf("DecodeOrderedMapAny: nested value decode error at %d: %w", i+1, err) + } + out.Set(key, v) + } + } + + if err := seq.Advance(); err != nil { + return nil, fmt.Errorf("DecodeOrderedMapAny: advance failed at pos %d: %w", pos, err) + } + return out, nil +} + // Decode: convenience entry point for decoding a buffer that contains a top-level tuple. +// Decode decodes a buffer into Go values. +// Maps inside tuples are decoded as plain map[string]any. func Decode(buf []byte) (any, error) { seq, err := NewSeqGetAccess(buf) if err != nil { return nil, fmt.Errorf("Decode: failed to create sequence: %w", err) } - // delegate to tuple decoder - vals, err := DecodeTupleGeneric(seq, true) + vals, err := DecodeTupleGeneric(seq, true, false) // ordered=false if err != nil { return nil, fmt.Errorf("Decode: tuple decode failed: %w", err) } @@ -225,3 +310,21 @@ func Decode(buf []byte) (any, error) { } return vals, nil } + +// DecodeOrdered decodes a buffer into Go values. +// Maps inside tuples are decoded as *types.OrderedMapAny. +func DecodeOrdered(buf []byte) (any, error) { + seq, err := NewSeqGetAccess(buf) + if err != nil { + return nil, fmt.Errorf("DecodeOrdered: failed to create sequence: %w", err) + } + + vals, err := DecodeTupleGeneric(seq, true, true) // ordered=true + if err != nil { + return nil, fmt.Errorf("DecodeOrdered: tuple decode failed: %w", err) + } + if len(vals) == 1 { + return vals[0], nil + } + return vals, nil +} diff --git a/access/get.go b/access/get.go index 6a09f5c..8237e58 100644 --- a/access/get.go +++ b/access/get.go @@ -419,6 +419,34 @@ func (g *GetAccess) GetMapAny(pos int) (map[string]any, error) { return out, nil } +// GetMapOrderedAny decodes a map at the given position into an OrderedMapAny, +// preserving insertion order of keys. +func (g *GetAccess) GetMapOrderedAny(pos int) (*types.OrderedMapAny, error) { + tp, start, end := g.rangeAt(pos) + if end < start || tp != types.TypeMap { + return nil, errors.New("decode error") + } + if end == start { + return nil, nil // nil map + } + + nested := NewGetAccess(g.buf[start:end]) + out := types.NewOrderedMapAny() + + for i := 0; i < nested.argCount; i += 2 { + key, err := nested.GetString(i) + if err != nil { + return nil, fmt.Errorf("ordered map key decode error at %d: %w", i, err) + } + val, err := GetAny(nested, i+1) + if err != nil { + return nil, fmt.Errorf("ordered map value decode error at %d: %w", i+1, err) + } + out.Set(key, val) + } + return out, nil +} + func (g *GetAccess) GetMapStr(pos int) (map[string]string, error) { tp, start, end := g.rangeAt(pos) if end < start || tp != types.TypeMap { diff --git a/access/get_test.go b/access/get_test.go index df5b298..ca1d3f3 100644 --- a/access/get_test.go +++ b/access/get_test.go @@ -3,6 +3,7 @@ package access import ( "testing" + "github.com/quickwritereader/PackOS/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -61,6 +62,39 @@ func TestGetAccess_Map2(t *testing.T) { }, m) } +func TestGetAccess_MapOrderedAny(t *testing.T) { + buf := []byte{ + 0x27, 0x00, 0xE0, 0x00, + 0x56, 0x00, 0x26, 0x00, 0x4E, 0x00, 0x6E, 0x00, 0x90, 0x00, + 'r', 'o', 'l', 'e', + 'a', 'd', 'm', 'i', 'n', + 'u', 's', 'e', 'r', + 'a', 'l', 'i', 'c', 'e', + } + get := NewGetAccess(buf) + + om, err := get.GetMapOrderedAny(0) + require.NoError(t, err) + require.NotNil(t, om) + + // Build expected ordered map + expected := types.NewOrderedMapAny( + types.OPAny("role", "admin"), + types.OPAny("user", "alice"), + ) + + // Use Equal method to compare + assert.True(t, om.Equal(expected), "decoded OrderedMapAny does not match expected") + + // Also check insertion order explicitly + keys := []string{} + for k := range om.KeysIter() { + keys = append(keys, k) + } + + assert.Equal(t, []string{"role", "user"}, keys) +} + func TestGetAccess_IntThenMapWithInnerMapAndString(t *testing.T) { buf := []byte{ 0x31, 0x00, 0x17, 0x00, 0xB0, 0x01, diff --git a/access/put.go b/access/put.go index 5b315ba..0fb1bc7 100644 --- a/access/put.go +++ b/access/put.go @@ -479,6 +479,8 @@ func packAnyValue(p *PutAccess, v any, useNumeric bool) error { p.AddMap(val) case []string: p.AddStringArray(val) + case *types.OrderedMap[any]: + err = p.AddMapAnyOrdered(val, useNumeric) case []interface{}: err = p.AddAnyTuple(val, useNumeric) case Packable: @@ -525,6 +527,8 @@ func packAnyValueSortedMap(p *PutAccess, v any, useNumeric bool) error { p.AddMapAny(val, useNumeric) case map[string][]byte: p.AddMapSortedKey(val) + case *types.OrderedMap[any]: + err = p.AddMapAnyOrdered(val, useNumeric) case Packable: val.PackInto(p) case []interface{}: @@ -578,6 +582,29 @@ func (p *PutAccess) AddMapAnySortedKey(m map[string]any, useNumeric bool) error return nil } +// AddMapAnyOrdered encodes an OrderedMap[string→any] preserving insertion order. +func (p *PutAccess) AddMapAnyOrdered(om *types.OrderedMap[any], useNumeric bool) error { + // Write map header + p.offsets = binary.LittleEndian.AppendUint16( + p.offsets, + types.EncodeHeader(p.position, types.TypeMap), + ) + + if om != nil && om.Len() > 0 { + nested := NewPutAccessFromPool() + for k, v := range om.ItemsIter() { + // Add key + nested.AddString(k) + // Add value + if err := packAnyValue(nested, v, useNumeric); err != nil { + return fmt.Errorf("AddMapAnyOrdered: key %q: %w", k, err) + } + } + p.appendAndReleaseNested(nested) + } + return nil +} + func (p *PutAccess) appendAndReleaseNested(nested *PutAccess) { p.buf = nested.PackAppend(p.buf) diff --git a/access/put_test.go b/access/put_test.go index a462ddc..d1f8da2 100644 --- a/access/put_test.go +++ b/access/put_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/quickwritereader/PackOS/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -175,3 +176,71 @@ func TestPutAccess_NullableFloat32ExplicitBuffer(t *testing.T) { } } } + +func TestPutAccess_IntThenMapOrderedAny(t *testing.T) { + put := NewPutAccess() + + put.AddInt16(12345) // 2 bytes + + // Build inner ordered map for "meta" using OP helper + meta := types.NewOrderedMapAny( + types.OPAny("role", []byte("admin")), + types.OPAny("user", []byte("alice")), + ) + + // Build outer ordered map + outer := types.NewOrderedMapAny( + types.OPAny("meta", meta), + types.OPAny("name", "gopher"), + ) + + // Add the ordered map + if err := put.AddMapAnyOrdered(outer, false); err != nil { + t.Fatalf("AddMapAnyOrdered failed: %v", err) + } + + expected := []byte{ + // Outer Header Block (base = 0) + 0x31, 0x00, // absolute offset = 6, TypeInt16 + 0x17, 0x00, // delta = 2, TypeMap → offset = 8 + 0xB0, 0x01, // delta = 54, TypeEnd → offset = 60 + + // Outer Payload + 0x39, 0x30, // int16(12345) + + // inner1 Header Block (base = 8) + 0x56, 0x00, // "meta" + 0x27, 0x00, // map + 0x06, 0x01, // "name" + 0x26, 0x01, // "gopher" + 0x50, 0x01, // TypeEnd + + // inner1 Payload + 'm', 'e', 't', 'a', + + // inner1.1 Header Block (base = 12) + 0x56, 0x00, // "role" + 0x26, 0x00, // "admin" + 0x4E, 0x00, // "user" + 0x6E, 0x00, // "alice" + 0x90, 0x00, // TypeEnd + + // inner1.1 Payload + 'r', 'o', 'l', 'e', + 'a', 'd', 'm', 'i', 'n', + 'u', 's', 'e', 'r', + 'a', 'l', 'i', 'c', 'e', + + // Remaining inner1 Payload + 'n', 'a', 'm', 'e', + 'g', 'o', 'p', 'h', 'e', 'r', + } + + actual := put.Pack() + fmt.Printf("% X \n(%d)\n", actual, len(actual)) + + require.Equal(t, len(expected), len(actual), "Length mismatch: expected %d, got %d", len(expected), len(actual)) + for i := range expected { + assert.Equalf(t, expected[i], actual[i], "Byte %d mismatch: expected %02X, got %02X", i, expected[i], actual[i]) + } +} diff --git a/go.mod b/go.mod index 1298147..0479435 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/quickwritereader/PackOS -go 1.21 +go 1.23 require ( github.com/goccy/go-json v0.10.5 diff --git a/packable/pack_test.go b/packable/pack_test.go index 749ef2e..502b4a6 100644 --- a/packable/pack_test.go +++ b/packable/pack_test.go @@ -169,3 +169,88 @@ func TestPackable_TwoTuplesByteMatch(t *testing.T) { assert.Equalf(t, expected[i], actual[i], "Byte %d mismatch", i) } } + +func TestPackable_TestWithOrderedMaps(t *testing.T) { + // func PP(key string, value access.Packable) PackPair { + // return PackPair{Key: key, Value: value} + // } + + actual := Pack( + PackInt16(12345), + PackMapOrdered( + PP("meta", + PackMapOrdered( + PP("role", PackByteArray([]byte("admin"))), + PP("user", PackByteArray([]byte("alice"))), + ), + ), + PP("name", PackString("gopher")), + ), + ) + + expected := []byte{ + // Outer Header Block + 0x31, 0x00, // offset = 6, TypeInt16 + 0x17, 0x00, // delta = 2, TypeMap + 0xB0, 0x01, // delta = 54, TypeEnd + + // Outer Payload + 0x39, 0x30, // int16(12345) + + // inner1 Header Block + 0x56, 0x00, // "meta" + 0x27, 0x00, // map + 0x06, 0x01, // "name" + 0x26, 0x01, // "gopher" + 0x50, 0x01, // TypeEnd + + // inner1 Payload + 'm', 'e', 't', 'a', + + // inner1.1 Header Block + 0x56, 0x00, // "role" + 0x26, 0x00, // "admin" + 0x4E, 0x00, // "user" + 0x6E, 0x00, // "alice" + 0x90, 0x00, // TypeEnd + + // inner1.1 Payload + 'r', 'o', 'l', 'e', + 'a', 'd', 'm', 'i', 'n', + 'u', 's', 'e', 'r', + 'a', 'l', 'i', 'c', 'e', + + // Remaining inner1 Payload + 'n', 'a', 'm', 'e', + 'g', 'o', 'p', 'h', 'e', 'r', + } + + require.Equal(t, len(expected), len(actual), "Length mismatch: expected %d, got %d", len(expected), len(actual)) + for i := range expected { + assert.Equalf(t, expected[i], actual[i], "Byte %d mismatch: expected %02X, got %02X", i, expected[i], actual[i]) + } +} + +func TestPackable_TestPutAccessWithPackOrdered(t *testing.T) { + mapx := PackMapOrdered( + PP("meta", + PackMapOrdered( + PP("role", PackByteArray([]byte("admin"))), + PP("user", PackByteArray([]byte("alice"))), + ), + ), + PP("name", PackString("gopher")), + ) + + p := access.NewPutAccess() + p.AddInt16(12345) + p.AddPackable(mapx) + p.AddFloat32(4.45) + actual := p.Pack() + + expected := Pack(PackInt16(12345), mapx, PackFloat32(4.45)) + require.Equal(t, len(expected), len(actual), "Length mismatch: expected %d, got %d", len(expected), len(actual)) + for i := range expected { + assert.Equalf(t, expected[i], actual[i], "Byte %d mismatch: expected %02X, got %02X", i, expected[i], actual[i]) + } +} diff --git a/packable/packable_mapPackables.go b/packable/packable_mapPackables.go index 99f1d11..5522cc0 100644 --- a/packable/packable_mapPackables.go +++ b/packable/packable_mapPackables.go @@ -200,6 +200,71 @@ func (p PackMapStrInt64) Write(buf []byte, pos int) int { return pos } +// Pair for Packable values +type PackPair struct { + Key string + Value access.Packable +} + +func PP(key string, value access.Packable) PackPair { + return PackPair{Key: key, Value: value} +} + +// PackableMapOrdered packs Packable values using OrderedMap to preserve insertion order. +type PackableMapOrdered struct { + om *types.OrderedMap[access.Packable] +} + +// PackMapOrdered creates a new PackableMapOrdered, optionally initialized with pairs. +func PackMapOrdered(pairs ...PackPair) *PackableMapOrdered { + om := types.NewOrderedMap[access.Packable]() + for _, p := range pairs { + om.Set(p.Key, p.Value) + } + return &PackableMapOrdered{om: om} +} + +// Set adds or updates a key/value pair. +func (p *PackableMapOrdered) Set(key string, val access.Packable) { + p.om.Set(key, val) +} + +// ValueSize returns the size of the packed map's content. +func (p *PackableMapOrdered) ValueSize() int { + size := 0 + for k, v := range p.om.ItemsIter() { + size += len(k) + v.ValueSize() + } + return size + len(p.om.Keys())*2*access.HeaderTagSize + access.HeaderTagSize +} + +// HeaderType returns the type of the header for a map. +func (p *PackableMapOrdered) HeaderType() types.Type { + return types.TypeMap +} + +// Write packs the map into a byte buffer in insertion order. +func (p *PackableMapOrdered) Write(buf []byte, pos int) int { + headerSize := len(p.om.Keys())*2*access.HeaderTagSize + access.HeaderTagSize + first := pos + posH := pos + pos += headerSize + deltaStart := pos + + for k, v := range p.om.ItemsIter() { + posH = access.WriteTypeHeader(buf, posH, pos-deltaStart, types.TypeString) + pos = access.WriteString(buf, pos, k) + posH = access.WriteTypeHeader(buf, posH, pos-deltaStart, v.HeaderType()) + pos = v.Write(buf, pos) + } + + // End-of-container marker + _ = access.WriteTypeHeader(buf, first, headerSize, types.TypeString) + _ = access.WriteTypeHeader(buf, posH, pos-deltaStart, types.TypeEnd) + + return pos +} + func (pack PackMap) PackInto(p *access.PutAccess) { size := pack.ValueSize() buffer := bPool.Acquire(size) @@ -226,3 +291,13 @@ func (pack PackMapStr) PackInto(p *access.PutAccess) { p.AppendTagAndValue(types.TypeMap, buffer[:pos]) bPool.Release(buffer) } + +// PackInto packs the ordered map into the PutAccess buffer. +func (pack *PackableMapOrdered) PackInto(p *access.PutAccess) { + size := pack.ValueSize() + buffer := bPool.Acquire(size) + pos := 0 + pos = pack.Write(buffer, pos) + p.AppendTagAndValue(types.TypeMap, buffer[:pos]) + bPool.Release(buffer) +} diff --git a/scheme/scheme.go b/scheme/scheme.go index 6e4b1ae..2e340a9 100644 --- a/scheme/scheme.go +++ b/scheme/scheme.go @@ -11,7 +11,6 @@ import ( "github.com/quickwritereader/PackOS/access" "github.com/quickwritereader/PackOS/types" - "github.com/quickwritereader/PackOS/utils" ) type ErrorCode int @@ -180,7 +179,13 @@ func (f SchemeGeneric) Encode(put *access.PutAccess, val any) error { return f.EncodeFunc(put, val) } -type SchemeAny struct{} +type SchemeAny struct { + DecodeAsOrderedMap bool +} + +func SchemeAnyOrdered() SchemeAny { + return SchemeAny{DecodeAsOrderedMap: true} +} func (s SchemeAny) Validate(seq *access.SeqGetAccess) error { if err := seq.Advance(); err != nil { @@ -190,7 +195,7 @@ func (s SchemeAny) Validate(seq *access.SeqGetAccess) error { } func (s SchemeAny) Decode(seq *access.SeqGetAccess) (any, error) { - v, err := access.DecodeTupleGeneric(seq, true) + v, err := access.DecodeTupleGeneric(seq, true, s.DecodeAsOrderedMap) if err != nil { return nil, NewSchemeError(ErrInvalidFormat, SchemeAnyName, "", seq.CurrentIndex(), err) } @@ -273,6 +278,7 @@ type SchemeMap struct { Schema []Scheme } +// Validate checks that the sequence matches the SchemeMap definition. func (s SchemeMap) Validate(seq *access.SeqGetAccess) error { pos := seq.CurrentIndex() _, err := precheck(SchemeMapName, pos, seq, types.TypeMap, s.Width, s.IsNullable()) @@ -298,6 +304,7 @@ func (s SchemeMap) Validate(seq *access.SeqGetAccess) error { return nil } +// Decode reads a SchemeMap from the sequence into an OrderedMapAny. func (s SchemeMap) Decode(seq *access.SeqGetAccess) (any, error) { pos := seq.CurrentIndex() _, err := precheck(SchemeMapName, pos, seq, types.TypeMap, s.Width, s.IsNullable()) @@ -306,17 +313,23 @@ func (s SchemeMap) Decode(seq *access.SeqGetAccess) (any, error) { } if len(s.Schema)%2 != 0 { - return nil, NewSchemeError(ErrConstraintViolated, SchemeMapName, "", pos, SizeExact{Actual: len(s.Schema), Exact: len(s.Schema) + 1}) + return nil, NewSchemeError( + ErrConstraintViolated, + SchemeMapName, + "", + pos, + SizeExact{Actual: len(s.Schema), Exact: len(s.Schema) + 1}, + ) } - var out map[string]any + var out *types.OrderedMapAny if s.Width != 0 { sub, err := seq.PeekNestedSeq() if err != nil { return nil, NewSchemeError(ErrInvalidFormat, SchemeMapName, "", pos, err) } - out = make(map[string]any, sub.ArgCount()/2) + out = types.NewOrderedMapAny() for i := 0; i < len(s.Schema); i += 2 { key, err := s.Schema[i].Decode(sub) if err != nil { @@ -328,7 +341,7 @@ func (s SchemeMap) Decode(seq *access.SeqGetAccess) (any, error) { return nil, NewSchemeError(ErrInvalidFormat, SchemeMapName, keyStr, pos, err) } if keyStr, ok := key.(string); ok { - out[keyStr] = value + out.Set(keyStr, value) } else { return nil, NewSchemeError(ErrInvalidFormat, SchemeMapName, "", pos-1, ErrUnsupportedType) } @@ -341,34 +354,41 @@ func (s SchemeMap) Decode(seq *access.SeqGetAccess) (any, error) { return out, nil } +// Encode writes an OrderedMapAny into the sequence according to the SchemeMap. func (s SchemeMap) Encode(put *access.PutAccess, val any) error { if s.IsNullable() && val == nil && len(s.Schema) < 1 { put.AddMapAny(nil, true) } if len(s.Schema)%2 != 0 { - return NewSchemeError(ErrConstraintViolated, SchemeMapName, "", -1, SizeExact{Actual: len(s.Schema), Exact: len(s.Schema) + 1}) - } - - if mapKV, ok := val.(map[string]any); ok { - keys := utils.SortKeys(mapKV) - if len(keys) != len(s.Schema)/2 { - return NewSchemeError(ErrInvalidFormat, SchemeMapName, "", -1, SizeExact{Actual: len(keys), Exact: len(s.Schema) / 2}) + return NewSchemeError( + ErrConstraintViolated, + SchemeMapName, + "", + -1, + SizeExact{Actual: len(s.Schema), Exact: len(s.Schema) + 1}, + ) + } + + if om, ok := val.(*types.OrderedMapAny); ok { + if om.Len() != len(s.Schema)/2 { + return NewSchemeError(ErrInvalidFormat, SchemeMapName, + "", -1, SizeExact{Actual: om.Len(), Exact: len(s.Schema) / 2}, + ) } nested := put.BeginMap() defer put.EndNested(nested) - j := 0 - for i := 0; i < len(s.Schema); i += 2 { - k := keys[j] - err := s.Schema[i].Encode(nested, k) - if err != nil { + + i := 0 + + for k, v := range om.ItemsIter() { + if err := s.Schema[i].Encode(nested, k); err != nil { return NewSchemeError(ErrInvalidFormat, SchemeMapName, k, -1, err) } - err = s.Schema[i+1].Encode(nested, mapKV[k]) - if err != nil { + if err := s.Schema[i+1].Encode(nested, v); err != nil { return NewSchemeError(ErrInvalidFormat, SchemeMapName, k, -1, err) } - j++ + i += 2 } } else { @@ -378,7 +398,8 @@ func (s SchemeMap) Encode(put *access.PutAccess, val any) error { } type SchemeTypeOnly struct { - Tag types.Type + Tag types.Type + DecodeOrdereMap bool } func (s SchemeTypeOnly) Validate(seq *access.SeqGetAccess) error { @@ -388,6 +409,9 @@ func (s SchemeTypeOnly) Validate(seq *access.SeqGetAccess) error { func (s SchemeTypeOnly) Decode(seq *access.SeqGetAccess) (any, error) { switch s.Tag { case types.TypeMap: + if s.DecodeOrdereMap { + return access.DecodeMapAny(seq) + } return access.DecodeMapAny(seq) case types.TypeTuple: return access.DecodeTuple(seq) @@ -468,9 +492,16 @@ func (s SchemeTypeOnly) Encode(put *access.PutAccess, val any) error { } case types.TypeMap: - if v, ok := val.(map[string]any); ok { + switch v := val.(type) { + case map[string]any: put.AddMapAny(v, true) - } else { + + case *types.OrderedMapAny: + if err := put.AddMapAnyOrdered(v, true); err != nil { + return NewSchemeError(ErrEncode, SchemeTypeOnlyName, "", -1, err) + } + + default: return NewSchemeError(ErrEncode, SchemeTypeOnlyName, "", -1, ErrTypeMisMatch) } diff --git a/scheme/scheme_test.go b/scheme/scheme_test.go index 49397d0..108b4d5 100644 --- a/scheme/scheme_test.go +++ b/scheme/scheme_test.go @@ -253,10 +253,11 @@ func TestDecodePackedStructure(t *testing.T) { expectedFloat32 := float32(3.14) expectedInt64 := int64(9876543210) expectedBool := true - expectedMeta := map[string]any{ - "role": []byte("admin"), - "user": []byte("alice"), - } + expectedMeta := types.NewOrderedMapAny( + types.OPAny("role", []byte("admin")), + types.OPAny("user", []byte("alice")), + ) + expectedName := "gopher" // ret should be []any @@ -268,18 +269,21 @@ func TestDecodePackedStructure(t *testing.T) { assert.Equal(t, expectedFloat32, resultSlice[1]) assert.Equal(t, expectedInt64, resultSlice[2]) assert.Equal(t, expectedBool, resultSlice[3]) - // Check map values without assuming order - resultMap, ok := resultSlice[4].(map[string]any) - assert.True(t, ok, "Last element should be a map") + resultMap, ok := resultSlice[4].(*types.OrderedMapAny) + assert.True(t, ok, "Last element should be an OrderedMapAny") // Compare "meta" submap - metaMap, ok := resultMap["meta"].(map[string]any) - assert.True(t, ok, "meta should be a map") - assert.EqualValues(t, expectedMeta, metaMap) + metaMap, ok := resultMap.Get("meta") + assert.True(t, ok, "meta should exist") + omMeta, ok := metaMap.(*types.OrderedMapAny) + assert.True(t, ok, "meta should be an OrderedMapAny") + assert.True(t, omMeta.Equal(expectedMeta), "meta OrderedMapAny does not match expected") // Compare "name" - assert.Equal(t, expectedName, resultMap["name"]) + valName, ok := resultMap.Get("name") + assert.True(t, ok, "name should exist") + assert.Equal(t, expectedName, valName) } @@ -315,17 +319,16 @@ func TestDecodePackedMapUnOrderedOptional(t *testing.T) { } expectedName := "gopher" - // Check map values without assuming order - resultMap, ok := ret.(map[string]any) - assert.True(t, ok, "element should be a map") + // Top-level result should be an OrderedMapAny + resultMap, ok := ret.(*types.OrderedMapAny) + assert.True(t, ok, "element should be an OrderedMapAny") // Compare "meta" submap - metaMap, ok := resultMap["meta"].(map[string]any) - assert.True(t, ok, "meta should be a map") + metaMap := types.GetAs[map[string]any](resultMap, "meta") assert.EqualValues(t, expectedMeta, metaMap) // Compare "name" - assert.Equal(t, expectedName, resultMap["name"]) + assert.Equal(t, expectedName, types.GetAs[string](resultMap, "name")) } @@ -1030,13 +1033,13 @@ func TestEncodePackedStructure(t *testing.T) { float32(3.14), int64(9876543210), true, - map[string]any{ - "meta": map[string]any{ - "role": []byte("admin"), - "user": []byte("alice"), - }, - "name": "gopher", - }, + types.NewOrderedMapAny( + types.OPAny("meta", types.NewOrderedMapAny( + types.OPAny("role", []byte("admin")), + types.OPAny("user", []byte("alice")), + )), + types.OPAny("name", "gopher"), + ), } actual, err := EncodeValue(val, chain) @@ -1065,14 +1068,14 @@ func TestEncodePackedStructure_WithInvalidValues(t *testing.T) { ) // Value to encode - val := map[string]any{ - "meta": map[string]any{ + val := types.NewOrderedMapAny( + types.OPAny("meta", map[string]any{ "user": []byte("alice"), "role": "adminX", // invalid "age": int32(17), // out of range - }, - "name": "gopher", - } + }), + types.OPAny("name", "gopher"), + ) actual, err := EncodeValue(val, chain) @@ -1099,14 +1102,15 @@ func TestEncodePackedStructure_WithValidValues(t *testing.T) { ) // Value to encode - val := map[string]any{ - "meta": map[string]any{ + val := types.NewOrderedMapAny( + types.OPAny("meta", map[string]any{ "user": []byte("alice"), - "role": "admin", // valid - "age": int32(25), // valid - }, - "name": "gopher", - } + "role": "admin", + "age": int32(27), + }), + types.OPAny("name", "gopher"), + ) + actual, err := EncodeValue(val, chain) assert.NoError(t, err, "Encoding should succeed with valid values") diff --git a/types/orderedmap.go b/types/orderedmap.go new file mode 100644 index 0000000..dfa57c0 --- /dev/null +++ b/types/orderedmap.go @@ -0,0 +1,328 @@ +package types + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "iter" + "reflect" +) + +// NOTE: Portions of this code were generated with AI assistance under human guidance. + +// Go 1.23 iterator package + +// Pair represents a key/value pair for initialization +type Pair[V any] struct { + Key string + Value V +} + +func OP[V any](k string, v V) Pair[V] { + return Pair[V]{Key: k, Value: v} +} + +// Alias for Pair[any] +type PairAny = Pair[any] + +// OPAny is a helper to construct a Pair[any] inline. +func OPAny(k string, v any) PairAny { + return PairAny{Key: k, Value: v} +} + +// node now contains key directly +type node[V any] struct { + key string + value V + prev *node[V] + next *node[V] +} + +// OrderedMap is a generic ordered map keyed by string +type OrderedMap[V any] struct { + data map[string]*node[V] // key → node + head *node[V] + tail *node[V] +} + +// NewOrderedMap creates a new OrderedMap, optionally initialized with pairs. +func NewOrderedMap[V any](pairs ...Pair[V]) *OrderedMap[V] { + om := &OrderedMap[V]{ + data: make(map[string]*node[V]), + } + for _, p := range pairs { + om.Set(p.Key, p.Value) + } + return om +} + +// Alias for OrderedMap with any values +type OrderedMapAny = OrderedMap[any] + +// NewOrderedMapAny creates an OrderedMap[any] initialized with pairs. +func NewOrderedMapAny(pairs ...PairAny) *OrderedMapAny { + om := &OrderedMapAny{ + data: make(map[string]*node[any]), + } + for _, p := range pairs { + om.Set(p.Key, p.Value) + } + return om +} + +// Length +func (om *OrderedMap[V]) Len() int { + return len(om.data) +} + +// Set inserts or updates a key +func (om *OrderedMap[V]) Set(key string, value V) { + if n, ok := om.data[key]; ok { + n.value = value + return + } + n := &node[V]{key: key, value: value} + om.data[key] = n + if om.tail == nil { + om.head, om.tail = n, n + } else { + n.prev = om.tail + om.tail.next = n + om.tail = n + } +} + +// Get retrieves a value +func (om *OrderedMap[V]) Get(key string) (V, bool) { + n, ok := om.data[key] + if !ok { + var zero V + return zero, false + } + return n.value, true +} + +func GetAs[U any](om *OrderedMapAny, key string) U { + v, ok := om.Get(key) // returns any + if !ok { + var zero U + return zero + } + u, ok := v.(U) + if !ok { + var zero U + return zero + } + return u +} + +// Delete removes a key +func (om *OrderedMap[V]) Delete(key string) { + n, ok := om.data[key] + if !ok { + return + } + delete(om.data, key) + if n.prev != nil { + n.prev.next = n.next + } else { + om.head = n.next + } + if n.next != nil { + n.next.prev = n.prev + } else { + om.tail = n.prev + } +} + +// Keys returns keys in insertion order +func (om *OrderedMap[V]) Keys() []string { + keys := []string{} + for n := om.head; n != nil; n = n.next { + keys = append(keys, n.key) + } + return keys +} + +// Values returns values in insertion order +func (om *OrderedMap[V]) Values() []V { + values := []V{} + for n := om.head; n != nil; n = n.next { + values = append(values, n.value) + } + return values +} + +// Items returns key/value pairs in insertion order +func (om *OrderedMap[V]) Items() []Pair[V] { + items := []Pair[V]{} + for n := om.head; n != nil; n = n.next { + items = append(items, Pair[V]{Key: n.key, Value: n.value}) + } + return items +} + +// MoveToEnd moves a key to front or back +func (om *OrderedMap[V]) MoveToEnd(key string, last bool) error { + n, ok := om.data[key] + if !ok { + return errors.New("key not found") + } + // detach + if n.prev != nil { + n.prev.next = n.next + } else { + om.head = n.next + } + if n.next != nil { + n.next.prev = n.prev + } else { + om.tail = n.prev + } + // attach + if last { + n.prev, n.next = om.tail, nil + if om.tail != nil { + om.tail.next = n + } + om.tail = n + if om.head == nil { + om.head = n + } + } else { + n.prev, n.next = nil, om.head + if om.head != nil { + om.head.prev = n + } + om.head = n + if om.tail == nil { + om.tail = n + } + } + return nil +} + +func (om *OrderedMap[V]) Equal(other *OrderedMap[V]) bool { + if om.Len() != other.Len() { + return false + } + n1, n2 := om.head, other.head + for n1 != nil && n2 != nil { + if n1.key != n2.key { + return false + } + if !reflect.DeepEqual(n1.value, n2.value) { + return false + } + n1, n2 = n1.next, n2.next + } + return true +} + +// MarshalJSON encodes as JSON object in insertion order +func (om *OrderedMap[V]) MarshalJSON() ([]byte, error) { + buf := []byte{'{'} + i := 0 + for n := om.head; n != nil; n = n.next { + keyBytes, err := json.Marshal(n.key) + if err != nil { + return nil, err + } + valBytes, err := json.Marshal(n.value) + if err != nil { + return nil, err + } + buf = append(buf, keyBytes...) + buf = append(buf, ':') + buf = append(buf, valBytes...) + if i < len(om.data)-1 { + buf = append(buf, ',') + } + i++ + } + buf = append(buf, '}') + return buf, nil +} + +// UnmarshalJSON decodes JSON object preserving order +func (om *OrderedMap[V]) UnmarshalJSON(data []byte) error { + *om = *NewOrderedMap[V]() + dec := json.NewDecoder(bytes.NewReader(data)) + + t, err := dec.Token() + if err != nil { + return err + } + if d, ok := t.(json.Delim); !ok || d != '{' { + return fmt.Errorf("expected {") + } + for dec.More() { + t, err := dec.Token() + if err != nil { + return err + } + key, ok := t.(string) + if !ok { + return fmt.Errorf("expected string key") + } + var val V + if err := dec.Decode(&val); err != nil { + return err + } + om.Set(key, val) + } + t, err = dec.Token() + if err != nil { + return err + } + if d, ok := t.(json.Delim); !ok || d != '}' { + return fmt.Errorf("expected }") + } + return nil +} + +// KeysIter returns an iterator over keys +func (om *OrderedMap[V]) KeysIter() iter.Seq[string] { + return func(yield func(string) bool) { + for n := om.head; n != nil; n = n.next { + if !yield(n.key) { + return + } + } + } +} + +// ValuesIter returns an iterator over values +func (om *OrderedMap[V]) ValuesIter() iter.Seq[V] { + return func(yield func(V) bool) { + for n := om.head; n != nil; n = n.next { + if !yield(n.value) { + return + } + } + } +} + +// ItemsIter returns an iterator over key/value pairs +func (om *OrderedMap[V]) ItemsIter() iter.Seq2[string, V] { + return func(yield func(string, V) bool) { + for n := om.head; n != nil; n = n.next { + if !yield(n.key, n.value) { + return + } + } + } +} + +// ConvertUnorderedToOrdered takes a plain map[string]any and a desired key order, +// and returns an OrderedMapAny with keys in that order. +func ConvertUnorderedToOrdered(m map[string]any, order []string) *OrderedMapAny { + om := NewOrderedMapAny() + for _, k := range order { + if v, ok := m[k]; ok { + om.Set(k, v) + } + } + return om +} diff --git a/types/orderedmap_test.go b/types/orderedmap_test.go new file mode 100644 index 0000000..261b9f3 --- /dev/null +++ b/types/orderedmap_test.go @@ -0,0 +1,208 @@ +package types + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSetAndGet(t *testing.T) { + om := NewOrderedMapAny() + om.Set("a", 1) + om.Set("b", "two") + + v, ok := om.Get("a") + require.True(t, ok) + assert.Equal(t, 1, v) + + v, ok = om.Get("b") + require.True(t, ok) + assert.Equal(t, "two", v) + + _, ok = om.Get("c") + assert.False(t, ok, "expected missing key") +} + +func TestUpdateValue(t *testing.T) { + om := NewOrderedMapAny() + om.Set("x", 10) + om.Set("x", 20) + + v, ok := om.Get("x") + require.True(t, ok) + assert.Equal(t, 20, v) +} + +func TestDelete(t *testing.T) { + om := NewOrderedMapAny() + om.Set("a", 1) + om.Set("b", 2) + om.Delete("a") + + _, ok := om.Get("a") + assert.False(t, ok, "expected 'a' deleted") + + v, ok := om.Get("b") + require.True(t, ok) + assert.Equal(t, 2, v) +} + +func TestKeysValuesItems(t *testing.T) { + om := NewOrderedMapAny() + om.Set("a", 1) + om.Set("b", 2) + om.Set("c", 3) + + keys := om.Keys() + assert.Equal(t, []string{"a", "b", "c"}, keys) + + values := om.Values() + assert.Equal(t, []interface{}{1, 2, 3}, values) + + items := om.Items() + expected := []PairAny{ + {Key: "a", Value: 1}, + {Key: "b", Value: 2}, + {Key: "c", Value: 3}, + } + assert.Equal(t, expected, items) + +} + +func TestMoveToEnd(t *testing.T) { + om := NewOrderedMapAny() + om.Set("a", 1) + om.Set("b", 2) + om.Set("c", 3) + + err := om.MoveToEnd("b", true) + require.NoError(t, err) + assert.Equal(t, []string{"a", "c", "b"}, om.Keys()) + + err = om.MoveToEnd("c", false) + require.NoError(t, err) + assert.Equal(t, []string{"c", "a", "b"}, om.Keys()) + + err = om.MoveToEnd("x", true) + assert.Error(t, err, "expected error for missing key") +} + +func TestMarshalUnmarshalJSON(t *testing.T) { + om := NewOrderedMapAny() + om.Set("a", 1) + om.Set("b", "two") + + data, err := json.Marshal(om) + require.NoError(t, err) + + var om2 OrderedMapAny + err = json.Unmarshal(data, &om2) + require.NoError(t, err) + + assert.Equal(t, []string{"a", "b"}, om2.Keys()) + + v, ok := om2.Get("b") + require.True(t, ok) + assert.Equal(t, "two", v) +} + +func TestIterators(t *testing.T) { + om := NewOrderedMapAny() + om.Set("a", 1) + om.Set("b", 2) + + expectedKeys := []string{"a", "b"} + expectedVals := []int{1, 2} + + // KeysIter + i := 0 + for k := range om.KeysIter() { + assert.Equal(t, expectedKeys[i], k) + i++ + } + assert.Equal(t, len(expectedKeys), i) + + // ValuesIter + i = 0 + for v := range om.ValuesIter() { + assert.Equal(t, expectedVals[i], v) + i++ + } + assert.Equal(t, len(expectedVals), i) + + // ItemsIter + i = 0 + for k, v := range om.ItemsIter() { + assert.Equal(t, expectedKeys[i], k) + assert.Equal(t, expectedVals[i], v) + i++ + } + assert.Equal(t, len(expectedKeys), i) +} + +func TestMarshalJSON(t *testing.T) { + om := NewOrderedMapAny( + PairAny{"a", 1}, + PairAny{"b", "two"}, + PairAny{"c", true}, + ) + + data, err := json.Marshal(om) + require.NoError(t, err) + + // JSON should be in insertion order + expected := `{"a":1,"b":"two","c":true}` + assert.JSONEq(t, expected, string(data)) +} + +func TestUnmarshalJSON(t *testing.T) { + jsonData := `{"x":42,"y":"hello","z":false}` + + var om OrderedMapAny + err := json.Unmarshal([]byte(jsonData), &om) + require.NoError(t, err) + + // Keys should preserve order + assert.Equal(t, []string{"x", "y", "z"}, om.Keys()) + + // Values should match + v, ok := om.Get("x") + require.True(t, ok) + assert.Equal(t, float64(42), v) // JSON numbers decode as float64 + + v, ok = om.Get("y") + require.True(t, ok) + assert.Equal(t, "hello", v) + + v, ok = om.Get("z") + require.True(t, ok) + assert.Equal(t, false, v) +} + +func TestMarshalUnmarshalRoundTrip(t *testing.T) { + om := NewOrderedMapAny( + PairAny{"first", 123}, + PairAny{"second", "abc"}, + ) + + data, err := json.Marshal(om) + require.NoError(t, err) + + var om2 OrderedMapAny + err = json.Unmarshal(data, &om2) + require.NoError(t, err) + + // Keys preserved + assert.Equal(t, []string{"first", "second"}, om2.Keys()) + + // Values preserved + v, ok := om2.Get("first") + require.True(t, ok) + assert.Equal(t, float64(123), v) // JSON numbers decode as float64 + + v, ok = om2.Get("second") + require.True(t, ok) + assert.Equal(t, "abc", v) +}