Skip to content

Commit

Permalink
Sort extensions before serializing them in JSON
Browse files Browse the repository at this point in the history
This matches Go's behavior for built-in types, which
uses deterministic ordering (alphabetical for maps and
by order defined in file for structs).

Signed-off-by: Joseph Jon Booker <[email protected]>
  • Loading branch information
sargas committed Jan 18, 2025
1 parent a1fb808 commit 4c40d06
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 5 deletions.
17 changes: 15 additions & 2 deletions v2/event/event_marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"encoding/base64"
"fmt"
"io"
"slices"
"strings"

jsoniter "github.com/json-iterator/go"
Expand Down Expand Up @@ -179,10 +180,22 @@ func WriteJson(in *Event, writer io.Writer) error {
return fmt.Errorf("error while writing the event data: %w", stream.Error)
}

// Add extensions in a deterministic predictable order, similar to how Go maps are serialized in a predictable order.
type keyValue struct {
key string
val any
}
extensionKeyValues := make([]keyValue, 0, len(ext))
for k, v := range ext {
extensionKeyValues = append(extensionKeyValues, keyValue{k, v})
}
slices.SortFunc(extensionKeyValues, func(i keyValue, j keyValue) int {
return strings.Compare(i.key, j.key)
})
for _, v := range extensionKeyValues {
stream.WriteMore()
stream.WriteObjectField(k)
stream.WriteVal(v)
stream.WriteObjectField(v.key)
stream.WriteVal(v.val)
}

stream.WriteObjectEnd()
Expand Down
35 changes: 32 additions & 3 deletions v2/event/event_marshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
package event_test

import (
"bytes"
"encoding/base64"
"encoding/json"
"net/url"
"slices"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -384,20 +387,21 @@ func TestMarshal(t *testing.T) {
}
for n, tc := range testCases {
t.Run(n, func(t *testing.T) {
event := tc.event
testEvent := tc.event

for k, v := range tc.eventExtensions {
event.SetExtension(k, v)
testEvent.SetExtension(k, v)
}

gotBytes, err := json.Marshal(event)
gotBytes, err := json.Marshal(testEvent)

if tc.wantErr != nil {
require.Error(t, err, *tc.wantErr)
return
}

assertJsonEquals(t, tc.want, gotBytes)
assertExtensionsAreOrdered(t, tc.eventExtensions, gotBytes)
})
}
}
Expand All @@ -420,3 +424,28 @@ func assertJsonEquals(t *testing.T, want map[string]interface{}, got []byte) {

require.Equal(t, wantToCompare, gotToCompare)
}

func assertExtensionsAreOrdered(t *testing.T, extensions map[string]any, got []byte) {
type extensionLocation struct {
extension string
idx int
}
extensionLocations := make([]extensionLocation, 0, len(extensions))
for ext := range extensions {
extensionLocations = append(extensionLocations, extensionLocation{
extension: ext,
idx: bytes.Index(got, []byte(ext)),
})
}

// Sort by extension name
slices.SortFunc(extensionLocations, func(i, j extensionLocation) int {
return strings.Compare(i.extension, j.extension)
})
indexes := make([]int, 0, len(extensions))
for ext := range extensionLocations {
indexes = append(indexes, extensionLocations[ext].idx)
}

require.IsIncreasingf(t, indexes, "Expected ordered extensions, found extensions at: %+v", extensionLocations)
}

0 comments on commit 4c40d06

Please sign in to comment.