Skip to content

Commit 9f2841c

Browse files
authored
Add option to prefer decoding as Map (#95)
* Test preferMap option * Add preferMap option to always decode as Map * Don't warn on string-keyed Maps when preferMap set * Document preferMap option * Polyfill Object.fromEntries() in tests for node 10
1 parent 195cfc5 commit 9f2841c

File tree

5 files changed

+94
-16
lines changed

5 files changed

+94
-16
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ options:
110110
- `sortKeys`, a boolean to force a determinate keys order
111111
- `compatibilityMode`, a boolean that enables "compatibility mode" which doesn't use str 8 format. Defaults to false.
112112
- `disableTimestampEncoding`, a boolean that when set disables the encoding of Dates into the [timestamp extension type](https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type). Defaults to false.
113+
- `preferMap`, a boolean that forces all maps to be decoded to `Map`s rather than plain objects. This ensures that `decode(encode(new Map())) instanceof Map` and that iteration order is preserved. Defaults to false.
113114

114115
-------------------------------------------------------
115116
<a name="encode"></a>

index.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ function msgpack (options) {
1818
compatibilityMode: false,
1919
// if true, skips encoding Dates using the msgpack
2020
// timestamp ext format (-1)
21-
disableTimestampEncoding: false
21+
disableTimestampEncoding: false,
22+
preferMap: false
2223
}
2324

2425
decodingTypes.set(DateCodec.type, DateCodec.decode)
@@ -72,7 +73,7 @@ function msgpack (options) {
7273

7374
return {
7475
encode: buildEncode(encodingTypes, options),
75-
decode: buildDecode(decodingTypes),
76+
decode: buildDecode(decodingTypes, options),
7677
register,
7778
registerEncoder,
7879
registerDecoder,

lib/decoder.js

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ function isValidDataSize (dataLength, bufLength, headerLength) {
3737
return bufLength >= headerLength + dataLength
3838
}
3939

40-
module.exports = function buildDecode (decodingTypes) {
40+
module.exports = function buildDecode (decodingTypes, options) {
4141
return decode
4242

4343
function decode (buf) {
@@ -72,13 +72,13 @@ module.exports = function buildDecode (decodingTypes) {
7272
if ((first & 0xf0) === 0x80) {
7373
const length = first & 0x0f
7474
const headerSize = offset - initialOffset
75-
// we have an array with less than 15 elements
76-
return decodeMap(buf, offset, length, headerSize)
75+
// we have a map with less than 15 elements
76+
return decodeMap(buf, offset, length, headerSize, options)
7777
}
7878
if ((first & 0xf0) === 0x90) {
7979
const length = first & 0x0f
8080
const headerSize = offset - initialOffset
81-
// we have a map with less than 15 elements
81+
// we have an array with less than 15 elements
8282
return decodeArray(buf, offset, length, headerSize)
8383
}
8484

@@ -138,12 +138,12 @@ module.exports = function buildDecode (decodingTypes) {
138138
length = buf.readUInt16BE(offset)
139139
offset += 2
140140
// console.log(offset - initialOffset)
141-
return decodeMap(buf, offset, length, 3)
141+
return decodeMap(buf, offset, length, 3, options)
142142

143143
case 0xdf:
144144
length = buf.readUInt32BE(offset)
145145
offset += 4
146-
return decodeMap(buf, offset, length, 5)
146+
return decodeMap(buf, offset, length, 5, options)
147147
}
148148
}
149149
if (first >= 0xe0) return [first - 0x100, 1] // 5 bits negative ints
@@ -166,16 +166,19 @@ module.exports = function buildDecode (decodingTypes) {
166166
return [result, headerLength + offset - initialOffset]
167167
}
168168

169-
function decodeMap (buf, offset, length, headerLength) {
169+
function decodeMap (buf, offset, length, headerLength, options) {
170170
const _temp = decodeArray(buf, offset, 2 * length, headerLength)
171171
if (!_temp) return null
172172
const [ result, consumedBytes ] = _temp
173173

174-
var isPlainObject = true
175-
for (let i = 0; i < 2 * length; i += 2) {
176-
if (typeof result[i] !== 'string') {
177-
isPlainObject = false
178-
break
174+
var isPlainObject = !options.preferMap
175+
176+
if (isPlainObject) {
177+
for (let i = 0; i < 2 * length; i += 2) {
178+
if (typeof result[i] !== 'string') {
179+
isPlainObject = false
180+
break
181+
}
179182
}
180183
}
181184

lib/encoder.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,10 @@ function encodeMap (map, options, encode) {
6262
const acc = [ getHeader(map.size, 0x80, 0xde) ]
6363
const keys = [ ...map.keys() ]
6464

65-
if (keys.every(item => typeof item === 'string')) {
66-
console.warn('Map with string only keys will be deserialized as an object!')
65+
if (!options.preferMap) {
66+
if (keys.every(item => typeof item === 'string')) {
67+
console.warn('Map with string only keys will be deserialized as an object!')
68+
}
6769
}
6870

6971
keys.forEach(key => {

test/prefer-map.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
const test = require('tape').test
2+
const msgpack = require('../')
3+
4+
const map = new Map()
5+
.set('a', 1)
6+
.set('1', 'hello')
7+
.set('world', 2)
8+
.set('0', 'again')
9+
.set('01', null)
10+
11+
test('round-trip string-keyed Maps', function (t) {
12+
const encoder = msgpack({preferMap: true})
13+
14+
for (const input of [new Map(), map]) {
15+
const result = encoder.decode(encoder.encode(input))
16+
t.assert(result instanceof Map)
17+
t.deepEqual(result, input)
18+
}
19+
20+
t.end()
21+
})
22+
23+
test('preserve iteration order of string-keyed Maps', function (t) {
24+
const encoder = msgpack({preferMap: true})
25+
const decoded = encoder.decode(encoder.encode(map))
26+
27+
t.deepEqual([...decoded.keys()], [...map.keys()])
28+
29+
t.end()
30+
})
31+
32+
test('user can still encode objects as ext maps', function (t) {
33+
const encoder = msgpack({preferMap: true})
34+
const tag = 0x42
35+
36+
// Polyfill Object.fromEntries for node 10
37+
const fromEntries = Object.fromEntries || (iterable => {
38+
const object = {}
39+
for (const [property, value] of iterable) {
40+
object[property] = value
41+
}
42+
return object
43+
})
44+
45+
encoder.register(
46+
tag,
47+
Object,
48+
obj => encoder.encode(new Map(Object.entries(obj))),
49+
data => fromEntries(encoder.decode(data))
50+
)
51+
52+
const inputs = [
53+
{},
54+
new Map(),
55+
{foo: 'bar'},
56+
new Map().set('foo', 'bar'),
57+
new Map().set(null, null),
58+
{0: 'baz'},
59+
['baz']
60+
]
61+
62+
for (const input of inputs) {
63+
const buf = encoder.encode(input)
64+
const result = encoder.decode(buf)
65+
66+
t.deepEqual(result, input)
67+
t.equal(Object.getPrototypeOf(result), Object.getPrototypeOf(input))
68+
}
69+
70+
t.end()
71+
})

0 commit comments

Comments
 (0)