├── .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 | [![Go Reference](https://pkg.go.dev/badge/github.com/wk8/go-ordered-map/v2.svg)](https://pkg.go.dev/github.com/wk8/go-ordered-map/v2) 2 | [![Build Status](https://circleci.com/gh/wk8/go-ordered-map.svg?style=svg)](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 | --------------------------------------------------------------------------------