Skip to content

Commit

Permalink
Merge pull request #16 from wk8/jrouge/json
Browse files Browse the repository at this point in the history
Adding support for JSON serialization/deserialization
  • Loading branch information
wk8 authored Dec 7, 2022
2 parents ab40b2b + 08d8ef8 commit 6b91e87
Show file tree
Hide file tree
Showing 11 changed files with 403 additions and 59 deletions.
5 changes: 0 additions & 5 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ linters:
- bodyclose
- containedctx
- contextcheck
- cyclop
- deadcode
- decorder
- depguard
- dogsled
Expand All @@ -33,7 +31,6 @@ linters:
- gocritic
- gocyclo
- godox
- goerr113
- gofmt
- gofumpt
- goheader
Expand All @@ -54,7 +51,6 @@ linters:
- makezero
- misspell
- nakedret
- nestif
- nilerr
- nilnil
- noctx
Expand Down Expand Up @@ -83,4 +79,3 @@ linters:
- varnamelen
- wastedassign
- whitespace
- wrapcheck
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Changelog

# 2.1 - Dec 7th 2022
* Added support for JSON serialization/deserialization
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ all: test lint
# the TEST_FLAGS env var can be set to eg run only specific tests
.PHONY: test
test:
go test -v -count=1 -race -cover "$$TEST_FLAGS"
go test -v -count=1 -race -cover $(TEST_FLAGS)

.PHONY: bench
bench:
go test -bench=.

.PHONY: lint
lint:
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,19 @@ func main() {
}
```

`OrderedMap`s also support JSON serialization/deserialization, and preserves order:

```go
// serialization
data, err := json.Marshal(om)
...

// deserialization
om := orderedmap.New[string, string]() // or orderedmap.New[int, any](), or any type you expect
err := json.Unmarshal(data, &om)
...
```

## Alternatives

There are several other ordered map golang implementations out there, but I believe that at the time of writing none of them offer the same functionality as this library; more specifically:
Expand Down
19 changes: 19 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package orderedmap_test

import (
"encoding/json"
"fmt"

"github.com/wk8/go-ordered-map/v2"
Expand Down Expand Up @@ -32,6 +33,20 @@ func Example() {
}
}

fmt.Println("## JSON serialization: ##")
data, err := json.Marshal(om)
if err != nil {
panic(err)
}
fmt.Println(string(data))

fmt.Println("## JSON deserialization: ##")
om2 := orderedmap.New[string, string]()
if err := json.Unmarshal(data, &om2); err != nil {
panic(err)
}
fmt.Println(om2.Oldest().Key)

// Output:
// ## Get operations: ##
// bar true
Expand All @@ -43,4 +58,8 @@ func Example() {
// ## Iterating over the 2 newest pairs: ##
// coucou => toi
// bar => baz
// ## JSON serialization: ##
// {"foo":"bar","bar":"baz","coucou":"toi"}
// ## JSON deserialization: ##
// foo
}
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ go 1.18

require (
github.com/bahlo/generic-list-go v0.2.0
github.com/buger/jsonparser v1.1.1
github.com/davecgh/go-spew v1.1.0
github.com/mailru/easyjson v0.7.7
github.com/stretchr/testify v1.6.1
)

require (
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down
126 changes: 126 additions & 0 deletions json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package orderedmap

import (
"encoding"
"encoding/json"
"fmt"

"github.com/buger/jsonparser"
"github.com/mailru/easyjson/jwriter"
)

var (
_ json.Marshaler = &OrderedMap[int, any]{}
_ json.Unmarshaler = &OrderedMap[int, any]{}
)

// MarshalJSON implements the json.Marshaler interface.
func (om *OrderedMap[K, V]) MarshalJSON() ([]byte, error) {
writer := jwriter.Writer{}
writer.RawByte('{')

for pair, firstIteration := om.Oldest(), true; pair != nil; pair = pair.Next() {
if firstIteration {
firstIteration = false
} else {
writer.RawByte(',')
}

switch key := any(pair.Key).(type) {
case string:
writer.String(key)
case encoding.TextMarshaler:
writer.RawByte('"')
writer.Raw(key.MarshalText())
writer.RawByte('"')
case int:
writer.IntStr(key)
case int8:
writer.Int8Str(key)
case int16:
writer.Int16Str(key)
case int32:
writer.Int32Str(key)
case int64:
writer.Int64Str(key)
case uint:
writer.UintStr(key)
case uint8:
writer.Uint8Str(key)
case uint16:
writer.Uint16Str(key)
case uint32:
writer.Uint32Str(key)
case uint64:
writer.Uint64Str(key)
default:
return nil, fmt.Errorf("unsupported key type: %T", key)
}

writer.RawByte(':')
// the error is checked at the end of the function
writer.Raw(json.Marshal(pair.Value)) //nolint:errchkjson
}

writer.RawByte('}')

return writer.Buffer.Buf, writer.Error
}

// UnmarshalJSON implements the json.Unmarshaler interface.
func (om *OrderedMap[K, V]) UnmarshalJSON(data []byte) error {
return jsonparser.ObjectEach(
data,
func(keyData []byte, valueData []byte, dataType jsonparser.ValueType, offset int) error {
if dataType == jsonparser.String {
// jsonparser removes the enclosing quotes; we need to restore them to make a valid JSON
valueData = data[offset-len(valueData)-2 : offset]
}

var key K
var value V

if typedKeyPointer, ok := any(&key).(encoding.TextUnmarshaler); ok {
// pointer receiver
if err := typedKeyPointer.UnmarshalText(keyData); err != nil {
return err
}
} else {
keyAlreadyUnmarshalled := false
switch typedKey := any(key).(type) {
case string:
keyData = quoteString(keyData)
case encoding.TextUnmarshaler:
// not a pointer receiver
if err := typedKey.UnmarshalText(keyData); err != nil {
return err
}
keyAlreadyUnmarshalled = true
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
default:
return fmt.Errorf("unsupported key type: %T", typedKey)
}

if !keyAlreadyUnmarshalled {
if err := json.Unmarshal(keyData, &key); err != nil {
return err
}
}
}

if err := json.Unmarshal(valueData, &value); err != nil {
return err
}

om.Set(key, value)
return nil
})
}

func quoteString(data []byte) []byte {
withQuotes := make([]byte, len(data)+2) //nolint:gomnd
copy(withQuotes[1:], data)
withQuotes[0] = '"'
withQuotes[len(data)+1] = '"'
return withQuotes
}
Loading

0 comments on commit 6b91e87

Please sign in to comment.