Skip to content

Commit c18b036

Browse files
committed
Add support for UnmarshalMaxMinDB
1 parent acf7938 commit c18b036

File tree

9 files changed

+883
-91
lines changed

9 files changed

+883
-91
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66
- `IncludeNetworksWithoutData` and `IncludeAliasedNetworks` now return a
77
`NetworksOption` rather than being one themselves. This was done to improve
88
the documentation organization.
9+
- Added `Unmarshaler` interface to allow custom decoding implementations for
10+
performance-critical applications. Types implementing
11+
`UnmarshalMaxMindDB(d *Decoder) error` will automatically use custom
12+
decoding logic instead of reflection, following the same pattern as
13+
`json.Unmarshaler`.
14+
- Added public `Decoder` type with methods for manual decoding including
15+
`DecodeMap()`, `DecodeSlice()`, `DecodeString()`, `DecodeUInt32()`, etc.
916

1017
## 2.0.0-beta.3 - 2025-02-16
1118

decoder.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package maxminddb
2+
3+
import "github.com/oschwald/maxminddb-golang/v2/internal/decoder"
4+
5+
// Decoder provides methods for decoding MaxMind DB data values.
6+
// This interface is passed to UnmarshalMaxMindDB methods to allow
7+
// custom decoding logic that avoids reflection for performance-critical applications.
8+
//
9+
// Types implementing Unmarshaler will automatically use custom decoding logic
10+
// instead of reflection when used with Reader.Lookup, providing better performance
11+
// for performance-critical applications.
12+
//
13+
// Example:
14+
//
15+
// type City struct {
16+
// Names map[string]string `maxminddb:"names"`
17+
// GeoNameID uint `maxminddb:"geoname_id"`
18+
// }
19+
//
20+
// func (c *City) UnmarshalMaxMindDB(d *maxminddb.Decoder) error {
21+
// for key, err := range d.DecodeMap() {
22+
// if err != nil { return err }
23+
// switch string(key) {
24+
// case "names":
25+
// names := make(map[string]string)
26+
// for nameKey, nameErr := range d.DecodeMap() {
27+
// if nameErr != nil { return nameErr }
28+
// value, valueErr := d.DecodeString()
29+
// if valueErr != nil { return valueErr }
30+
// names[string(nameKey)] = value
31+
// }
32+
// c.Names = names
33+
// case "geoname_id":
34+
// geoID, err := d.DecodeUInt32()
35+
// if err != nil { return err }
36+
// c.GeoNameID = uint(geoID)
37+
// default:
38+
// if err := d.SkipValue(); err != nil { return err }
39+
// }
40+
// }
41+
// return nil
42+
// }
43+
type Decoder = decoder.Decoder
44+
45+
// Unmarshaler is implemented by types that can unmarshal MaxMind DB data.
46+
// This follows the same pattern as json.Unmarshaler and other Go standard library interfaces.
47+
type Unmarshaler = decoder.Unmarshaler

internal/decoder/decoder.go

Lines changed: 68 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -17,52 +17,6 @@ type Decoder struct {
1717
nextOffset uint
1818
}
1919

