├── .circleci
├── circle_build.sh
└── config.yml
├── .gitignore
├── .golangci.yml
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.md
├── example_test.go
├── go.mod
├── go.sum
├── json.go
├── json_fuzz_test.go
├── json_test.go
├── orderedmap.go
├── orderedmap_test.go
├── testdata
└── fuzz
│ └── FuzzRoundTripJSON
│ ├── 62c005f96216d8ba8f62ac0799dfc1a6893e68418238a831ee79cd9c39b4cfc6
│ └── 8093511184ad3e258aa13b957e75ff26c7fae64672dae0c0bc0a9fa5b61a05e7
├── utils_test.go
├── yaml.go
├── yaml_fuzz_test.go
└── yaml_test.go
/.circleci/circle_build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -ex
4 |
5 | # might as well run a little longer
6 | export FUZZ_TIME=20s
7 |
8 | make
9 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 |
3 | jobs:
4 | test:
5 | parameters:
6 | golang-version:
7 | type: string
8 | docker:
9 | - image: cimg/go:<< parameters.golang-version >>
10 | steps:
11 | - checkout
12 | - run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.60.1
13 | - run: .circleci/circle_build.sh
14 |
15 | workflows:
16 | test-workflow:
17 | jobs:
18 | - test:
19 | matrix:
20 | parameters:
21 | golang-version:
22 | - "1.23"
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | run:
2 | tests: false
3 |
4 | linters:
5 | disable-all: true
6 | enable:
7 | - asciicheck
8 | - bidichk
9 | - bodyclose
10 | - containedctx
11 | - contextcheck
12 | - decorder
13 | # Disabling depguard as there is no guarded list of imports
14 | # - depguard
15 | - dogsled
16 | - dupl
17 | - durationcheck
18 | - errcheck
19 | - errchkjson
20 | - errname
21 | - errorlint
22 | - exportloopref
23 | - forbidigo
24 | - funlen
25 | # Don't need gci and goimports
26 | # - gci
27 | - gochecknoglobals
28 | - gochecknoinits
29 | - gocognit
30 | - goconst
31 | - gocritic
32 | - gocyclo
33 | - godox
34 | - gofmt
35 | - gofumpt
36 | - goheader
37 | - goimports
38 | - mnd
39 | - gomoddirectives
40 | - gomodguard
41 | - goprintffuncname
42 | - gosec
43 | - gosimple
44 | - govet
45 | - grouper
46 | - importas
47 | - ineffassign
48 | - lll
49 | - maintidx
50 | - makezero
51 | - misspell
52 | - nakedret
53 | - nilerr
54 | - nilnil
55 | - noctx
56 | - nolintlint
57 | - paralleltest
58 | - prealloc
59 | - predeclared
60 | - promlinter
61 | - revive
62 | - rowserrcheck
63 | - sqlclosecheck
64 | - staticcheck
65 | - stylecheck
66 | - tagliatelle
67 | - tenv
68 | - testpackage
69 | - thelper
70 | - tparallel
71 | # FIXME: doesn't support 1.23 yet
72 | # - typecheck
73 | - unconvert
74 | - unparam
75 | - unused
76 | - varnamelen
77 | - wastedassign
78 | - whitespace
79 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | [comment]: # (Changes since last release go here)
4 |
5 | ## 2.1.8 - Jun 27th 2023
6 |
7 | * Added support for YAML serialization/deserialization
8 |
9 | ## 2.1.7 - Apr 13th 2023
10 |
11 | * Renamed test_utils.go to utils_test.go
12 |
13 | ## 2.1.6 - Feb 15th 2023
14 |
15 | * Added `GetAndMoveToBack()` and `GetAndMoveToFront()` methods
16 |
17 | ## 2.1.5 - Dec 13th 2022
18 |
19 | * Added `Value()` method
20 |
21 | ## 2.1.4 - Dec 12th 2022
22 |
23 | * Fixed a bug with UTF-8 special characters in JSON keys
24 |
25 | ## 2.1.3 - Dec 11th 2022
26 |
27 | * Added support for JSON marshalling/unmarshalling of wrapper of primitive types
28 |
29 | ## 2.1.2 - Dec 10th 2022
30 | * Allowing to pass options to `New`, to give a capacity hint, or initial data
31 | * Allowing to deserialize nested ordered maps from JSON without having to explicitly instantiate them
32 | * Added the `AddPairs` method
33 |
34 | ## 2.1.1 - Dec 9th 2022
35 | * Fixing a bug with JSON marshalling
36 |
37 | ## 2.1.0 - Dec 7th 2022
38 | * Added support for JSON serialization/deserialization
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .DEFAULT_GOAL := all
2 |
3 | .PHONY: all
4 | all: test_with_fuzz lint
5 |
6 | # the TEST_FLAGS env var can be set to eg run only specific tests
7 | TEST_COMMAND = go test -v -count=1 -race -cover $(TEST_FLAGS)
8 |
9 | .PHONY: test
10 | test:
11 | $(TEST_COMMAND)
12 |
13 | .PHONY: bench
14 | bench:
15 | go test -bench=.
16 |
17 | FUZZ_TIME ?= 10s
18 |
19 | # see https://github.com/golang/go/issues/46312
20 | # and https://stackoverflow.com/a/72673487/4867444
21 | # if we end up having more fuzz tests
22 | .PHONY: test_with_fuzz
23 | test_with_fuzz:
24 | $(TEST_COMMAND) -fuzz=FuzzRoundTripJSON -fuzztime=$(FUZZ_TIME)
25 | $(TEST_COMMAND) -fuzz=FuzzRoundTripYAML -fuzztime=$(FUZZ_TIME)
26 |
27 | .PHONY: fuzz
28 | fuzz: test_with_fuzz
29 |
30 | .PHONY: lint
31 | lint:
32 | golangci-lint run
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://pkg.go.dev/github.com/wk8/go-ordered-map/v2)
2 | [](https://app.circleci.com/pipelines/github/wk8/go-ordered-map)
3 |
4 | # Golang Ordered Maps
5 |
6 | Same as regular maps, but also remembers the order in which keys were inserted, akin to [Python's `collections.OrderedDict`s](https://docs.python.org/3.7/library/collections.html#ordereddict-objects).
7 |
8 | It offers the following features:
9 | * optimal runtime performance (all operations are constant time)
10 | * optimal memory usage (only one copy of values, no unnecessary memory allocation)
11 | * allows iterating from newest or oldest keys indifferently, without memory copy, allowing to `break` the iteration, and in time linear to the number of keys iterated over rather than the total length of the ordered map
12 | * supports any generic types for both keys and values. If you're running go < 1.18, you can use [version 1](https://github.com/wk8/go-ordered-map/tree/v1) that takes and returns generic `interface{}`s instead of using generics
13 | * idiomatic API, akin to that of [`container/list`](https://golang.org/pkg/container/list)
14 | * support for JSON and YAML marshalling
15 |
16 | ## Documentation
17 |
18 | [The full documentation is available on pkg.go.dev](https://pkg.go.dev/github.com/wk8/go-ordered-map/v2).
19 |
20 | ## Installation
21 | ```bash
22 | go get -u github.com/wk8/go-ordered-map/v2
23 | ```
24 |
25 | Or use your favorite golang vendoring tool!
26 |
27 | ## Supported go versions
28 |
29 | Go >= 1.23 is required to use version >= 2.2.0 of this library, as it uses generics and iterators.
30 |
31 | if you're running go < 1.23, you can use [version 2.1.8](https://github.com/wk8/go-ordered-map/tree/v2.1.8) instead.
32 |
33 | If you're running go < 1.18, you can use [version 1](https://github.com/wk8/go-ordered-map/tree/v1) instead.
34 |
35 | ## Example / usage
36 |
37 | ```go
38 | package main
39 |
40 | import (
41 | "fmt"
42 | "strings"
43 |
44 | "github.com/wk8/go-ordered-map/v2"
45 | )
46 |
47 | func main() {
48 | om := orderedmap.New[string, string]()
49 |
50 | om.Set("foo", "bar")
51 | om.Set("bar", "baz")
52 | om.Set("coucou", "toi")
53 |
54 | fmt.Println(om.Get("foo")) // => "bar", true
55 | fmt.Println(om.Get("i dont exist")) // => "", false
56 |
57 | // iterating pairs from oldest to newest:
58 | for pair := om.Oldest(); pair != nil; pair = pair.Next() {
59 | fmt.Printf("%s => %s\n", pair.Key, pair.Value)
60 | } // prints:
61 | // foo => bar
62 | // bar => baz
63 | // coucou => toi
64 |
65 | // iterating over the 2 newest pairs:
66 | i := 0
67 | for pair := om.Newest(); pair != nil; pair = pair.Prev() {
68 | fmt.Printf("%s => %s\n", pair.Key, pair.Value)
69 | i++
70 | if i >= 2 {
71 | break
72 | }
73 | } // prints:
74 | // coucou => toi
75 | // bar => baz
76 |
77 | // removing all pairs which do not have an "o" in their key
78 | om.Filter(func(key, value string) bool { return strings.Contains(key, "o") })
79 |
80 | // new iteration syntax
81 | for key, value := range om.FromOldest() {
82 | fmt.Printf("%s => %s\n", key, value)
83 | }// prints:
84 | // foo => bar
85 | // coucou => toi
86 | }
87 | ```
88 |
89 | An `OrderedMap`'s keys must implement `comparable`, and its values can be anything, for example:
90 |
91 | ```go
92 | type myStruct struct {
93 | payload string
94 | }
95 |
96 | func main() {
97 | om := orderedmap.New[int, *myStruct]()
98 |
99 | om.Set(12, &myStruct{"foo"})
100 | om.Set(1, &myStruct{"bar"})
101 |
102 | value, present := om.Get(12)
103 | if !present {
104 | panic("should be there!")
105 | }
106 | fmt.Println(value.payload) // => foo
107 |
108 | for pair := om.Oldest(); pair != nil; pair = pair.Next() {
109 | fmt.Printf("%d => %s\n", pair.Key, pair.Value.payload)
110 | } // prints:
111 | // 12 => foo
112 | // 1 => bar
113 | }
114 | ```
115 |
116 | 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`):
117 | ```go
118 | om := orderedmap.New[int, *myStruct](28)
119 | ```
120 |
121 | You can also pass in some initial data to store in the map:
122 | ```go
123 | om := orderedmap.New[int, string](orderedmap.WithInitialData[int, string](
124 | orderedmap.Pair[int, string]{
125 | Key: 12,
126 | Value: "foo",
127 | },
128 | orderedmap.Pair[int, string]{
129 | Key: 28,
130 | Value: "bar",
131 | },
132 | ))
133 | ```
134 |
135 | `OrderedMap`s also support JSON serialization/deserialization, and preserves order:
136 |
137 | ```go
138 | // serialization
139 | data, err := json.Marshal(om)
140 | ...
141 |
142 | // deserialization
143 | om := orderedmap.New[string, string]() // or orderedmap.New[int, any](), or any type you expect
144 | err := json.Unmarshal(data, &om)
145 | ...
146 | ```
147 |
148 | Similarly, it also supports YAML serialization/deserialization using the yaml.v3 package, which also preserves order:
149 |
150 | ```go
151 | // serialization
152 | data, err := yaml.Marshal(om)
153 | ...
154 |
155 | // deserialization
156 | om := orderedmap.New[string, string]() // or orderedmap.New[int, any](), or any type you expect
157 | err := yaml.Unmarshal(data, &om)
158 | ...
159 | ```
160 |
161 | ## Iterator support (go >= 1.23)
162 |
163 | The `FromOldest`, `FromNewest`, `KeysFromOldest`, `KeysFromNewest`, `ValuesFromOldest` and `ValuesFromNewest` methods return iterators over the map's pairs, starting from the oldest or newest pair, respectively.
164 |
165 | For example:
166 |
167 | ```go
168 | om := orderedmap.New[int, string]()
169 | om.Set(1, "foo")
170 | om.Set(2, "bar")
171 | om.Set(3, "baz")
172 |
173 | for k, v := range om.FromOldest() {
174 | fmt.Printf("%d => %s\n", k, v)
175 | }
176 |
177 | // prints:
178 | // 1 => foo
179 | // 2 => bar
180 | // 3 => baz
181 |
182 | for k := range om.KeysNewest() {
183 | fmt.Printf("%d\n", k)
184 | }
185 |
186 | // prints:
187 | // 3
188 | // 2
189 | // 1
190 | ```
191 |
192 | `From` is a convenience function that creates a new `OrderedMap` from an iterator over key-value pairs.
193 |
194 | ```go
195 | om := orderedmap.New[int, string]()
196 | om.Set(1, "foo")
197 | om.Set(2, "bar")
198 | om.Set(3, "baz")
199 |
200 | om2 := orderedmap.From(om.FromOldest())
201 |
202 | for k, v := range om2.FromOldest() {
203 | fmt.Printf("%d => %s\n", k, v)
204 | }
205 |
206 | // prints:
207 | // 1 => foo
208 | // 2 => bar
209 | // 3 => baz
210 | ```
211 |
212 | ## Alternatives
213 |
214 | 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:
215 | * [iancoleman/orderedmap](https://github.com/iancoleman/orderedmap) only accepts `string` keys, its `Delete` operations are linear
216 | * [cevaris/ordered_map](https://github.com/cevaris/ordered_map) uses a channel for iterations, and leaks goroutines if the iteration is interrupted before fully traversing the map
217 | * [mantyr/iterator](https://github.com/mantyr/iterator) also uses a channel for iterations, and its `Delete` operations are linear
218 | * [samdolan/go-ordered-map](https://github.com/samdolan/go-ordered-map) adds unnecessary locking (users should add their own locking instead if they need it), its `Delete` and `Get` operations are linear, iterations trigger a linear memory allocation
219 |
--------------------------------------------------------------------------------
/example_test.go:
--------------------------------------------------------------------------------
1 | package orderedmap_test
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 |
7 | "github.com/wk8/go-ordered-map/v2"
8 | )
9 |
10 | func Example() {
11 | om := orderedmap.New[string, string](3)
12 |
13 | om.Set("foo", "bar")
14 | om.Set("bar", "baz")
15 | om.Set("coucou", "toi")
16 |
17 | fmt.Println("## Get operations: ##")
18 | fmt.Println(om.Get("foo"))
19 | fmt.Println(om.Get("i dont exist"))
20 | fmt.Println(om.Value("coucou"))
21 |
22 | fmt.Println("## Iterating over pairs from oldest to newest: ##")
23 | for pair := om.Oldest(); pair != nil; pair = pair.Next() {
24 | fmt.Printf("%s => %s\n", pair.Key, pair.Value)
25 | }
26 |
27 | fmt.Println("## Iterating over the 2 newest pairs: ##")
28 | i := 0
29 | for pair := om.Newest(); pair != nil; pair = pair.Prev() {
30 | fmt.Printf("%s => %s\n", pair.Key, pair.Value)
31 | i++
32 | if i >= 2 {
33 | break
34 | }
35 | }
36 |
37 | fmt.Println("## JSON serialization: ##")
38 | data, err := json.Marshal(om)
39 | if err != nil {
40 | panic(err)
41 | }
42 | fmt.Println(string(data))
43 |
44 | fmt.Println("## JSON deserialization: ##")
45 | om2 := orderedmap.New[string, string]()
46 | if err := json.Unmarshal(data, &om2); err != nil {
47 | panic(err)
48 | }
49 | fmt.Println(om2.Oldest().Key)
50 |
51 | // Output:
52 | // ## Get operations: ##
53 | // bar true
54 | // false
55 | // toi
56 | // ## Iterating over pairs from oldest to newest: ##
57 | // foo => bar
58 | // bar => baz
59 | // coucou => toi
60 | // ## Iterating over the 2 newest pairs: ##
61 | // coucou => toi
62 | // bar => baz
63 | // ## JSON serialization: ##
64 | // {"foo":"bar","bar":"baz","coucou":"toi"}
65 | // ## JSON deserialization: ##
66 | // foo
67 | }
68 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/wk8/go-ordered-map/v2
2 |
3 | go 1.23
4 |
5 | require (
6 | github.com/bahlo/generic-list-go v0.2.0
7 | github.com/buger/jsonparser v1.1.1
8 | github.com/mailru/easyjson v0.7.7
9 | github.com/stretchr/testify v1.8.1
10 | gopkg.in/yaml.v3 v3.0.1
11 | )
12 |
13 | require (
14 | github.com/davecgh/go-spew v1.1.1 // indirect
15 | github.com/pmezard/go-difflib v1.0.0 // indirect
16 | )
17 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
2 | github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
3 | github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
4 | github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
9 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
10 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
13 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
14 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
15 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
16 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
17 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
18 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
19 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
20 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
21 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
22 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
23 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
24 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
25 |
--------------------------------------------------------------------------------
/json.go:
--------------------------------------------------------------------------------
1 | package orderedmap
2 |
3 | import (
4 | "bytes"
5 | "encoding"
6 | "encoding/json"
7 | "fmt"
8 | "reflect"
9 | "unicode/utf8"
10 |
11 | "github.com/buger/jsonparser"
12 | "github.com/mailru/easyjson/jwriter"
13 | )
14 |
15 | var (
16 | _ json.Marshaler = &OrderedMap[int, any]{}
17 | _ json.Unmarshaler = &OrderedMap[int, any]{}
18 | )
19 |
20 | // MarshalJSON implements the json.Marshaler interface.
21 | func (om *OrderedMap[K, V]) MarshalJSON() ([]byte, error) { //nolint:funlen
22 | if om == nil || om.list == nil {
23 | return []byte("null"), nil
24 | }
25 |
26 | writer := jwriter.Writer{
27 | NoEscapeHTML: om.disableHTMLEscape,
28 | }
29 | writer.RawByte('{')
30 |
31 | for pair, firstIteration := om.Oldest(), true; pair != nil; pair = pair.Next() {
32 | if firstIteration {
33 | firstIteration = false
34 | } else {
35 | writer.RawByte(',')
36 | }
37 |
38 | switch key := any(pair.Key).(type) {
39 | case string:
40 | writer.String(key)
41 | case encoding.TextMarshaler:
42 | writer.RawByte('"')
43 | writer.Raw(key.MarshalText())
44 | writer.RawByte('"')
45 | case int:
46 | writer.IntStr(key)
47 | case int8:
48 | writer.Int8Str(key)
49 | case int16:
50 | writer.Int16Str(key)
51 | case int32:
52 | writer.Int32Str(key)
53 | case int64:
54 | writer.Int64Str(key)
55 | case uint:
56 | writer.UintStr(key)
57 | case uint8:
58 | writer.Uint8Str(key)
59 | case uint16:
60 | writer.Uint16Str(key)
61 | case uint32:
62 | writer.Uint32Str(key)
63 | case uint64:
64 | writer.Uint64Str(key)
65 | default:
66 |
67 | // this switch takes care of wrapper types around primitive types, such as
68 | // type myType string
69 | switch keyValue := reflect.ValueOf(key); keyValue.Type().Kind() {
70 | case reflect.String:
71 | writer.String(keyValue.String())
72 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
73 | writer.Int64Str(keyValue.Int())
74 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
75 | writer.Uint64Str(keyValue.Uint())
76 | default:
77 | return nil, fmt.Errorf("unsupported key type: %T", key)
78 | }
79 | }
80 |
81 | writer.RawByte(':')
82 | // the error is checked at the end of the function
83 | writer.Raw(jsonMarshal(pair.Value, om.disableHTMLEscape))
84 | }
85 |
86 | writer.RawByte('}')
87 |
88 | return dumpWriter(&writer)
89 | }
90 |
91 | func jsonMarshal(t interface{}, disableHTMLEscape bool) ([]byte, error) {
92 | if disableHTMLEscape {
93 | buffer := &bytes.Buffer{}
94 | encoder := json.NewEncoder(buffer)
95 | encoder.SetEscapeHTML(false)
96 | err := encoder.Encode(t)
97 | // Encode() adds an extra newline, strip it off to guarantee same behavior as json.Marshal
98 | return bytes.TrimRight(buffer.Bytes(), "\n"), err
99 | }
100 | return json.Marshal(t)
101 | }
102 |
103 | func dumpWriter(writer *jwriter.Writer) ([]byte, error) {
104 | if writer.Error != nil {
105 | return nil, writer.Error
106 | }
107 |
108 | var buf bytes.Buffer
109 | buf.Grow(writer.Size())
110 | if _, err := writer.DumpTo(&buf); err != nil {
111 | return nil, err
112 | }
113 |
114 | return buf.Bytes(), nil
115 | }
116 |
117 | // UnmarshalJSON implements the json.Unmarshaler interface.
118 | func (om *OrderedMap[K, V]) UnmarshalJSON(data []byte) error {
119 | if om.list == nil {
120 | om.initialize(0, om.disableHTMLEscape)
121 | }
122 |
123 | return jsonparser.ObjectEach(
124 | data,
125 | func(keyData []byte, valueData []byte, dataType jsonparser.ValueType, offset int) error {
126 | if dataType == jsonparser.String {
127 | // jsonparser removes the enclosing quotes; we need to restore them to make a valid JSON
128 | valueData = data[offset-len(valueData)-2 : offset]
129 | }
130 |
131 | var key K
132 | var value V
133 |
134 | switch typedKey := any(&key).(type) {
135 | case *string:
136 | s, err := decodeUTF8(keyData)
137 | if err != nil {
138 | return err
139 | }
140 | *typedKey = s
141 | case encoding.TextUnmarshaler:
142 | if err := typedKey.UnmarshalText(keyData); err != nil {
143 | return err
144 | }
145 | case *int, *int8, *int16, *int32, *int64, *uint, *uint8, *uint16, *uint32, *uint64:
146 | if err := json.Unmarshal(keyData, typedKey); err != nil {
147 | return err
148 | }
149 | default:
150 | // this switch takes care of wrapper types around primitive types, such as
151 | // type myType string
152 | switch reflect.TypeOf(key).Kind() {
153 | case reflect.String:
154 | s, err := decodeUTF8(keyData)
155 | if err != nil {
156 | return err
157 | }
158 |
159 | convertedKeyData := reflect.ValueOf(s).Convert(reflect.TypeOf(key))
160 | reflect.ValueOf(&key).Elem().Set(convertedKeyData)
161 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
162 | reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
163 | if err := json.Unmarshal(keyData, &key); err != nil {
164 | return err
165 | }
166 | default:
167 | return fmt.Errorf("unsupported key type: %T", key)
168 | }
169 | }
170 |
171 | if err := json.Unmarshal(valueData, &value); err != nil {
172 | return err
173 | }
174 |
175 | om.Set(key, value)
176 | return nil
177 | })
178 | }
179 |
180 | func decodeUTF8(input []byte) (string, error) {
181 | remaining, offset := input, 0
182 | runes := make([]rune, 0, len(remaining))
183 |
184 | for len(remaining) > 0 {
185 | r, size := utf8.DecodeRune(remaining)
186 | if r == utf8.RuneError && size <= 1 {
187 | return "", fmt.Errorf("not a valid UTF-8 string (at position %d): %s", offset, string(input))
188 | }
189 |
190 | runes = append(runes, r)
191 | remaining = remaining[size:]
192 | offset += size
193 | }
194 |
195 | return string(runes), nil
196 | }
197 |
--------------------------------------------------------------------------------
/json_fuzz_test.go:
--------------------------------------------------------------------------------
1 | package orderedmap
2 |
3 | // Adapted from https://github.com/dvyukov/go-fuzz-corpus/blob/c42c1b2/json/json.go
4 |
5 | import (
6 | "encoding/json"
7 | "testing"
8 |
9 | "github.com/stretchr/testify/assert"
10 | "github.com/stretchr/testify/require"
11 | )
12 |
13 | func FuzzRoundTripJSON(f *testing.F) {
14 | f.Fuzz(func(t *testing.T, data []byte) {
15 | for _, testCase := range []struct {
16 | name string
17 | constructor func() any
18 | // should be a function that asserts that 2 objects of the type returned by constructor are equal
19 | equalityAssertion func(*testing.T, any, any) bool
20 | }{
21 | {
22 | name: "with a string -> string map",
23 | constructor: func() any { return &OrderedMap[string, string]{} },
24 | equalityAssertion: assertOrderedMapsEqual[string, string],
25 | },
26 | {
27 | name: "with a string -> int map",
28 | constructor: func() any { return &OrderedMap[string, int]{} },
29 | equalityAssertion: assertOrderedMapsEqual[string, int],
30 | },
31 | {
32 | name: "with a string -> any map",
33 | constructor: func() any { return &OrderedMap[string, any]{} },
34 | equalityAssertion: assertOrderedMapsEqual[string, any],
35 | },
36 | {
37 | name: "with a struct with map fields",
38 | constructor: func() any { return new(testFuzzStruct) },
39 | equalityAssertion: assertTestFuzzStructEqual,
40 | },
41 | } {
42 | t.Run(testCase.name, func(t *testing.T) {
43 | v1 := testCase.constructor()
44 | if json.Unmarshal(data, v1) != nil {
45 | return
46 | }
47 |
48 | jsonData, err := json.Marshal(v1)
49 | require.NoError(t, err)
50 |
51 | v2 := testCase.constructor()
52 | require.NoError(t, json.Unmarshal(jsonData, v2))
53 |
54 | if !assert.True(t, testCase.equalityAssertion(t, v1, v2), "failed with input data %q", string(data)) {
55 | // look at that what the standard lib does with regular map, to help with debugging
56 |
57 | var m1 map[string]any
58 | require.NoError(t, json.Unmarshal(data, &m1))
59 |
60 | mapJsonData, err := json.Marshal(m1)
61 | require.NoError(t, err)
62 |
63 | var m2 map[string]any
64 | require.NoError(t, json.Unmarshal(mapJsonData, &m2))
65 |
66 | t.Logf("initial data = %s", string(data))
67 | t.Logf("unmarshalled map = %v", m1)
68 | t.Logf("re-marshalled from map = %s", string(mapJsonData))
69 | t.Logf("re-marshalled from test obj = %s", string(jsonData))
70 | t.Logf("re-unmarshalled map = %s", m2)
71 | }
72 | })
73 | }
74 | })
75 | }
76 |
77 | // only works for fairly basic maps, that's why it's just in this file
78 | func assertOrderedMapsEqual[K comparable, V any](t *testing.T, v1, v2 any) bool {
79 | om1, ok1 := v1.(*OrderedMap[K, V])
80 | om2, ok2 := v2.(*OrderedMap[K, V])
81 |
82 | if !assert.True(t, ok1, "v1 not an orderedmap") ||
83 | !assert.True(t, ok2, "v2 not an orderedmap") {
84 | return false
85 | }
86 |
87 | success := assert.Equal(t, om1.Len(), om2.Len(), "om1 and om2 have different lengths: %d vs %d", om1.Len(), om2.Len())
88 |
89 | for i, pair1, pair2 := 0, om1.Oldest(), om2.Oldest(); pair1 != nil && pair2 != nil; i, pair1, pair2 = i+1, pair1.Next(), pair2.Next() {
90 | success = assert.Equal(t, pair1.Key, pair2.Key, "different keys at position %d: %v vs %v", i, pair1.Key, pair2.Key) && success
91 | success = assert.Equal(t, pair1.Value, pair2.Value, "different values at position %d: %v vs %v", i, pair1.Value, pair2.Value) && success
92 | }
93 |
94 | return success
95 | }
96 |
97 | type testFuzzStruct struct {
98 | M1 *OrderedMap[int, any]
99 | M2 *OrderedMap[int, string]
100 | M3 *OrderedMap[string, string]
101 | }
102 |
103 | func assertTestFuzzStructEqual(t *testing.T, v1, v2 any) bool {
104 | s1, ok1 := v1.(*testFuzzStruct)
105 | s2, ok2 := v2.(*testFuzzStruct)
106 |
107 | if !assert.True(t, ok1, "v1 not an testFuzzStruct") ||
108 | !assert.True(t, ok2, "v2 not an testFuzzStruct") {
109 | return false
110 | }
111 |
112 | success := assertOrderedMapsEqual[int, any](t, s1.M1, s2.M1)
113 | success = assertOrderedMapsEqual[int, string](t, s1.M2, s2.M2) && success
114 | success = assertOrderedMapsEqual[string, string](t, s1.M3, s2.M3) && success
115 |
116 | return success
117 | }
118 |
--------------------------------------------------------------------------------
/json_test.go:
--------------------------------------------------------------------------------
1 | package orderedmap
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "strconv"
8 | "strings"
9 | "testing"
10 |
11 | "github.com/stretchr/testify/assert"
12 | "github.com/stretchr/testify/require"
13 | )
14 |
15 | // to test marshalling TextMarshalers and unmarshalling TextUnmarshalers
16 | type marshallable int
17 |
18 | func (m marshallable) MarshalText() ([]byte, error) {
19 | return []byte(fmt.Sprintf("#%d#", m)), nil
20 | }
21 |
22 | func (m *marshallable) UnmarshalText(text []byte) error {
23 | if len(text) < 3 {
24 | return errors.New("too short")
25 | }
26 | if text[0] != '#' || text[len(text)-1] != '#' {
27 | return errors.New("missing prefix or suffix")
28 | }
29 |
30 | value, err := strconv.Atoi(string(text[1 : len(text)-1]))
31 | if err != nil {
32 | return err
33 | }
34 |
35 | *m = marshallable(value)
36 | return nil
37 | }
38 |
39 | func TestMarshalJSON(t *testing.T) {
40 | t.Run("int key", func(t *testing.T) {
41 | om := New[int, any]()
42 | om.Set(1, "bar")
43 | om.Set(7, "baz")
44 | om.Set(2, 28)
45 | om.Set(3, 100)
46 | om.Set(4, "baz")
47 | om.Set(5, "28")
48 | om.Set(6, "100")
49 | om.Set(8, "baz")
50 | om.Set(8, "baz")
51 | om.Set(9, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque auctor augue accumsan mi maximus, quis viverra massa pretium. Phasellus imperdiet sapien a interdum sollicitudin. Duis at commodo lectus, a lacinia sem.")
52 |
53 | b, err := json.Marshal(om)
54 | assert.NoError(t, err)
55 | assert.Equal(t, `{"1":"bar","7":"baz","2":28,"3":100,"4":"baz","5":"28","6":"100","8":"baz","9":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque auctor augue accumsan mi maximus, quis viverra massa pretium. Phasellus imperdiet sapien a interdum sollicitudin. Duis at commodo lectus, a lacinia sem."}`, string(b))
56 | })
57 |
58 | t.Run("string key", func(t *testing.T) {
59 | om := New[string, any]()
60 | om.Set("test", "bar")
61 | om.Set("abc", true)
62 |
63 | b, err := json.Marshal(om)
64 | assert.NoError(t, err)
65 | assert.Equal(t, `{"test":"bar","abc":true}`, string(b))
66 | })
67 |
68 | t.Run("typed string key", func(t *testing.T) {
69 | type myString string
70 | om := New[myString, any]()
71 | om.Set("test", "bar")
72 | om.Set("abc", true)
73 |
74 | b, err := json.Marshal(om)
75 | assert.NoError(t, err)
76 | assert.Equal(t, `{"test":"bar","abc":true}`, string(b))
77 | })
78 |
79 | t.Run("typed int key", func(t *testing.T) {
80 | type myInt uint32
81 | om := New[myInt, any]()
82 | om.Set(1, "bar")
83 | om.Set(7, "baz")
84 | om.Set(2, 28)
85 | om.Set(3, 100)
86 | om.Set(4, "baz")
87 |
88 | b, err := json.Marshal(om)
89 | assert.NoError(t, err)
90 | assert.Equal(t, `{"1":"bar","7":"baz","2":28,"3":100,"4":"baz"}`, string(b))
91 | })
92 |
93 | t.Run("TextMarshaller key", func(t *testing.T) {
94 | om := New[marshallable, any]()
95 | om.Set(marshallable(1), "bar")
96 | om.Set(marshallable(28), true)
97 |
98 | b, err := json.Marshal(om)
99 | assert.NoError(t, err)
100 | assert.Equal(t, `{"#1#":"bar","#28#":true}`, string(b))
101 | })
102 |
103 | t.Run("empty map", func(t *testing.T) {
104 | om := New[string, any]()
105 |
106 | b, err := json.Marshal(om)
107 | assert.NoError(t, err)
108 | assert.Equal(t, `{}`, string(b))
109 | })
110 |
111 | t.Run("HTML escaping enabled (default)", func(t *testing.T) {
112 | om := New[marshallable, any]()
113 | om.Set(marshallable(1), "hello this is bold")
114 | om.Set(marshallable(28), "some book")
115 |
116 | b, err := jsonMarshal(om, false)
117 | assert.NoError(t, err)
118 | assert.Equal(t, `{"#1#":"hello \u003cstrong\u003ethis is bold\u003c/strong\u003e","#28#":"\u003c?xml version=\"1.0\"?\u003e\u003ccatalog\u003e\u003cbook\u003esome book\u003c/book\u003e\u003c/catalog\u003e"}`, string(b))
119 | })
120 |
121 | t.Run("HTML escaping disabled", func(t *testing.T) {
122 | om := New[marshallable, any](WithDisableHTMLEscape[marshallable, any]())
123 | om.Set(marshallable(1), "hello this is bold")
124 | om.Set(marshallable(28), "some book")
125 |
126 | b, err := jsonMarshal(om, true /* we need to disable HTML escaping here also */)
127 | assert.NoError(t, err)
128 | assert.Equal(t, `{"#1#":"hello this is bold","#28#":"some book"}`, string(b))
129 | })
130 | }
131 |
132 | func TestUnmarshallJSON(t *testing.T) {
133 | t.Run("int key", func(t *testing.T) {
134 | data := `{"1":"bar","7":"baz","2":28,"3":100,"4":"baz","5":"28","6":"100","8":"baz"}`
135 |
136 | om := New[int, any]()
137 | require.NoError(t, json.Unmarshal([]byte(data), &om))
138 |
139 | assertOrderedPairsEqual(t, om,
140 | []int{1, 7, 2, 3, 4, 5, 6, 8},
141 | []any{"bar", "baz", float64(28), float64(100), "baz", "28", "100", "baz"})
142 | })
143 |
144 | t.Run("string key", func(t *testing.T) {
145 | data := `{"test":"bar","abc":true}`
146 |
147 | om := New[string, any]()
148 | require.NoError(t, json.Unmarshal([]byte(data), &om))
149 |
150 | assertOrderedPairsEqual(t, om,
151 | []string{"test", "abc"},
152 | []any{"bar", true})
153 | })
154 |
155 | t.Run("typed string key", func(t *testing.T) {
156 | data := `{"test":"bar","abc":true}`
157 |
158 | type myString string
159 | om := New[myString, any]()
160 | require.NoError(t, json.Unmarshal([]byte(data), &om))
161 |
162 | assertOrderedPairsEqual(t, om,
163 | []myString{"test", "abc"},
164 | []any{"bar", true})
165 | })
166 |
167 | t.Run("typed int key", func(t *testing.T) {
168 | data := `{"1":"bar","7":"baz","2":28,"3":100,"4":"baz","5":"28","6":"100","8":"baz"}`
169 |
170 | type myInt uint32
171 | om := New[myInt, any]()
172 | require.NoError(t, json.Unmarshal([]byte(data), &om))
173 |
174 | assertOrderedPairsEqual(t, om,
175 | []myInt{1, 7, 2, 3, 4, 5, 6, 8},
176 | []any{"bar", "baz", float64(28), float64(100), "baz", "28", "100", "baz"})
177 | })
178 |
179 | t.Run("TextUnmarshaler key", func(t *testing.T) {
180 | data := `{"#1#":"bar","#28#":true}`
181 |
182 | om := New[marshallable, any]()
183 | require.NoError(t, json.Unmarshal([]byte(data), &om))
184 |
185 | assertOrderedPairsEqual(t, om,
186 | []marshallable{1, 28},
187 | []any{"bar", true})
188 | })
189 |
190 | t.Run("when fed with an input that's not an object", func(t *testing.T) {
191 | for _, data := range []string{"true", `["foo"]`, "42", `"foo"`} {
192 | om := New[int, any]()
193 | require.Error(t, json.Unmarshal([]byte(data), &om))
194 | }
195 | })
196 |
197 | t.Run("empty map", func(t *testing.T) {
198 | data := `{}`
199 |
200 | om := New[int, any]()
201 | require.NoError(t, json.Unmarshal([]byte(data), &om))
202 |
203 | assertLenEqual(t, om, 0)
204 | })
205 | }
206 |
207 | // const specialCharacters = "\\\\/\"\b\f\n\r\t\x00\uffff\ufffd世界\u007f\u00ff\U0010FFFF"
208 | const specialCharacters = "\uffff\ufffd世界\u007f\u00ff\U0010FFFF"
209 |
210 | func TestJSONSpecialCharacters(t *testing.T) {
211 | baselineMap := map[string]any{specialCharacters: specialCharacters}
212 | baselineData, err := json.Marshal(baselineMap)
213 | require.NoError(t, err) // baseline proves this key is supported by official json library
214 | t.Logf("specialCharacters: %#v as []rune:%v", specialCharacters, []rune(specialCharacters))
215 | t.Logf("baseline json data: %s", baselineData)
216 |
217 | t.Run("marshal special characters", func(t *testing.T) {
218 | om := New[string, any]()
219 | om.Set(specialCharacters, specialCharacters)
220 | b, err := json.Marshal(om)
221 | require.NoError(t, err)
222 | require.Equal(t, baselineData, b)
223 |
224 | type myString string
225 | om2 := New[myString, myString]()
226 | om2.Set(specialCharacters, specialCharacters)
227 | b, err = json.Marshal(om2)
228 | require.NoError(t, err)
229 | require.Equal(t, baselineData, b)
230 | })
231 |
232 | t.Run("unmarshall special characters", func(t *testing.T) {
233 | om := New[string, any]()
234 | require.NoError(t, json.Unmarshal(baselineData, &om))
235 | assertOrderedPairsEqual(t, om,
236 | []string{specialCharacters},
237 | []any{specialCharacters})
238 |
239 | type myString string
240 | om2 := New[myString, myString]()
241 | require.NoError(t, json.Unmarshal(baselineData, &om2))
242 | assertOrderedPairsEqual(t, om2,
243 | []myString{specialCharacters},
244 | []myString{specialCharacters})
245 | })
246 | }
247 |
248 | // to test structs that have nested map fields
249 | type nestedMaps struct {
250 | X int `json:"x" yaml:"x"`
251 | M *OrderedMap[string, []*OrderedMap[int, *OrderedMap[string, any]]] `json:"m" yaml:"m"`
252 | }
253 |
254 | func TestJSONRoundTrip(t *testing.T) {
255 | for _, testCase := range []struct {
256 | name string
257 | input string
258 | targetFactory func() any
259 | isPrettyPrinted bool
260 | }{
261 | {
262 | name: "",
263 | input: `{
264 | "x": 28,
265 | "m": {
266 | "foo": [
267 | {
268 | "12": {
269 | "i": 12,
270 | "b": true,
271 | "n": null,
272 | "m": {
273 | "a": "b",
274 | "c": 28
275 | }
276 | },
277 | "28": {
278 | "a": false,
279 | "b": [
280 | 1,
281 | 2,
282 | 3
283 | ]
284 | }
285 | },
286 | {
287 | "3": {
288 | "c": null,
289 | "d": 87
290 | },
291 | "4": {
292 | "e": true
293 | },
294 | "5": {
295 | "f": 4,
296 | "g": 5,
297 | "h": 6
298 | }
299 | }
300 | ],
301 | "bar": [
302 | {
303 | "5": {
304 | "foo": "bar"
305 | }
306 | }
307 | ]
308 | }
309 | }`,
310 | targetFactory: func() any { return &nestedMaps{} },
311 | isPrettyPrinted: true,
312 | },
313 | {
314 | name: "with UTF-8 special chars in key",
315 | input: `{"�":0}`,
316 | targetFactory: func() any { return &OrderedMap[string, int]{} },
317 | },
318 | } {
319 | t.Run(testCase.name, func(t *testing.T) {
320 | target := testCase.targetFactory()
321 |
322 | require.NoError(t, json.Unmarshal([]byte(testCase.input), target))
323 |
324 | var (
325 | out []byte
326 | err error
327 | )
328 | if testCase.isPrettyPrinted {
329 | out, err = json.MarshalIndent(target, "", " ")
330 | } else {
331 | out, err = json.Marshal(target)
332 | }
333 |
334 | if assert.NoError(t, err) {
335 | assert.Equal(t, strings.TrimSpace(testCase.input), string(out))
336 | }
337 | })
338 | }
339 | }
340 |
341 | func BenchmarkMarshalJSON(b *testing.B) {
342 | om := New[int, any]()
343 | om.Set(1, "bar")
344 | om.Set(7, "baz")
345 | om.Set(2, 28)
346 | om.Set(3, 100)
347 | om.Set(4, "baz")
348 | om.Set(5, "28")
349 | om.Set(6, "100")
350 | om.Set(8, "baz")
351 | om.Set(8, "baz")
352 |
353 | b.ResetTimer()
354 |
355 | for i := 0; i < b.N; i++ {
356 | _, _ = json.Marshal(om)
357 | }
358 | }
359 |
--------------------------------------------------------------------------------
/orderedmap.go:
--------------------------------------------------------------------------------
1 | // Package orderedmap implements an ordered map, i.e. a map that also keeps track of
2 | // the order in which keys were inserted.
3 | //
4 | // All operations are constant-time.
5 | //
6 | // Github repo: https://github.com/wk8/go-ordered-map
7 | package orderedmap
8 |
9 | import (
10 | "fmt"
11 | "iter"
12 |
13 | list "github.com/bahlo/generic-list-go"
14 | )
15 |
16 | type Pair[K comparable, V any] struct {
17 | Key K
18 | Value V
19 |
20 | element *list.Element[*Pair[K, V]]
21 | }
22 |
23 | type OrderedMap[K comparable, V any] struct {
24 | pairs map[K]*Pair[K, V]
25 | list *list.List[*Pair[K, V]]
26 | disableHTMLEscape bool
27 | }
28 |
29 | type initConfig[K comparable, V any] struct {
30 | capacity int
31 | initialData []Pair[K, V]
32 | disableHTMLEscape bool
33 | }
34 |
35 | type InitOption[K comparable, V any] func(config *initConfig[K, V])
36 |
37 | // WithCapacity allows giving a capacity hint for the map, akin to the standard make(map[K]V, capacity).
38 | func WithCapacity[K comparable, V any](capacity int) InitOption[K, V] {
39 | return func(c *initConfig[K, V]) {
40 | c.capacity = capacity
41 | }
42 | }
43 |
44 | // WithInitialData allows passing in initial data for the map.
45 | func WithInitialData[K comparable, V any](initialData ...Pair[K, V]) InitOption[K, V] {
46 | return func(c *initConfig[K, V]) {
47 | c.initialData = initialData
48 | if c.capacity < len(initialData) {
49 | c.capacity = len(initialData)
50 | }
51 | }
52 | }
53 |
54 | // WithDisableHTMLEscape disables HTMl escaping when marshalling to JSON
55 | func WithDisableHTMLEscape[K comparable, V any]() InitOption[K, V] {
56 | return func(c *initConfig[K, V]) {
57 | c.disableHTMLEscape = true
58 | }
59 | }
60 |
61 | // New creates a new OrderedMap.
62 | // options can either be one or several InitOption[K, V], or a single integer,
63 | // which is then interpreted as a capacity hint, à la make(map[K]V, capacity).
64 | func New[K comparable, V any](options ...any) *OrderedMap[K, V] {
65 | orderedMap := &OrderedMap[K, V]{}
66 |
67 | var config initConfig[K, V]
68 | for _, untypedOption := range options {
69 | switch option := untypedOption.(type) {
70 | case int:
71 | if len(options) != 1 {
72 | invalidOption()
73 | }
74 | config.capacity = option
75 | case bool:
76 | if len(options) != 1 {
77 | invalidOption()
78 | }
79 | config.disableHTMLEscape = option
80 |
81 | case InitOption[K, V]:
82 | option(&config)
83 |
84 | default:
85 | invalidOption()
86 | }
87 | }
88 |
89 | orderedMap.initialize(config.capacity, config.disableHTMLEscape)
90 | orderedMap.AddPairs(config.initialData...)
91 |
92 | return orderedMap
93 | }
94 |
95 | 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
96 |
97 | func invalidOption() { panic(invalidOptionMessage) }
98 |
99 | func (om *OrderedMap[K, V]) initialize(capacity int, disableHTMLEscape bool) {
100 | om.pairs = make(map[K]*Pair[K, V], capacity)
101 | om.list = list.New[*Pair[K, V]]()
102 | om.disableHTMLEscape = disableHTMLEscape
103 | }
104 |
105 | // Get looks for the given key, and returns the value associated with it,
106 | // or V's nil value if not found. The boolean it returns says whether the key is present in the map.
107 | func (om *OrderedMap[K, V]) Get(key K) (val V, present bool) {
108 | if pair, present := om.pairs[key]; present {
109 | return pair.Value, true
110 | }
111 |
112 | return
113 | }
114 |
115 | // Load is an alias for Get, mostly to present an API similar to `sync.Map`'s.
116 | func (om *OrderedMap[K, V]) Load(key K) (V, bool) {
117 | return om.Get(key)
118 | }
119 |
120 | // Value returns the value associated with the given key or the zero value.
121 | func (om *OrderedMap[K, V]) Value(key K) (val V) {
122 | if pair, present := om.pairs[key]; present {
123 | val = pair.Value
124 | }
125 | return
126 | }
127 |
128 | // GetPair looks for the given key, and returns the pair associated with it,
129 | // or nil if not found. The Pair struct can then be used to iterate over the ordered map
130 | // from that point, either forward or backward.
131 | func (om *OrderedMap[K, V]) GetPair(key K) *Pair[K, V] {
132 | return om.pairs[key]
133 | }
134 |
135 | // Set sets the key-value pair, and returns what `Get` would have returned
136 | // on that key prior to the call to `Set`.
137 | func (om *OrderedMap[K, V]) Set(key K, value V) (val V, present bool) {
138 | if pair, present := om.pairs[key]; present {
139 | oldValue := pair.Value
140 | pair.Value = value
141 | return oldValue, true
142 | }
143 |
144 | pair := &Pair[K, V]{
145 | Key: key,
146 | Value: value,
147 | }
148 | pair.element = om.list.PushBack(pair)
149 | om.pairs[key] = pair
150 |
151 | return
152 | }
153 |
154 | // AddPairs allows setting multiple pairs at a time. It's equivalent to calling
155 | // Set on each pair sequentially.
156 | func (om *OrderedMap[K, V]) AddPairs(pairs ...Pair[K, V]) {
157 | for _, pair := range pairs {
158 | om.Set(pair.Key, pair.Value)
159 | }
160 | }
161 |
162 | // Store is an alias for Set, mostly to present an API similar to `sync.Map`'s.
163 | func (om *OrderedMap[K, V]) Store(key K, value V) (V, bool) {
164 | return om.Set(key, value)
165 | }
166 |
167 | // Delete removes the key-value pair, and returns what `Get` would have returned
168 | // on that key prior to the call to `Delete`.
169 | func (om *OrderedMap[K, V]) Delete(key K) (val V, present bool) {
170 | if pair, present := om.pairs[key]; present {
171 | om.list.Remove(pair.element)
172 | delete(om.pairs, key)
173 | return pair.Value, true
174 | }
175 | return
176 | }
177 |
178 | // Len returns the length of the ordered map.
179 | func (om *OrderedMap[K, V]) Len() int {
180 | if om == nil || om.pairs == nil {
181 | return 0
182 | }
183 | return len(om.pairs)
184 | }
185 |
186 | // Oldest returns a pointer to the oldest pair. It's meant to be used to iterate on the ordered map's
187 | // pairs from the oldest to the newest, e.g.:
188 | // for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() { fmt.Printf("%v => %v\n", pair.Key, pair.Value) }
189 | func (om *OrderedMap[K, V]) Oldest() *Pair[K, V] {
190 | if om == nil || om.list == nil {
191 | return nil
192 | }
193 | return listElementToPair(om.list.Front())
194 | }
195 |
196 | // Newest returns a pointer to the newest pair. It's meant to be used to iterate on the ordered map's
197 | // pairs from the newest to the oldest, e.g.:
198 | // for pair := orderedMap.Newest(); pair != nil; pair = pair.Prev() { fmt.Printf("%v => %v\n", pair.Key, pair.Value) }
199 | func (om *OrderedMap[K, V]) Newest() *Pair[K, V] {
200 | if om == nil || om.list == nil {
201 | return nil
202 | }
203 | return listElementToPair(om.list.Back())
204 | }
205 |
206 | // Next returns a pointer to the next pair.
207 | func (p *Pair[K, V]) Next() *Pair[K, V] {
208 | return listElementToPair(p.element.Next())
209 | }
210 |
211 | // Prev returns a pointer to the previous pair.
212 | func (p *Pair[K, V]) Prev() *Pair[K, V] {
213 | return listElementToPair(p.element.Prev())
214 | }
215 |
216 | func listElementToPair[K comparable, V any](element *list.Element[*Pair[K, V]]) *Pair[K, V] {
217 | if element == nil {
218 | return nil
219 | }
220 | return element.Value
221 | }
222 |
223 | // KeyNotFoundError may be returned by functions in this package when they're called with keys that are not present
224 | // in the map.
225 | type KeyNotFoundError[K comparable] struct {
226 | MissingKey K
227 | }
228 |
229 | func (e *KeyNotFoundError[K]) Error() string {
230 | return fmt.Sprintf("missing key: %v", e.MissingKey)
231 | }
232 |
233 | // MoveAfter moves the value associated with key to its new position after the one associated with markKey.
234 | // Returns an error iff key or markKey are not present in the map. If an error is returned,
235 | // it will be a KeyNotFoundError.
236 | func (om *OrderedMap[K, V]) MoveAfter(key, markKey K) error {
237 | elements, err := om.getElements(key, markKey)
238 | if err != nil {
239 | return err
240 | }
241 | om.list.MoveAfter(elements[0], elements[1])
242 | return nil
243 | }
244 |
245 | // MoveBefore moves the value associated with key to its new position before the one associated with markKey.
246 | // Returns an error iff key or markKey are not present in the map. If an error is returned,
247 | // it will be a KeyNotFoundError.
248 | func (om *OrderedMap[K, V]) MoveBefore(key, markKey K) error {
249 | elements, err := om.getElements(key, markKey)
250 | if err != nil {
251 | return err
252 | }
253 | om.list.MoveBefore(elements[0], elements[1])
254 | return nil
255 | }
256 |
257 | func (om *OrderedMap[K, V]) getElements(keys ...K) ([]*list.Element[*Pair[K, V]], error) {
258 | elements := make([]*list.Element[*Pair[K, V]], len(keys))
259 | for i, k := range keys {
260 | pair, present := om.pairs[k]
261 | if !present {
262 | return nil, &KeyNotFoundError[K]{k}
263 | }
264 | elements[i] = pair.element
265 | }
266 | return elements, nil
267 | }
268 |
269 | // MoveToBack moves the value associated with key to the back of the ordered map,
270 | // i.e. makes it the newest pair in the map.
271 | // Returns an error iff key is not present in the map. If an error is returned,
272 | // it will be a KeyNotFoundError.
273 | func (om *OrderedMap[K, V]) MoveToBack(key K) error {
274 | _, err := om.GetAndMoveToBack(key)
275 | return err
276 | }
277 |
278 | // MoveToFront moves the value associated with key to the front of the ordered map,
279 | // i.e. makes it the oldest pair in the map.
280 | // Returns an error iff key is not present in the map. If an error is returned,
281 | // it will be a KeyNotFoundError.
282 | func (om *OrderedMap[K, V]) MoveToFront(key K) error {
283 | _, err := om.GetAndMoveToFront(key)
284 | return err
285 | }
286 |
287 | // GetAndMoveToBack combines Get and MoveToBack in the same call. If an error is returned,
288 | // it will be a KeyNotFoundError.
289 | func (om *OrderedMap[K, V]) GetAndMoveToBack(key K) (val V, err error) {
290 | if pair, present := om.pairs[key]; present {
291 | val = pair.Value
292 | om.list.MoveToBack(pair.element)
293 | } else {
294 | err = &KeyNotFoundError[K]{key}
295 | }
296 |
297 | return
298 | }
299 |
300 | // GetAndMoveToFront combines Get and MoveToFront in the same call. If an error is returned,
301 | // it will be a KeyNotFoundError.
302 | func (om *OrderedMap[K, V]) GetAndMoveToFront(key K) (val V, err error) {
303 | if pair, present := om.pairs[key]; present {
304 | val = pair.Value
305 | om.list.MoveToFront(pair.element)
306 | } else {
307 | err = &KeyNotFoundError[K]{key}
308 | }
309 |
310 | return
311 | }
312 |
313 | // FromOldest returns an iterator over all the key-value pairs in the map, starting from the oldest pair.
314 | func (om *OrderedMap[K, V]) FromOldest() iter.Seq2[K, V] {
315 | return func(yield func(K, V) bool) {
316 | for pair := om.Oldest(); pair != nil; pair = pair.Next() {
317 | if !yield(pair.Key, pair.Value) {
318 | return
319 | }
320 | }
321 | }
322 | }
323 |
324 | // FromNewest returns an iterator over all the key-value pairs in the map, starting from the newest pair.
325 | func (om *OrderedMap[K, V]) FromNewest() iter.Seq2[K, V] {
326 | return func(yield func(K, V) bool) {
327 | for pair := om.Newest(); pair != nil; pair = pair.Prev() {
328 | if !yield(pair.Key, pair.Value) {
329 | return
330 | }
331 | }
332 | }
333 | }
334 |
335 | // KeysFromOldest returns an iterator over all the keys in the map, starting from the oldest pair.
336 | func (om *OrderedMap[K, V]) KeysFromOldest() iter.Seq[K] {
337 | return func(yield func(K) bool) {
338 | for pair := om.Oldest(); pair != nil; pair = pair.Next() {
339 | if !yield(pair.Key) {
340 | return
341 | }
342 | }
343 | }
344 | }
345 |
346 | // KeysFromNewest returns an iterator over all the keys in the map, starting from the newest pair.
347 | func (om *OrderedMap[K, V]) KeysFromNewest() iter.Seq[K] {
348 | return func(yield func(K) bool) {
349 | for pair := om.Newest(); pair != nil; pair = pair.Prev() {
350 | if !yield(pair.Key) {
351 | return
352 | }
353 | }
354 | }
355 | }
356 |
357 | // ValuesFromOldest returns an iterator over all the values in the map, starting from the oldest pair.
358 | func (om *OrderedMap[K, V]) ValuesFromOldest() iter.Seq[V] {
359 | return func(yield func(V) bool) {
360 | for pair := om.Oldest(); pair != nil; pair = pair.Next() {
361 | if !yield(pair.Value) {
362 | return
363 | }
364 | }
365 | }
366 | }
367 |
368 | // ValuesFromNewest returns an iterator over all the values in the map, starting from the newest pair.
369 | func (om *OrderedMap[K, V]) ValuesFromNewest() iter.Seq[V] {
370 | return func(yield func(V) bool) {
371 | for pair := om.Newest(); pair != nil; pair = pair.Prev() {
372 | if !yield(pair.Value) {
373 | return
374 | }
375 | }
376 | }
377 | }
378 |
379 | // From creates a new OrderedMap from an iterator over key-value pairs.
380 | func From[K comparable, V any](i iter.Seq2[K, V]) *OrderedMap[K, V] {
381 | oMap := New[K, V]()
382 |
383 | for k, v := range i {
384 | oMap.Set(k, v)
385 | }
386 |
387 | return oMap
388 | }
389 |
390 | func (om *OrderedMap[K, V]) Filter(predicate func (K, V) bool) {
391 | for pair := om.Oldest(); pair != nil; {
392 | key, value := pair.Key, pair.Value
393 | pair = pair.Next()
394 | if !predicate(key, value) {
395 | om.Delete(key)
396 | }
397 | }
398 | }
399 |
--------------------------------------------------------------------------------
/orderedmap_test.go:
--------------------------------------------------------------------------------
1 | package orderedmap
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestBasicFeatures(t *testing.T) {
11 | n := 100
12 | om := New[int, int]()
13 |
14 | // set(i, 2 * i)
15 | for i := 0; i < n; i++ {
16 | assertLenEqual(t, om, i)
17 | oldValue, present := om.Set(i, 2*i)
18 | assertLenEqual(t, om, i+1)
19 |
20 | assert.Equal(t, 0, oldValue)
21 | assert.False(t, present)
22 | }
23 |
24 | // get what we just set
25 | for i := 0; i < n; i++ {
26 | value, present := om.Get(i)
27 |
28 | assert.Equal(t, 2*i, value)
29 | assert.Equal(t, value, om.Value(i))
30 | assert.True(t, present)
31 | }
32 |
33 | // get pairs of what we just set
34 | for i := 0; i < n; i++ {
35 | pair := om.GetPair(i)
36 |
37 | assert.NotNil(t, pair)
38 | assert.Equal(t, 2*i, pair.Value)
39 | }
40 |
41 | // forward iteration
42 | i := 0
43 | for pair := om.Oldest(); pair != nil; pair = pair.Next() {
44 | assert.Equal(t, i, pair.Key)
45 | assert.Equal(t, 2*i, pair.Value)
46 | i++
47 | }
48 | // backward iteration
49 | i = n - 1
50 | for pair := om.Newest(); pair != nil; pair = pair.Prev() {
51 | assert.Equal(t, i, pair.Key)
52 | assert.Equal(t, 2*i, pair.Value)
53 | i--
54 | }
55 |
56 | // forward iteration starting from known key
57 | i = 42
58 | for pair := om.GetPair(i); pair != nil; pair = pair.Next() {
59 | assert.Equal(t, i, pair.Key)
60 | assert.Equal(t, 2*i, pair.Value)
61 | i++
62 | }
63 |
64 | // double values for pairs with even keys
65 | for j := 0; j < n/2; j++ {
66 | i = 2 * j
67 | oldValue, present := om.Set(i, 4*i)
68 |
69 | assert.Equal(t, 2*i, oldValue)
70 | assert.True(t, present)
71 | }
72 | // and delete pairs with odd keys
73 | for j := 0; j < n/2; j++ {
74 | i = 2*j + 1
75 | assertLenEqual(t, om, n-j)
76 | value, present := om.Delete(i)
77 | assertLenEqual(t, om, n-j-1)
78 |
79 | assert.Equal(t, 2*i, value)
80 | assert.True(t, present)
81 |
82 | // deleting again shouldn't change anything
83 | value, present = om.Delete(i)
84 | assertLenEqual(t, om, n-j-1)
85 | assert.Equal(t, 0, value)
86 | assert.False(t, present)
87 | }
88 |
89 | // get the whole range
90 | for j := 0; j < n/2; j++ {
91 | i = 2 * j
92 | value, present := om.Get(i)
93 | assert.Equal(t, 4*i, value)
94 | assert.Equal(t, value, om.Value(i))
95 | assert.True(t, present)
96 |
97 | i = 2*j + 1
98 | value, present = om.Get(i)
99 | assert.Equal(t, 0, value)
100 | assert.Equal(t, value, om.Value(i))
101 | assert.False(t, present)
102 | }
103 |
104 | // check iterations again
105 | i = 0
106 | for pair := om.Oldest(); pair != nil; pair = pair.Next() {
107 | assert.Equal(t, i, pair.Key)
108 | assert.Equal(t, 4*i, pair.Value)
109 | i += 2
110 | }
111 | i = 2 * ((n - 1) / 2)
112 | for pair := om.Newest(); pair != nil; pair = pair.Prev() {
113 | assert.Equal(t, i, pair.Key)
114 | assert.Equal(t, 4*i, pair.Value)
115 | i -= 2
116 | }
117 | }
118 |
119 | func TestUpdatingDoesntChangePairsOrder(t *testing.T) {
120 | om := New[string, any]()
121 | om.Set("foo", "bar")
122 | om.Set("wk", 28)
123 | om.Set("po", 100)
124 | om.Set("bar", "baz")
125 |
126 | oldValue, present := om.Set("po", 102)
127 | assert.Equal(t, 100, oldValue)
128 | assert.True(t, present)
129 |
130 | assertOrderedPairsEqual(t, om,
131 | []string{"foo", "wk", "po", "bar"},
132 | []any{"bar", 28, 102, "baz"})
133 | }
134 |
135 | func TestDeletingAndReinsertingChangesPairsOrder(t *testing.T) {
136 | om := New[string, any]()
137 | om.Set("foo", "bar")
138 | om.Set("wk", 28)
139 | om.Set("po", 100)
140 | om.Set("bar", "baz")
141 |
142 | // delete a pair
143 | oldValue, present := om.Delete("po")
144 | assert.Equal(t, 100, oldValue)
145 | assert.True(t, present)
146 |
147 | // re-insert the same pair
148 | oldValue, present = om.Set("po", 100)
149 | assert.Nil(t, oldValue)
150 | assert.False(t, present)
151 |
152 | assertOrderedPairsEqual(t, om,
153 | []string{"foo", "wk", "bar", "po"},
154 | []any{"bar", 28, "baz", 100})
155 | }
156 |
157 | func TestEmptyMapOperations(t *testing.T) {
158 | om := New[string, any]()
159 |
160 | oldValue, present := om.Get("foo")
161 | assert.Nil(t, oldValue)
162 | assert.Nil(t, om.Value("foo"))
163 | assert.False(t, present)
164 |
165 | oldValue, present = om.Delete("bar")
166 | assert.Nil(t, oldValue)
167 | assert.False(t, present)
168 |
169 | assertLenEqual(t, om, 0)
170 |
171 | assert.Nil(t, om.Oldest())
172 | assert.Nil(t, om.Newest())
173 | }
174 |
175 | type dummyTestStruct struct {
176 | value string
177 | }
178 |
179 | func TestPackUnpackStructs(t *testing.T) {
180 | om := New[string, dummyTestStruct]()
181 | om.Set("foo", dummyTestStruct{"foo!"})
182 | om.Set("bar", dummyTestStruct{"bar!"})
183 |
184 | value, present := om.Get("foo")
185 | assert.True(t, present)
186 | assert.Equal(t, value, om.Value("foo"))
187 | if assert.NotNil(t, value) {
188 | assert.Equal(t, "foo!", value.value)
189 | }
190 |
191 | value, present = om.Set("bar", dummyTestStruct{"baz!"})
192 | assert.True(t, present)
193 | if assert.NotNil(t, value) {
194 | assert.Equal(t, "bar!", value.value)
195 | }
196 |
197 | value, present = om.Get("bar")
198 | assert.Equal(t, value, om.Value("bar"))
199 | assert.True(t, present)
200 | if assert.NotNil(t, value) {
201 | assert.Equal(t, "baz!", value.value)
202 | }
203 | }
204 |
205 | // shamelessly stolen from https://github.com/python/cpython/blob/e19a91e45fd54a56e39c2d12e6aaf4757030507f/Lib/test/test_ordered_dict.py#L55-L61
206 | func TestShuffle(t *testing.T) {
207 | ranLen := 100
208 |
209 | for _, n := range []int{0, 10, 20, 100, 1000, 10000} {
210 | t.Run(fmt.Sprintf("shuffle test with %d items", n), func(t *testing.T) {
211 | om := New[string, string]()
212 |
213 | keys := make([]string, n)
214 | values := make([]string, n)
215 |
216 | for i := 0; i < n; i++ {
217 | // we prefix with the number to ensure that we don't get any duplicates
218 | keys[i] = fmt.Sprintf("%d_%s", i, randomHexString(t, ranLen))
219 | values[i] = randomHexString(t, ranLen)
220 |
221 | value, present := om.Set(keys[i], values[i])
222 | assert.Equal(t, "", value)
223 | assert.False(t, present)
224 | }
225 |
226 | assertOrderedPairsEqual(t, om, keys, values)
227 | })
228 | }
229 | }
230 |
231 | func TestMove(t *testing.T) {
232 | om := New[int, any]()
233 | om.Set(1, "bar")
234 | om.Set(2, 28)
235 | om.Set(3, 100)
236 | om.Set(4, "baz")
237 | om.Set(5, "28")
238 | om.Set(6, "100")
239 | om.Set(7, "baz")
240 | om.Set(8, "baz")
241 |
242 | err := om.MoveAfter(2, 3)
243 | assert.Nil(t, err)
244 | assertOrderedPairsEqual(t, om,
245 | []int{1, 3, 2, 4, 5, 6, 7, 8},
246 | []any{"bar", 100, 28, "baz", "28", "100", "baz", "baz"})
247 |
248 | err = om.MoveBefore(6, 4)
249 | assert.Nil(t, err)
250 | assertOrderedPairsEqual(t, om,
251 | []int{1, 3, 2, 6, 4, 5, 7, 8},
252 | []any{"bar", 100, 28, "100", "baz", "28", "baz", "baz"})
253 |
254 | err = om.MoveToBack(3)
255 | assert.Nil(t, err)
256 | assertOrderedPairsEqual(t, om,
257 | []int{1, 2, 6, 4, 5, 7, 8, 3},
258 | []any{"bar", 28, "100", "baz", "28", "baz", "baz", 100})
259 |
260 | err = om.MoveToFront(5)
261 | assert.Nil(t, err)
262 | assertOrderedPairsEqual(t, om,
263 | []int{5, 1, 2, 6, 4, 7, 8, 3},
264 | []any{"28", "bar", 28, "100", "baz", "baz", "baz", 100})
265 |
266 | err = om.MoveToFront(100)
267 | assert.Equal(t, &KeyNotFoundError[int]{100}, err)
268 | }
269 |
270 | func TestGetAndMove(t *testing.T) {
271 | om := New[int, any]()
272 | om.Set(1, "bar")
273 | om.Set(2, 28)
274 | om.Set(3, 100)
275 | om.Set(4, "baz")
276 | om.Set(5, "28")
277 | om.Set(6, "100")
278 | om.Set(7, "baz")
279 | om.Set(8, "baz")
280 |
281 | value, err := om.GetAndMoveToBack(3)
282 | assert.Nil(t, err)
283 | assert.Equal(t, 100, value)
284 | assertOrderedPairsEqual(t, om,
285 | []int{1, 2, 4, 5, 6, 7, 8, 3},
286 | []any{"bar", 28, "baz", "28", "100", "baz", "baz", 100})
287 |
288 | value, err = om.GetAndMoveToFront(5)
289 | assert.Nil(t, err)
290 | assert.Equal(t, "28", value)
291 | assertOrderedPairsEqual(t, om,
292 | []int{5, 1, 2, 4, 6, 7, 8, 3},
293 | []any{"28", "bar", 28, "baz", "100", "baz", "baz", 100})
294 |
295 | value, err = om.GetAndMoveToBack(100)
296 | assert.Equal(t, &KeyNotFoundError[int]{100}, err)
297 | }
298 |
299 | func TestAddPairs(t *testing.T) {
300 | om := New[int, any]()
301 | om.AddPairs(
302 | Pair[int, any]{
303 | Key: 28,
304 | Value: "foo",
305 | },
306 | Pair[int, any]{
307 | Key: 12,
308 | Value: "bar",
309 | },
310 | Pair[int, any]{
311 | Key: 28,
312 | Value: "baz",
313 | },
314 | )
315 |
316 | assertOrderedPairsEqual(t, om,
317 | []int{28, 12},
318 | []any{"baz", "bar"})
319 | }
320 |
321 | // sadly, we can't test the "actual" capacity here, see https://github.com/golang/go/issues/52157
322 | func TestNewWithCapacity(t *testing.T) {
323 | zero := New[int, string](0)
324 | assert.Empty(t, zero.Len())
325 |
326 | assert.PanicsWithValue(t, invalidOptionMessage, func() {
327 | _ = New[int, string](1, 2)
328 | })
329 | assert.PanicsWithValue(t, invalidOptionMessage, func() {
330 | _ = New[int, string](1, 2, 3)
331 | })
332 |
333 | om := New[int, string](-1)
334 | om.Set(1337, "quarante-deux")
335 | assert.Equal(t, 1, om.Len())
336 | }
337 |
338 | func TestNewWithOptions(t *testing.T) {
339 | t.Run("wih capacity", func(t *testing.T) {
340 | om := New[string, any](WithCapacity[string, any](98))
341 | assert.Equal(t, 0, om.Len())
342 | })
343 |
344 | t.Run("with initial data", func(t *testing.T) {
345 | om := New[string, int](WithInitialData(
346 | Pair[string, int]{
347 | Key: "a",
348 | Value: 1,
349 | },
350 | Pair[string, int]{
351 | Key: "b",
352 | Value: 2,
353 | },
354 | Pair[string, int]{
355 | Key: "c",
356 | Value: 3,
357 | },
358 | ))
359 |
360 | assertOrderedPairsEqual(t, om,
361 | []string{"a", "b", "c"},
362 | []int{1, 2, 3})
363 | })
364 |
365 | t.Run("with an invalid option type", func(t *testing.T) {
366 | assert.PanicsWithValue(t, invalidOptionMessage, func() {
367 | _ = New[int, string]("foo")
368 | })
369 | })
370 | }
371 |
372 | func TestNilMap(t *testing.T) {
373 | // we want certain behaviors of a nil ordered map to be the same as they are for standard nil maps
374 | var om *OrderedMap[int, any]
375 |
376 | t.Run("len", func(t *testing.T) {
377 | assert.Equal(t, 0, om.Len())
378 | })
379 |
380 | t.Run("iterating - akin to range", func(t *testing.T) {
381 | assert.Nil(t, om.Oldest())
382 | assert.Nil(t, om.Newest())
383 | })
384 | }
385 |
386 | func TestIterators(t *testing.T) {
387 | om := New[int, any]()
388 | om.Set(1, "bar")
389 | om.Set(2, 28)
390 | om.Set(3, 100)
391 | om.Set(4, "baz")
392 | om.Set(5, "28")
393 | om.Set(6, "100")
394 | om.Set(7, "baz")
395 | om.Set(8, "baz")
396 |
397 | expectedKeys := []int{1, 2, 3, 4, 5, 6, 7, 8}
398 | expectedKeysFromNewest := []int{8, 7, 6, 5, 4, 3, 2, 1}
399 | expectedValues := []any{"bar", 28, 100, "baz", "28", "100", "baz", "baz"}
400 | expectedValuesFromNewest := []any{"baz", "baz", "100", "28", "baz", 100, 28, "bar"}
401 |
402 | var keys []int
403 | var values []any
404 |
405 | for k, v := range om.FromOldest() {
406 | keys = append(keys, k)
407 | values = append(values, v)
408 | }
409 |
410 | assert.Equal(t, expectedKeys, keys)
411 | assert.Equal(t, expectedValues, values)
412 |
413 | keys, values = []int{}, []any{}
414 |
415 | for k, v := range om.FromNewest() {
416 | keys = append(keys, k)
417 | values = append(values, v)
418 | }
419 |
420 | assert.Equal(t, expectedKeysFromNewest, keys)
421 | assert.Equal(t, expectedValuesFromNewest, values)
422 |
423 | keys = []int{}
424 |
425 | for k := range om.KeysFromOldest() {
426 | keys = append(keys, k)
427 | }
428 |
429 | assert.Equal(t, expectedKeys, keys)
430 |
431 | keys = []int{}
432 |
433 | for k := range om.KeysFromNewest() {
434 | keys = append(keys, k)
435 | }
436 |
437 | assert.Equal(t, expectedKeysFromNewest, keys)
438 |
439 | values = []any{}
440 |
441 | for v := range om.ValuesFromOldest() {
442 | values = append(values, v)
443 | }
444 |
445 | assert.Equal(t, expectedValues, values)
446 |
447 | values = []any{}
448 |
449 | for v := range om.ValuesFromNewest() {
450 | values = append(values, v)
451 | }
452 |
453 | assert.Equal(t, expectedValuesFromNewest, values)
454 | }
455 |
456 | func TestIteratorsFrom(t *testing.T) {
457 | om := New[int, any]()
458 | om.Set(1, "bar")
459 | om.Set(2, 28)
460 | om.Set(3, 100)
461 | om.Set(4, "baz")
462 | om.Set(5, "28")
463 | om.Set(6, "100")
464 | om.Set(7, "baz")
465 | om.Set(8, "baz")
466 |
467 | om2 := From(om.FromOldest())
468 |
469 | expectedKeys := []int{1, 2, 3, 4, 5, 6, 7, 8}
470 | expectedValues := []any{"bar", 28, 100, "baz", "28", "100", "baz", "baz"}
471 |
472 | var keys []int
473 | var values []any
474 |
475 | for k, v := range om2.FromOldest() {
476 | keys = append(keys, k)
477 | values = append(values, v)
478 | }
479 |
480 | assert.Equal(t, expectedKeys, keys)
481 | assert.Equal(t, expectedValues, values)
482 |
483 | expectedKeysFromNewest := []int{8, 7, 6, 5, 4, 3, 2, 1}
484 | expectedValuesFromNewest := []any{"baz", "baz", "100", "28", "baz", 100, 28, "bar"}
485 |
486 | om2 = From(om.FromNewest())
487 |
488 | keys = []int{}
489 | values = []any{}
490 |
491 | for k, v := range om2.FromOldest() {
492 | keys = append(keys, k)
493 | values = append(values, v)
494 | }
495 |
496 | assert.Equal(t, expectedKeysFromNewest, keys)
497 | assert.Equal(t, expectedValuesFromNewest, values)
498 | }
499 |
500 | func TestFilter(t *testing.T) {
501 | om := New[int, int]()
502 |
503 | n := 10 * 3 // ensure divisibility by 3 for the length check below
504 | for i := range n {
505 | om.Set(i, i*i)
506 | }
507 |
508 | om.Filter(func(k, v int) bool {
509 | return k % 3 == 0
510 | })
511 |
512 | assert.Equal(t, n / 3, om.Len())
513 | for k := range om.FromOldest() {
514 | assert.True(t, k%3==0)
515 | }
516 | }
517 |
--------------------------------------------------------------------------------
/testdata/fuzz/FuzzRoundTripJSON/62c005f96216d8ba8f62ac0799dfc1a6893e68418238a831ee79cd9c39b4cfc6:
--------------------------------------------------------------------------------
1 | go test fuzz v1
2 | []byte("{\"\xcc\":0}")
3 |
--------------------------------------------------------------------------------
/testdata/fuzz/FuzzRoundTripJSON/8093511184ad3e258aa13b957e75ff26c7fae64672dae0c0bc0a9fa5b61a05e7:
--------------------------------------------------------------------------------
1 | go test fuzz v1
2 | []byte("{}")
3 |
--------------------------------------------------------------------------------
/utils_test.go:
--------------------------------------------------------------------------------
1 | package orderedmap
2 |
3 | import (
4 | "crypto/rand"
5 | "encoding/hex"
6 | "fmt"
7 | "testing"
8 |
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | // assertOrderedPairsEqual asserts that the map contains the given keys and values
13 | // from oldest to newest.
14 | func assertOrderedPairsEqual[K comparable, V any](
15 | t *testing.T, orderedMap *OrderedMap[K, V], expectedKeys []K, expectedValues []V,
16 | ) {
17 | t.Helper()
18 |
19 | assertOrderedPairsEqualFromNewest(t, orderedMap, expectedKeys, expectedValues)
20 | assertOrderedPairsEqualFromOldest(t, orderedMap, expectedKeys, expectedValues)
21 | }
22 |
23 | func assertOrderedPairsEqualFromNewest[K comparable, V any](
24 | t *testing.T, orderedMap *OrderedMap[K, V], expectedKeys []K, expectedValues []V,
25 | ) {
26 | t.Helper()
27 |
28 | if assert.Equal(t, len(expectedKeys), len(expectedValues)) && assert.Equal(t, len(expectedKeys), orderedMap.Len()) {
29 | i := orderedMap.Len() - 1
30 | for pair := orderedMap.Newest(); pair != nil; pair = pair.Prev() {
31 | assert.Equal(t, expectedKeys[i], pair.Key, "from newest index=%d on key", i)
32 | assert.Equal(t, expectedValues[i], pair.Value, "from newest index=%d on value", i)
33 | i--
34 | }
35 | }
36 | }
37 |
38 | func assertOrderedPairsEqualFromOldest[K comparable, V any](
39 | t *testing.T, orderedMap *OrderedMap[K, V], expectedKeys []K, expectedValues []V,
40 | ) {
41 | t.Helper()
42 |
43 | if assert.Equal(t, len(expectedKeys), len(expectedValues)) && assert.Equal(t, len(expectedKeys), orderedMap.Len()) {
44 | i := 0
45 | for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() {
46 | assert.Equal(t, expectedKeys[i], pair.Key, "from oldest index=%d on key", i)
47 | assert.Equal(t, expectedValues[i], pair.Value, "from oldest index=%d on value", i)
48 | i++
49 | }
50 | }
51 | }
52 |
53 | func assertLenEqual[K comparable, V any](t *testing.T, orderedMap *OrderedMap[K, V], expectedLen int) {
54 | t.Helper()
55 |
56 | assert.Equal(t, expectedLen, orderedMap.Len())
57 |
58 | // also check the list length, for good measure
59 | assert.Equal(t, expectedLen, orderedMap.list.Len())
60 | }
61 |
62 | func randomHexString(t *testing.T, length int) string {
63 | t.Helper()
64 |
65 | b := length / 2 //nolint:gomnd
66 | randBytes := make([]byte, b)
67 |
68 | if n, err := rand.Read(randBytes); err != nil || n != b {
69 | if err == nil {
70 | err = fmt.Errorf("only got %v random bytes, expected %v", n, b)
71 | }
72 | t.Fatal(err)
73 | }
74 |
75 | return hex.EncodeToString(randBytes)
76 | }
77 |
--------------------------------------------------------------------------------
/yaml.go:
--------------------------------------------------------------------------------
1 | package orderedmap
2 |
3 | import (
4 | "fmt"
5 |
6 | "gopkg.in/yaml.v3"
7 | )
8 |
9 | var (
10 | _ yaml.Marshaler = &OrderedMap[int, any]{}
11 | _ yaml.Unmarshaler = &OrderedMap[int, any]{}
12 | )
13 |
14 | // MarshalYAML implements the yaml.Marshaler interface.
15 | func (om *OrderedMap[K, V]) MarshalYAML() (interface{}, error) {
16 | if om == nil {
17 | return []byte("null"), nil
18 | }
19 |
20 | node := yaml.Node{
21 | Kind: yaml.MappingNode,
22 | }
23 |
24 | for pair := om.Oldest(); pair != nil; pair = pair.Next() {
25 | key, value := pair.Key, pair.Value
26 |
27 | keyNode := &yaml.Node{}
28 |
29 | // serialize key to yaml, then deserialize it back into the node
30 | // this is a hack to get the correct tag for the key
31 | if err := keyNode.Encode(key); err != nil {
32 | return nil, err
33 | }
34 |
35 | valueNode := &yaml.Node{}
36 | if err := valueNode.Encode(value); err != nil {
37 | return nil, err
38 | }
39 |
40 | node.Content = append(node.Content, keyNode, valueNode)
41 | }
42 |
43 | return &node, nil
44 | }
45 |
46 | // UnmarshalYAML implements the yaml.Unmarshaler interface.
47 | func (om *OrderedMap[K, V]) UnmarshalYAML(value *yaml.Node) error {
48 | if value.Kind != yaml.MappingNode {
49 | return fmt.Errorf("pipeline must contain YAML mapping, has %v", value.Kind)
50 | }
51 |
52 | if om.list == nil {
53 | om.initialize(0, om.disableHTMLEscape)
54 | }
55 |
56 | for index := 0; index < len(value.Content); index += 2 {
57 | var key K
58 | var val V
59 |
60 | if err := value.Content[index].Decode(&key); err != nil {
61 | return err
62 | }
63 | if err := value.Content[index+1].Decode(&val); err != nil {
64 | return err
65 | }
66 |
67 | om.Set(key, val)
68 | }
69 |
70 | return nil
71 | }
72 |
--------------------------------------------------------------------------------
/yaml_fuzz_test.go:
--------------------------------------------------------------------------------
1 | package orderedmap
2 |
3 | // Adapted from https://github.com/dvyukov/go-fuzz-corpus/blob/c42c1b2/json/json.go
4 |
5 | import (
6 | "github.com/stretchr/testify/assert"
7 | "github.com/stretchr/testify/require"
8 | "gopkg.in/yaml.v3"
9 | "testing"
10 | )
11 |
12 | func FuzzRoundTripYAML(f *testing.F) {
13 | f.Fuzz(func(t *testing.T, data []byte) {
14 | for _, testCase := range []struct {
15 | name string
16 | constructor func() any
17 | // should be a function that asserts that 2 objects of the type returned by constructor are equal
18 | equalityAssertion func(*testing.T, any, any) bool
19 | }{
20 | {
21 | name: "with a string -> string map",
22 | constructor: func() any { return &OrderedMap[string, string]{} },
23 | equalityAssertion: assertOrderedMapsEqual[string, string],
24 | },
25 | {
26 | name: "with a string -> int map",
27 | constructor: func() any { return &OrderedMap[string, int]{} },
28 | equalityAssertion: assertOrderedMapsEqual[string, int],
29 | },
30 | {
31 | name: "with a string -> any map",
32 | constructor: func() any { return &OrderedMap[string, any]{} },
33 | equalityAssertion: assertOrderedMapsEqual[string, any],
34 | },
35 | {
36 | name: "with a struct with map fields",
37 | constructor: func() any { return new(testFuzzStruct) },
38 | equalityAssertion: assertTestFuzzStructEqual,
39 | },
40 | } {
41 | t.Run(testCase.name, func(t *testing.T) {
42 | v1 := testCase.constructor()
43 | if yaml.Unmarshal(data, v1) != nil {
44 | return
45 | }
46 | t.Log(data)
47 | t.Log(v1)
48 |
49 | yamlData, err := yaml.Marshal(v1)
50 | require.NoError(t, err)
51 | t.Log(string(yamlData))
52 |
53 | v2 := testCase.constructor()
54 | err = yaml.Unmarshal(yamlData, v2)
55 | if err != nil {
56 | t.Log(string(yamlData))
57 | t.Fatal(err)
58 | }
59 |
60 | if !assert.True(t, testCase.equalityAssertion(t, v1, v2), "failed with input data %q", string(data)) {
61 | // look at that what the standard lib does with regular map, to help with debugging
62 |
63 | var m1 map[string]any
64 | require.NoError(t, yaml.Unmarshal(data, &m1))
65 |
66 | mapJsonData, err := yaml.Marshal(m1)
67 | require.NoError(t, err)
68 |
69 | var m2 map[string]any
70 | require.NoError(t, yaml.Unmarshal(mapJsonData, &m2))
71 |
72 | t.Logf("initial data = %s", string(data))
73 | t.Logf("unmarshalled map = %v", m1)
74 | t.Logf("re-marshalled from map = %s", string(mapJsonData))
75 | t.Logf("re-marshalled from test obj = %s", string(yamlData))
76 | t.Logf("re-unmarshalled map = %s", m2)
77 | }
78 | })
79 | }
80 | })
81 | }
82 |
--------------------------------------------------------------------------------
/yaml_test.go:
--------------------------------------------------------------------------------
1 | package orderedmap
2 |
3 | import (
4 | "github.com/stretchr/testify/assert"
5 | "github.com/stretchr/testify/require"
6 | "gopkg.in/yaml.v3"
7 | "testing"
8 | )
9 |
10 | func TestMarshalYAML(t *testing.T) {
11 | t.Run("int key", func(t *testing.T) {
12 | om := New[int, any]()
13 | om.Set(1, "bar")
14 | om.Set(7, "baz")
15 | om.Set(2, 28)
16 | om.Set(3, 100)
17 | om.Set(4, "baz")
18 | om.Set(5, "28")
19 | om.Set(6, "100")
20 | om.Set(8, "baz")
21 | om.Set(8, "baz")
22 | om.Set(9, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque auctor augue accumsan mi maximus, quis viverra massa pretium. Phasellus imperdiet sapien a interdum sollicitudin. Duis at commodo lectus, a lacinia sem.")
23 |
24 | b, err := yaml.Marshal(om)
25 |
26 | expected := `1: bar
27 | 7: baz
28 | 2: 28
29 | 3: 100
30 | 4: baz
31 | 5: "28"
32 | 6: "100"
33 | 8: baz
34 | 9: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque auctor augue accumsan mi maximus, quis viverra massa pretium. Phasellus imperdiet sapien a interdum sollicitudin. Duis at commodo lectus, a lacinia sem.
35 | `
36 | assert.NoError(t, err)
37 | assert.Equal(t, expected, string(b))
38 | })
39 |
40 | t.Run("string key", func(t *testing.T) {
41 | om := New[string, any]()
42 | om.Set("test", "bar")
43 | om.Set("abc", true)
44 |
45 | b, err := yaml.Marshal(om)
46 | assert.NoError(t, err)
47 | expected := `test: bar
48 | abc: true
49 | `
50 | assert.Equal(t, expected, string(b))
51 | })
52 |
53 | t.Run("typed string key", func(t *testing.T) {
54 | type myString string
55 | om := New[myString, any]()
56 | om.Set("test", "bar")
57 | om.Set("abc", true)
58 |
59 | b, err := yaml.Marshal(om)
60 | assert.NoError(t, err)
61 | assert.Equal(t, `test: bar
62 | abc: true
63 | `, string(b))
64 | })
65 |
66 | t.Run("typed int key", func(t *testing.T) {
67 | type myInt uint32
68 | om := New[myInt, any]()
69 | om.Set(1, "bar")
70 | om.Set(7, "baz")
71 | om.Set(2, 28)
72 | om.Set(3, 100)
73 | om.Set(4, "baz")
74 |
75 | b, err := yaml.Marshal(om)
76 | assert.NoError(t, err)
77 | assert.Equal(t, `1: bar
78 | 7: baz
79 | 2: 28
80 | 3: 100
81 | 4: baz
82 | `, string(b))
83 | })
84 |
85 | t.Run("TextMarshaller key", func(t *testing.T) {
86 | om := New[marshallable, any]()
87 | om.Set(marshallable(1), "bar")
88 | om.Set(marshallable(28), true)
89 |
90 | b, err := yaml.Marshal(om)
91 | assert.NoError(t, err)
92 | assert.Equal(t, `'#1#': bar
93 | '#28#': true
94 | `, string(b))
95 | })
96 |
97 | t.Run("empty map with 0 elements", func(t *testing.T) {
98 | om := New[string, any]()
99 |
100 | b, err := yaml.Marshal(om)
101 | assert.NoError(t, err)
102 | assert.Equal(t, "{}\n", string(b))
103 | })
104 |
105 | t.Run("empty map with no elements (null)", func(t *testing.T) {
106 | om := &OrderedMap[string, string]{}
107 |
108 | b, err := yaml.Marshal(om)
109 | assert.NoError(t, err)
110 | assert.Equal(t, "{}\n", string(b))
111 | })
112 | }
113 |
114 | func TestUnmarshallYAML(t *testing.T) {
115 | t.Run("int key", func(t *testing.T) {
116 | data := `
117 | 1: bar
118 | 7: baz
119 | 2: 28
120 | 3: 100
121 | 4: baz
122 | 5: "28"
123 | 6: "100"
124 | 8: baz
125 | `
126 | om := New[int, any]()
127 | require.NoError(t, yaml.Unmarshal([]byte(data), &om))
128 |
129 | assertOrderedPairsEqual(t, om,
130 | []int{1, 7, 2, 3, 4, 5, 6, 8},
131 | []any{"bar", "baz", 28, 100, "baz", "28", "100", "baz"})
132 |
133 | // serialize back to yaml to make sure things are equal
134 | })
135 |
136 | t.Run("string key", func(t *testing.T) {
137 | data := `{"test":"bar","abc":true}`
138 |
139 | om := New[string, any]()
140 | require.NoError(t, yaml.Unmarshal([]byte(data), &om))
141 |
142 | assertOrderedPairsEqual(t, om,
143 | []string{"test", "abc"},
144 | []any{"bar", true})
145 | })
146 |
147 | t.Run("typed string key", func(t *testing.T) {
148 | data := `{"test":"bar","abc":true}`
149 |
150 | type myString string
151 | om := New[myString, any]()
152 | require.NoError(t, yaml.Unmarshal([]byte(data), &om))
153 |
154 | assertOrderedPairsEqual(t, om,
155 | []myString{"test", "abc"},
156 | []any{"bar", true})
157 | })
158 |
159 | t.Run("typed int key", func(t *testing.T) {
160 | data := `
161 | 1: bar
162 | 7: baz
163 | 2: 28
164 | 3: 100
165 | 4: baz
166 | 5: "28"
167 | 6: "100"
168 | 8: baz
169 | `
170 | type myInt uint32
171 | om := New[myInt, any]()
172 | require.NoError(t, yaml.Unmarshal([]byte(data), &om))
173 |
174 | assertOrderedPairsEqual(t, om,
175 | []myInt{1, 7, 2, 3, 4, 5, 6, 8},
176 | []any{"bar", "baz", 28, 100, "baz", "28", "100", "baz"})
177 | })
178 |
179 | t.Run("TextUnmarshaler key", func(t *testing.T) {
180 | data := `{"#1#":"bar","#28#":true}`
181 |
182 | om := New[marshallable, any]()
183 | require.NoError(t, yaml.Unmarshal([]byte(data), &om))
184 |
185 | assertOrderedPairsEqual(t, om,
186 | []marshallable{1, 28},
187 | []any{"bar", true})
188 | })
189 |
190 | t.Run("when fed with an input that's not an object", func(t *testing.T) {
191 | for _, data := range []string{"true", `["foo"]`, "42", `"foo"`} {
192 | om := New[int, any]()
193 | require.Error(t, yaml.Unmarshal([]byte(data), &om))
194 | }
195 | })
196 |
197 | t.Run("empty map", func(t *testing.T) {
198 | data := `{}`
199 |
200 | om := New[int, any]()
201 | require.NoError(t, yaml.Unmarshal([]byte(data), &om))
202 |
203 | assertLenEqual(t, om, 0)
204 | })
205 | }
206 |
207 | func TestYAMLSpecialCharacters(t *testing.T) {
208 | baselineMap := map[string]any{specialCharacters: specialCharacters}
209 | baselineData, err := yaml.Marshal(baselineMap)
210 | require.NoError(t, err) // baseline proves this key is supported by official yaml library
211 | t.Logf("specialCharacters: %#v as []rune:%v", specialCharacters, []rune(specialCharacters))
212 | t.Logf("baseline yaml data: %s", baselineData)
213 |
214 | t.Run("marshal special characters", func(t *testing.T) {
215 | om := New[string, any]()
216 | om.Set(specialCharacters, specialCharacters)
217 | b, err := yaml.Marshal(om)
218 | require.NoError(t, err)
219 | require.Equal(t, baselineData, b)
220 |
221 | type myString string
222 | om2 := New[myString, myString]()
223 | om2.Set(specialCharacters, specialCharacters)
224 | b, err = yaml.Marshal(om2)
225 | require.NoError(t, err)
226 | require.Equal(t, baselineData, b)
227 | })
228 |
229 | t.Run("unmarshall special characters", func(t *testing.T) {
230 | om := New[string, any]()
231 | require.NoError(t, yaml.Unmarshal(baselineData, &om))
232 | assertOrderedPairsEqual(t, om,
233 | []string{specialCharacters},
234 | []any{specialCharacters})
235 |
236 | type myString string
237 | om2 := New[myString, myString]()
238 | require.NoError(t, yaml.Unmarshal(baselineData, &om2))
239 | assertOrderedPairsEqual(t, om2,
240 | []myString{specialCharacters},
241 | []myString{specialCharacters})
242 | })
243 | }
244 |
245 | func TestYAMLRoundTrip(t *testing.T) {
246 | for _, testCase := range []struct {
247 | name string
248 | input string
249 | targetFactory func() any
250 | }{
251 | {
252 | name: "empty map",
253 | input: "{}\n",
254 | targetFactory: func() any {
255 | return &OrderedMap[string, any]{}
256 | },
257 | },
258 | {
259 | name: "",
260 | input: `x: 28
261 | m:
262 | bar:
263 | - 5:
264 | foo: bar
265 | foo:
266 | - 12:
267 | b: true
268 | i: 12
269 | m:
270 | a: b
271 | c: 28
272 | "n": null
273 | 28:
274 | a: false
275 | b:
276 | - 1
277 | - 2
278 | - 3
279 | - 3:
280 | c: null
281 | d: 87
282 | 4:
283 | e: true
284 | 5:
285 | f: 4
286 | g: 5
287 | h: 6
288 | `,
289 | targetFactory: func() any { return &nestedMaps{} },
290 | },
291 | {
292 | name: "with UTF-8 special chars in key",
293 | input: "�: 0\n",
294 | targetFactory: func() any { return &OrderedMap[string, int]{} },
295 | },
296 | } {
297 | t.Run(testCase.name, func(t *testing.T) {
298 | target := testCase.targetFactory()
299 |
300 | require.NoError(t, yaml.Unmarshal([]byte(testCase.input), target))
301 |
302 | var (
303 | out []byte
304 | err error
305 | )
306 |
307 | out, err = yaml.Marshal(target)
308 |
309 | if assert.NoError(t, err) {
310 | assert.Equal(t, testCase.input, string(out))
311 | }
312 | })
313 | }
314 | }
315 |
316 | func BenchmarkMarshalYAML(b *testing.B) {
317 | om := New[int, any]()
318 | om.Set(1, "bar")
319 | om.Set(7, "baz")
320 | om.Set(2, 28)
321 | om.Set(3, 100)
322 | om.Set(4, "baz")
323 | om.Set(5, "28")
324 | om.Set(6, "100")
325 | om.Set(8, "baz")
326 | om.Set(8, "baz")
327 |
328 | b.ResetTimer()
329 |
330 | for i := 0; i < b.N; i++ {
331 | _, _ = yaml.Marshal(om)
332 | }
333 | }
334 |
--------------------------------------------------------------------------------