Skip to content

Commit

Permalink
Merge pull request #21 from wk8/jrouge/lazy_init_unmarshall
Browse files Browse the repository at this point in the history
v2.1.2
  • Loading branch information
wk8 authored Dec 11, 2022
2 parents a449e02 + 819117c commit 10e9fad
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 18 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

# 2.1.2 - Dec 10th 2022
* Allowing to pass options to `New`, to give a capacity hint, or initial data
* Allowing to deserialize nested ordered maps from JSON without having to explicitly instantiate them
* Added the `AddPairs` method

# 2.1.1 - Dec 9th 2022
* Fixing a bug with JSON marshalling

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

Also worth noting that you can provision ordered maps with a capacity hint, as you would do by passing an optional hint to `make(map[K]V, capacity`):
```go
om := orderedmap.New[int, *myStruct](28)
```

You can also pass in some initial data to store in the map:
```go
om := orderedmap.New[int, string](orderedmap.WithInitialData[int, string](
orderedmap.Pair[int, string]{
Key: 12,
Value: "foo",
},
orderedmap.Pair[int, string]{
Key: 28,
Value: "bar",
},
))
```

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

```go
Expand Down
4 changes: 4 additions & 0 deletions json.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ func dumpWriter(writer *jwriter.Writer) ([]byte, error) {

// UnmarshalJSON implements the json.Unmarshaler interface.
func (om *OrderedMap[K, V]) UnmarshalJSON(data []byte) error {
if om.list == nil {
om.initialize(0)
}

return jsonparser.ObjectEach(
data,
func(keyData []byte, valueData []byte, dataType jsonparser.ValueType, offset int) error {
Expand Down
90 changes: 90 additions & 0 deletions json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import (
"errors"
"fmt"
"strconv"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// to test marshalling TextMarshalers and unmarshalling TextUnmarshalers
type marshallable int

func (m marshallable) MarshalText() ([]byte, error) {
Expand Down Expand Up @@ -133,6 +135,94 @@ func TestUnmarshallJSON(t *testing.T) {
})
}

// to test structs that have nested map fields
type nestedMaps struct {
X int `json:"x"`
M *OrderedMap[string, []*OrderedMap[int, *OrderedMap[string, any]]] `json:"m"`
}

func TestJSONRoundTrip(t *testing.T) {
for _, testCase := range []struct {
name string
input string
targetFactory func() any
isPrettyPrinted bool
}{
{
name: "",
input: `{
"x": 28,
"m": {
"foo": [
{
"12": {
"i": 12,
"b": true,
"n": null,
"m": {
"a": "b",
"c": 28
}
},
"28": {
"a": false,
"b": [
1,
2,
3
]
}
},
{
"3": {
"c": null,
"d": 87
},
"4": {
"e": true
},
"5": {
"f": 4,
"g": 5,
"h": 6
}
}
],
"bar": [
{
"5": {
"foo": "bar"
}
}
]
}
}`,
targetFactory: func() any { return &nestedMaps{} },
isPrettyPrinted: true,
},
} {
t.Run(testCase.name, func(t *testing.T) {
target := testCase.targetFactory()

require.NoError(t, json.Unmarshal([]byte(testCase.input), target))

var (
out []byte
err error
)
if testCase.isPrettyPrinted {
out, err = json.MarshalIndent(target, "", " ")
} else {
out, err = json.Marshal(target)
}

if assert.NoError(t, err) {
assert.Equal(t, strings.TrimSpace(testCase.input), string(out))
}
})
}
}

func BenchmarkMarshalJSON(b *testing.B) {
om := New[int, any]()
om.Set(1, "bar")
Expand Down
80 changes: 66 additions & 14 deletions orderedmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,68 @@ type OrderedMap[K comparable, V any] struct {
list *list.List[*Pair[K, V]]
}

// New creates a new OrderedMap.
// An optional capacity can be given à la make(map[K]V, capacity).
func New[K comparable, V any](capacity ...int) *OrderedMap[K, V] {
var pairs map[K]*Pair[K, V]
switch len(capacity) {
case 0:
pairs = make(map[K]*Pair[K, V])
case 1:
pairs = make(map[K]*Pair[K, V], capacity[0])
default:
panic("too many arguments to New[K,V]()")
type initConfig[K comparable, V any] struct {
capacity int
initialData []Pair[K, V]
}

type InitOption[K comparable, V any] func(config *initConfig[K, V])

// WithCapacity allows giving a capacity hint for the map, akin to the standard make(map[K]V, capacity).
func WithCapacity[K comparable, V any](capacity int) InitOption[K, V] {
return func(c *initConfig[K, V]) {
c.capacity = capacity
}
return &OrderedMap[K, V]{
pairs: pairs,
list: list.New[*Pair[K, V]](),
}

// WithInitialData allows passing in initial data for the map.
func WithInitialData[K comparable, V any](initialData ...Pair[K, V]) InitOption[K, V] {
return func(c *initConfig[K, V]) {
c.initialData = initialData
if c.capacity < len(initialData) {
c.capacity = len(initialData)
}
}
}

// New creates a new OrderedMap.
// options can either be one or several InitOption[K, V], or a single integer,
// which is then interpreted as a capacity hint, à la make(map[K]V, capacity).
func New[K comparable, V any](options ...any) *OrderedMap[K, V] { //nolint:varnamelen
orderedMap := &OrderedMap[K, V]{}

var config initConfig[K, V]
for _, untypedOption := range options {
switch option := untypedOption.(type) {
case int:
if len(options) != 1 {
invalidOption()
}
config.capacity = option

case InitOption[K, V]:
option(&config)

default:
invalidOption()
}
}

orderedMap.initialize(config.capacity)
orderedMap.AddPairs(config.initialData...)

return orderedMap
}

const invalidOptionMessage = `when using orderedmap.New[K,V]() with options, either provide one or several InitOption[K, V]; or a single integer which is then interpreted as a capacity hint, à la make(map[K]V, capacity).` //nolint:lll

func invalidOption() { panic(invalidOptionMessage) }

func (om *OrderedMap[K, V]) initialize(capacity int) {
om.pairs = make(map[K]*Pair[K, V], capacity)
om.list = list.New[*Pair[K, V]]()
}

// Get looks for the given key, and returns the value associated with it,
// or V's nil value if not found. The boolean it returns says whether the key is present in the map.
func (om *OrderedMap[K, V]) Get(key K) (val V, present bool) {
Expand Down Expand Up @@ -84,6 +128,14 @@ func (om *OrderedMap[K, V]) Set(key K, value V) (val V, present bool) {
return
}

// AddPairs allows setting multiple pairs at a time. It's equivalent to calling
// Set on each pair sequentially.
func (om *OrderedMap[K, V]) AddPairs(pairs ...Pair[K, V]) {
for _, pair := range pairs {
om.Set(pair.Key, pair.Value)
}
}

// Store is an alias for Set, mostly to present an API similar to `sync.Map`'s.
func (om *OrderedMap[K, V]) Store(key K, value V) (V, bool) {
return om.Set(key, value)
Expand Down
62 changes: 61 additions & 1 deletion orderedmap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,15 +261,75 @@ func TestMove(t *testing.T) {
assert.NotEqual(t, err, nil)
}

func TestAddPairs(t *testing.T) {
om := New[int, any]()
om.AddPairs(
Pair[int, any]{
Key: 28,
Value: "foo",
},
Pair[int, any]{
Key: 12,
Value: "bar",
},
Pair[int, any]{
Key: 28,
Value: "baz",
},
)

assertOrderedPairsEqual(t, om,
[]int{28, 12},
[]any{"baz", "bar"})
}

// sadly, we can't test the "actual" capacity here, see https://github.com/golang/go/issues/52157
func TestNewWithCapacity(t *testing.T) {
zero := New[int, string](0)
assert.Empty(t, zero.Len())

assert.PanicsWithValue(t, "too many arguments to New[K,V]()", func() {
assert.PanicsWithValue(t, invalidOptionMessage, func() {
_ = New[int, string](1, 2)
})
assert.PanicsWithValue(t, invalidOptionMessage, func() {
_ = New[int, string](1, 2, 3)
})

om := New[int, string](-1)
om.Set(1337, "quarante-deux")
assert.Equal(t, 1, om.Len())
}

func TestWithOptions(t *testing.T) {
t.Run("wih capacity", func(t *testing.T) {
om := New[string, any](WithCapacity[string, any](98))
assert.Equal(t, 0, om.Len())
})

t.Run("with initial data", func(t *testing.T) {
om := New[string, int](WithInitialData(
Pair[string, int]{
Key: "a",
Value: 1,
},
Pair[string, int]{
Key: "b",
Value: 2,
},
Pair[string, int]{
Key: "c",
Value: 3,
},
))

assertOrderedPairsEqual(t, om,
[]string{"a", "b", "c"},
[]int{1, 2, 3})
})

t.Run("with an invalid option type", func(t *testing.T) {
assert.PanicsWithValue(t, invalidOptionMessage, func() {
_ = New[int, string]("foo")
})
})
}
6 changes: 3 additions & 3 deletions test_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ func assertOrderedPairsEqualFromOldest[K comparable, V any](
t.Helper()

if assert.Equal(t, len(expectedKeys), len(expectedValues)) && assert.Equal(t, len(expectedKeys), orderedMap.Len()) {
i := orderedMap.Len() - 1
for pair := orderedMap.Newest(); pair != nil; pair = pair.Prev() {
i := 0
for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() {
assert.Equal(t, expectedKeys[i], pair.Key)
assert.Equal(t, expectedValues[i], pair.Value)
i--
i++
}
}
}
Expand Down

0 comments on commit 10e9fad

Please sign in to comment.