20-
func (d *Decoder) reset(offset uint) {
21-
d.offset = offset
22-
d.hasNextOffset = false
23-
d.nextOffset = 0
24-
}
25-
26-
func (d *Decoder) setNextOffset(offset uint) {
27-
if !d.hasNextOffset {
28-
d.hasNextOffset = true
29-
d.nextOffset = offset
30-
}
31-
}
32-
33-
func unexpectedTypeErr(expectedType, actualType Type) error {
34-
return fmt.Errorf("unexpected type %d, expected %d", actualType, expectedType)
35-
}
36-
37-
func (d *Decoder) decodeCtrlDataAndFollow(expectedType Type) (uint, uint, error) {
38-
dataOffset := d.offset
39-
for {
40-
var typeNum Type
41-
var size uint
42-
var err error
43-
typeNum, size, dataOffset, err = d.d.decodeCtrlData(dataOffset)
44-
if err != nil {
45-
return 0, 0, err
46-
}
47-
48-
if typeNum == TypePointer {
49-
var nextOffset uint
50-
dataOffset, nextOffset, err = d.d.decodePointer(size, dataOffset)
51-
if err != nil {
52-
return 0, 0, err
53-
}
54-
d.setNextOffset(nextOffset)
55-
continue
56-
}
57-
58-
if typeNum != expectedType {
59-
return 0, 0, unexpectedTypeErr(expectedType, typeNum)
60-
}
61-
62-
return size, dataOffset, nil
63-
}
64-
}
65-
6620
// DecodeBool decodes the value pointed by the decoder as a bool.
6721
//
6822
// Returns an error if the database is malformed or if the pointed value is not a bool.
@@ -85,15 +39,6 @@ func (d *Decoder) DecodeBool() (bool, error) {
8539
return value, nil
8640
}
8741

88-
func (d *Decoder) decodeBytes(typ Type) ([]byte, error) {
89-
size, offset, err := d.decodeCtrlDataAndFollow(typ)
90-
if err != nil {
91-
return nil, err
92-
}
93-
d.setNextOffset(offset + size)
94-
return d.d.buffer[offset : offset+size], nil
95-
}
96-
9742
// DecodeString decodes the value pointed by the decoder as a string.
9843
//
9944
// Returns an error if the database is malformed or if the pointed value is not a string.
@@ -382,3 +327,71 @@ func (d *Decoder) DecodeSlice() iter.Seq[error] {
382327
d.reset(currentOffset)
383328
}
384329
}
330+
331+
// SkipValue skips over the current value without decoding it.
332+
// This is useful in custom decoders when encountering unknown fields.
333+
// The decoder will be positioned after the skipped value.
334+
func (d *Decoder) SkipValue() error {
335+
// We can reuse the existing nextValueOffset logic by jumping to the next value
336+
nextOffset, err := d.d.nextValueOffset(d.offset, 1)
337+
if err != nil {
338+
return err
339+
}
340+
d.reset(nextOffset)
341+
return nil
342+
}
343+
344+
func (d *Decoder) reset(offset uint) {
345+
d.offset = offset
346+
d.hasNextOffset = false
347+
d.nextOffset = 0
348+
}
349+
350+
func (d *Decoder) setNextOffset(offset uint) {
351+
if !d.hasNextOffset {
352+
d.hasNextOffset = true
353+
d.nextOffset = offset
354+
}
355+
}
356+
357+
func unexpectedTypeErr(expectedType, actualType Type) error {
358+
return fmt.Errorf("unexpected type %d, expected %d", actualType, expectedType)
359+
}
360+
361+
func (d *Decoder) decodeCtrlDataAndFollow(expectedType Type) (uint, uint, error) {
362+
dataOffset := d.offset
363+
for {
364+
var typeNum Type
365+
var size uint
366+
var err error
367+
typeNum, size, dataOffset, err = d.d.decodeCtrlData(dataOffset)
368+
if err != nil {
369+
return 0, 0, err
370+
}
371+
372+
if typeNum == TypePointer {
373+
var nextOffset uint
374+
dataOffset, nextOffset, err = d.d.decodePointer(size, dataOffset)
375+
if err != nil {
376+
return 0, 0, err
377+
}
378+
d.setNextOffset(nextOffset)
379+
continue
380+
}
381+
382+
if typeNum != expectedType {
383+
return 0, 0, unexpectedTypeErr(expectedType, typeNum)
384+
}
385+
386+
return size, dataOffset, nil
387+
}
388+
}
389+
390+
func (d *Decoder) decodeBytes(typ Type) ([]byte, error) {
391+
size, offset, err := d.decodeCtrlDataAndFollow(typ)
392+
if err != nil {
393+
return nil, err
394+
}
395+
d.setNextOffset(offset + size)
396+
return d.d.buffer[offset : offset+size], nil
397+
}

0 commit comments

Comments
 (0)