Skip to content

Commit

Permalink
Add option to prefer decoding as Map (#95)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ninevra authored Mar 2, 2021
1 parent 195cfc5 commit 9f2841c
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 16 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ options:
- `sortKeys`, a boolean to force a determinate keys order
- `compatibilityMode`, a boolean that enables "compatibility mode" which doesn't use str 8 format. Defaults to false.
- `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.
- `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.

-------------------------------------------------------
<a name="encode"></a>
Expand Down
5 changes: 3 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ function msgpack (options) {
compatibilityMode: false,
// if true, skips encoding Dates using the msgpack
// timestamp ext format (-1)
disableTimestampEncoding: false
disableTimestampEncoding: false,
preferMap: false
}

decodingTypes.set(DateCodec.type, DateCodec.decode)
Expand Down Expand Up @@ -72,7 +73,7 @@ function msgpack (options) {

return {
encode: buildEncode(encodingTypes, options),
decode: buildDecode(decodingTypes),
decode: buildDecode(decodingTypes, options),
register,
registerEncoder,
registerDecoder,
Expand Down
27 changes: 15 additions & 12 deletions lib/decoder.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function isValidDataSize (dataLength, bufLength, headerLength) {
return bufLength >= headerLength + dataLength
}

module.exports = function buildDecode (decodingTypes) {
module.exports = function buildDecode (decodingTypes, options) {
return decode

function decode (buf) {
Expand Down Expand Up @@ -72,13 +72,13 @@ module.exports = function buildDecode (decodingTypes) {
if ((first & 0xf0) === 0x80) {
const length = first & 0x0f
const headerSize = offset - initialOffset
// we have an array with less than 15 elements
return decodeMap(buf, offset, length, headerSize)
// we have a map with less than 15 elements
return decodeMap(buf, offset, length, headerSize, options)
}
if ((first & 0xf0) === 0x90) {
const length = first & 0x0f
const headerSize = offset - initialOffset
// we have a map with less than 15 elements
// we have an array with less than 15 elements
return decodeArray(buf, offset, length, headerSize)
}

Expand Down Expand Up @@ -138,12 +138,12 @@ module.exports = function buildDecode (decodingTypes) {
length = buf.readUInt16BE(offset)
offset += 2
// console.log(offset - initialOffset)
return decodeMap(buf, offset, length, 3)
return decodeMap(buf, offset, length, 3, options)

case 0xdf:
length = buf.readUInt32BE(offset)
offset += 4
return decodeMap(buf, offset, length, 5)
return decodeMap(buf, offset, length, 5, options)
}
}
if (first >= 0xe0) return [first - 0x100, 1] // 5 bits negative ints
Expand All @@ -166,16 +166,19 @@ module.exports = function buildDecode (decodingTypes) {
return [result, headerLength + offset - initialOffset]
}

function decodeMap (buf, offset, length, headerLength) {
function decodeMap (buf, offset, length, headerLength, options) {
const _temp = decodeArray(buf, offset, 2 * length, headerLength)
if (!_temp) return null
const [ result, consumedBytes ] = _temp

var isPlainObject = true
for (let i = 0; i < 2 * length; i += 2) {
if (typeof result[i] !== 'string') {
isPlainObject = false
break
var isPlainObject = !options.preferMap

if (isPlainObject) {
for (let i = 0; i < 2 * length; i += 2) {
if (typeof result[i] !== 'string') {
isPlainObject = false
break
}
}
}

Expand Down
6 changes: 4 additions & 2 deletions lib/encoder.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ function encodeMap (map, options, encode) {
const acc = [ getHeader(map.size, 0x80, 0xde) ]
const keys = [ ...map.keys() ]

if (keys.every(item => typeof item === 'string')) {
console.warn('Map with string only keys will be deserialized as an object!')
if (!options.preferMap) {
if (keys.every(item => typeof item === 'string')) {
console.warn('Map with string only keys will be deserialized as an object!')
}
}

keys.forEach(key => {
Expand Down
71 changes: 71 additions & 0 deletions test/prefer-map.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
const test = require('tape').test
const msgpack = require('../')

const map = new Map()
.set('a', 1)
.set('1', 'hello')
.set('world', 2)
.set('0', 'again')
.set('01', null)

test('round-trip string-keyed Maps', function (t) {
const encoder = msgpack({preferMap: true})

for (const input of [new Map(), map]) {
const result = encoder.decode(encoder.encode(input))
t.assert(result instanceof Map)
t.deepEqual(result, input)
}

t.end()
})

test('preserve iteration order of string-keyed Maps', function (t) {
const encoder = msgpack({preferMap: true})
const decoded = encoder.decode(encoder.encode(map))

t.deepEqual([...decoded.keys()], [...map.keys()])

t.end()
})

test('user can still encode objects as ext maps', function (t) {
const encoder = msgpack({preferMap: true})
const tag = 0x42

// Polyfill Object.fromEntries for node 10
const fromEntries = Object.fromEntries || (iterable => {
const object = {}
for (const [property, value] of iterable) {
object[property] = value
}
return object
})

encoder.register(
tag,
Object,
obj => encoder.encode(new Map(Object.entries(obj))),
data => fromEntries(encoder.decode(data))
)

const inputs = [
{},
new Map(),
{foo: 'bar'},
new Map().set('foo', 'bar'),
new Map().set(null, null),
{0: 'baz'},
['baz']
]

for (const input of inputs) {
const buf = encoder.encode(input)
const result = encoder.decode(buf)

t.deepEqual(result, input)
t.equal(Object.getPrototypeOf(result), Object.getPrototypeOf(input))
}

t.end()
})

0 comments on commit 9f2841c

Please sign in to comment.