Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to prefer decoding as Map #95

Merged
merged 5 commits into from
Mar 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
})