Skip to content

Commit 0719621

Browse files
committed
Add size return to ReadMap and ReadSlice methods
Update ReadMap and ReadSlice to return collection size along with iterators, enabling efficient pre-allocation of maps and slices. Iterator remains the primary return value for natural usage patterns.
1 parent 92c2915 commit 0719621

File tree

7 files changed

+95
-45
lines changed

7 files changed

+95
-45
lines changed

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Version 2.0 includes significant improvements:
2626
- **Network Iteration**: Iterate over all networks in a database with
2727
`Networks()` and `NetworksWithin()`
2828
- **Enhanced Performance**: Optimized data structures and decoding paths
29-
- **Go 1.24+ Support**: Takes advantage of modern Go features including
29+
- **Go 1.23+ Support**: Takes advantage of modern Go features including
3030
iterators
3131
- **Better Error Handling**: More detailed error types and improved debugging
3232

@@ -117,13 +117,23 @@ type FastCity struct {
117117
}
118118

119119
func (c *FastCity) UnmarshalMaxMindDB(d *maxminddb.Decoder) error {
120-
for key, err := range d.ReadMap() {
120+
mapIter, size, err := d.ReadMap()
121+
if err != nil {
122+
return err
123+
}
124+
// Pre-allocate with correct capacity for better performance
125+
_ = size // Use for pre-allocation if storing map data
126+
for key, err := range mapIter {
121127
if err != nil {
122128
return err
123129
}
124130
switch string(key) {
125131
case "country":
126-
for countryKey, countryErr := range d.ReadMap() {
132+
countryIter, _, err := d.ReadMap()
133+
if err != nil {
134+
return err
135+
}
136+
for countryKey, countryErr := range countryIter {
127137
if countryErr != nil {
128138
return countryErr
129139
}

example_test.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,23 +183,35 @@ type CustomCity struct {
183183
// This provides custom decoding logic, similar to how json.Unmarshaler works
184184
// with encoding/json, allowing fine-grained control over data processing.
185185
func (c *CustomCity) UnmarshalMaxMindDB(d *mmdbdata.Decoder) error {
186-
for key, err := range d.ReadMap() {
186+
mapIter, _, err := d.ReadMap()
187+
if err != nil {
188+
return err
189+
}
190+
for key, err := range mapIter {
187191
if err != nil {
188192
return err
189193
}
190194

191195
switch string(key) {
192196
case "city":
193197
// Decode nested city structure
194-
for cityKey, cityErr := range d.ReadMap() {
198+
cityMapIter, _, err := d.ReadMap()
199+
if err != nil {
200+
return err
201+
}
202+
for cityKey, cityErr := range cityMapIter {
195203
if cityErr != nil {
196204
return cityErr
197205
}
198206
switch string(cityKey) {
199207
case "names":
200208
// Decode nested map[string]string for localized names
201209
names := make(map[string]string)
202-
for nameKey, nameErr := range d.ReadMap() {
210+
nameMapIter, _, err := d.ReadMap()
211+
if err != nil {
212+
return err
213+
}
214+
for nameKey, nameErr := range nameMapIter {
203215
if nameErr != nil {
204216
return nameErr
205217
}

internal/decoder/decoder.go

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -214,20 +214,20 @@ func (d *Decoder) ReadUInt128() (hi, lo uint64, err error) {
214214
return hi, lo, nil
215215
}
216216

217-
// ReadMap returns an iterator to read the map. The first value from the
218-
// iterator is the key. Please note that this byte slice is only valid during
219-
// the iteration. This is done to avoid an unnecessary allocation. You must
220-
// make a copy of it if you are storing it for later use. The second value is
221-
// an error indicating that the database is malformed or that the pointed
222-
// value is not a map.
223-
func (d *Decoder) ReadMap() iter.Seq2[[]byte, error] {
224-
return func(yield func([]byte, error) bool) {
225-
size, offset, err := d.decodeCtrlDataAndFollow(KindMap)
226-
if err != nil {
227-
yield(nil, d.wrapError(err))
228-
return
229-
}
217+
// ReadMap returns an iterator to read the map along with the map size. The
218+
// size can be used to pre-allocate a map with the correct capacity for better
219+
// performance. The first value from the iterator is the key. Please note that
220+
// this byte slice is only valid during the iteration. This is done to avoid
221+
// an unnecessary allocation. You must make a copy of it if you are storing it
222+
// for later use. The second value is an error indicating that the database is
223+
// malformed or that the pointed value is not a map.
224+
func (d *Decoder) ReadMap() (iter.Seq2[[]byte, error], uint, error) {
225+
size, offset, err := d.decodeCtrlDataAndFollow(KindMap)
226+
if err != nil {
227+
return nil, 0, d.wrapError(err)
228+
}
230229

230+
iterator := func(yield func([]byte, error) bool) {
231231
currentOffset := offset
232232

233233
for range size {
@@ -257,19 +257,21 @@ func (d *Decoder) ReadMap() iter.Seq2[[]byte, error] {
257257
// Set the final offset after map iteration
258258
d.reset(currentOffset)
259259
}
260+
261+
return iterator, size, nil
260262
}
261263

262-
// ReadSlice returns an iterator over the values of the slice. The iterator
263-
// returns an error if the database is malformed or if the pointed value is
264-
// not a slice.
265-
func (d *Decoder) ReadSlice() iter.Seq[error] {
266-
return func(yield func(error) bool) {
267-
size, offset, err := d.decodeCtrlDataAndFollow(KindSlice)
268-
if err != nil {
269-
yield(d.wrapError(err))
270-
return
271-
}
264+
// ReadSlice returns an iterator over the values of the slice along with the
265+
// slice size. The size can be used to pre-allocate a slice with the correct
266+
// capacity for better performance. The iterator returns an error if the
267+
// database is malformed or if the pointed value is not a slice.
268+
func (d *Decoder) ReadSlice() (iter.Seq[error], uint, error) {
269+
size, offset, err := d.decodeCtrlDataAndFollow(KindSlice)
270+
if err != nil {
271+
return nil, 0, d.wrapError(err)
272+
}
272273

274+
iterator := func(yield func(error) bool) {
273275
currentOffset := offset
274276

275277
for i := range size {
@@ -301,6 +303,8 @@ func (d *Decoder) ReadSlice() iter.Seq[error] {
301303
// Set final offset after slice iteration
302304
d.reset(currentOffset)
303305
}
306+
307+
return iterator, size, nil
304308
}
305309

306310
// SkipValue skips over the current value without decoding it.

internal/decoder/decoder_test.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,9 @@ func TestDecodeMap(t *testing.T) {
146146
for hexStr, expected := range tests {
147147
t.Run(hexStr, func(t *testing.T) {
148148
decoder := newDecoderFromHex(t, hexStr)
149-
resultMap := make(map[string]any)
150-
mapIter := decoder.ReadMap() // [cite: 53]
149+
mapIter, size, err := decoder.ReadMap() // [cite: 53]
150+
require.NoError(t, err, "ReadMap failed")
151+
resultMap := make(map[string]any, size) // Pre-allocate with correct capacity
151152

152153
// Iterate through the map [cite: 54]
153154
for keyBytes, err := range mapIter { // [cite: 50]
@@ -178,8 +179,9 @@ func TestDecodeSlice(t *testing.T) {
178179
for hexStr, expected := range tests {
179180
t.Run(hexStr, func(t *testing.T) {
180181
decoder := newDecoderFromHex(t, hexStr)
181-
results := make([]any, 0)
182-
sliceIter := decoder.ReadSlice() // [cite: 56]
182+
sliceIter, size, err := decoder.ReadSlice() // [cite: 56]
183+
require.NoError(t, err, "ReadSlice failed")
184+
results := make([]any, 0, size) // Pre-allocate with correct capacity
183185

184186
// Iterate through the slice [cite: 57]
185187
for err := range sliceIter {
@@ -359,7 +361,9 @@ func TestPointersInDecoder(t *testing.T) {
359361
actualValue := make(map[string]string)
360362

361363
// Expecting a map at the target offset (may be behind a pointer)
362-
mapIter := decoder.ReadMap()
364+
mapIter, size, err := decoder.ReadMap()
365+
require.NoError(t, err, "ReadMap failed")
366+
_ = size // Use size if needed for pre-allocation
363367
for keyBytes, errIter := range mapIter {
364368
require.NoError(t, errIter)
365369
key := string(keyBytes)

internal/decoder/error_context_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,8 @@ func TestContextualErrorIntegration(t *testing.T) {
171171
// Test new Decoder API - no automatic path tracking
172172
dd := NewDataDecoder(buffer)
173173
decoder := NewDecoder(dd, 0)
174-
mapIter := decoder.ReadMap()
174+
mapIter, _, err := decoder.ReadMap()
175+
require.NoError(t, err, "ReadMap failed")
175176

176177
var mapErr error
177178
for _, iterErr := range mapIter {
@@ -230,7 +231,8 @@ func TestContextualErrorIntegration(t *testing.T) {
230231
decoder := NewDecoder(dd, 0)
231232

232233
// Navigate through the nested structure manually
233-
mapIter := decoder.ReadMap()
234+
mapIter, _, err := decoder.ReadMap()
235+
require.NoError(t, err, "ReadMap failed")
234236
var mapErr error
235237

236238
for key, iterErr := range mapIter {
@@ -241,7 +243,8 @@ func TestContextualErrorIntegration(t *testing.T) {
241243
require.Equal(t, "list", string(key))
242244

243245
// Read the array
244-
sliceIter := decoder.ReadSlice()
246+
sliceIter, _, err := decoder.ReadSlice()
247+
require.NoError(t, err, "ReadSlice failed")
245248
sliceIndex := 0
246249
for sliceIterErr := range sliceIter {
247250
if sliceIterErr != nil {
@@ -251,7 +254,8 @@ func TestContextualErrorIntegration(t *testing.T) {
251254
require.Equal(t, 0, sliceIndex) // Should be first element
252255

253256
// Read the nested map (array element)
254-
innerMapIter := decoder.ReadMap()
257+
innerMapIter, _, err := decoder.ReadMap()
258+
require.NoError(t, err, "ReadMap failed")
255259
for innerKey, innerIterErr := range innerMapIter {
256260
if innerIterErr != nil {
257261
mapErr = innerIterErr

mmdbdata/doc.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,16 @@
1414
// }
1515
//
1616
// func (c *City) UnmarshalMaxMindDB(d *mmdbdata.Decoder) error {
17-
// for key, err := range d.ReadMap() {
17+
// mapIter, _, err := d.ReadMap()
18+
// if err != nil { return err }
19+
// for key, err := range mapIter {
1820
// if err != nil { return err }
1921
// switch string(key) {
2022
// case "names":
21-
// names := make(map[string]string)
22-
// for nameKey, nameErr := range d.ReadMap() {
23+
// nameIter, size, err := d.ReadMap()
24+
// if err != nil { return err }
25+
// names := make(map[string]string, size) // Pre-allocate with size
26+
// for nameKey, nameErr := range nameIter {
2327
// if nameErr != nil { return nameErr }
2428
// value, valueErr := d.ReadString()
2529
// if valueErr != nil { return valueErr }

reader_test.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,16 +1119,24 @@ type TestCity struct {
11191119
// UnmarshalMaxMindDB implements the Unmarshaler interface for TestCity.
11201120
// This demonstrates custom decoding that avoids reflection for better performance.
11211121
func (c *TestCity) UnmarshalMaxMindDB(d *mmdbdata.Decoder) error {
1122-
for key, err := range d.ReadMap() {
1122+
mapIter, _, err := d.ReadMap()
1123+
if err != nil {
1124+
return err
1125+
}
1126+
for key, err := range mapIter {
11231127
if err != nil {
11241128
return err
11251129
}
11261130

11271131
switch string(key) {
11281132
case "names":
11291133
// Decode nested map[string]string for localized names
1130-
names := make(map[string]string)
1131-
for nameKey, nameErr := range d.ReadMap() {
1134+
nameMapIter, size, err := d.ReadMap()
1135+
if err != nil {
1136+
return err
1137+
}
1138+
names := make(map[string]string, size) // Pre-allocate with correct capacity
1139+
for nameKey, nameErr := range nameMapIter {
11321140
if nameErr != nil {
11331141
return nameErr
11341142
}
@@ -1163,7 +1171,11 @@ type TestASN struct {
11631171

11641172
// UnmarshalMaxMindDB implements the Unmarshaler interface for TestASN.
11651173
func (a *TestASN) UnmarshalMaxMindDB(d *mmdbdata.Decoder) error {
1166-
for key, err := range d.ReadMap() {
1174+
mapIter, _, err := d.ReadMap()
1175+
if err != nil {
1176+
return err
1177+
}
1178+
for key, err := range mapIter {
11671179
if err != nil {
11681180
return err
11691181
}

0 commit comments

Comments
 (0)