Skip to content

Commit 61174e6

Browse files
committed
feat: Added ability to enforce numeric map keys are encoded as numbers rather than strings
1 parent 1fc7622 commit 61174e6

File tree

3 files changed

+130
-4
lines changed

3 files changed

+130
-4
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ initialBufferSize | number | `2048`
119119
sortKeys | boolean | false
120120
forceFloat32 | boolean | false
121121
forceIntegerToFloat | boolean | false
122+
forceNumericMapKeys | boolean | false
122123
ignoreUndefined | boolean | false
123124

124125
### `decode(buffer: ArrayLike<number> | BufferSource, options?: DecoderOptions): unknown`

src/Encoder.ts

+27-4
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ export type EncoderOptions<ContextType = undefined> = Partial<
5252
*/
5353
forceFloat32: boolean;
5454

55+
/**
56+
* If `true`, numeric map keys will be encoded as ints/floats (as appropriate) rather than strings (the default).
57+
*
58+
* Defaults to `false`.
59+
*/
60+
forceNumericMapKeys: boolean;
61+
5562
/**
5663
* If `true`, an object property with `undefined` value are ignored.
5764
* e.g. `{ foo: undefined }` will be encoded as `{}`, as `JSON.stringify()` does.
@@ -81,6 +88,7 @@ export class Encoder<ContextType = undefined> {
8188
private readonly forceFloat32: boolean;
8289
private readonly ignoreUndefined: boolean;
8390
private readonly forceIntegerToFloat: boolean;
91+
private readonly forceNumericMapKeys: boolean;
8492

8593
private pos: number;
8694
private view: DataView;
@@ -97,6 +105,7 @@ export class Encoder<ContextType = undefined> {
97105
this.forceFloat32 = options?.forceFloat32 ?? false;
98106
this.ignoreUndefined = options?.ignoreUndefined ?? false;
99107
this.forceIntegerToFloat = options?.forceIntegerToFloat ?? false;
108+
this.forceNumericMapKeys = options?.forceNumericMapKeys ?? false;
100109

101110
this.pos = 0;
102111
this.view = new DataView(new ArrayBuffer(this.initialBufferSize));
@@ -362,13 +371,23 @@ export class Encoder<ContextType = undefined> {
362371
return count;
363372
}
364373

374+
private isNumber(value: string | number) {
375+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
376+
return !isNaN(value as any) && !isNaN(parseFloat(value as any));
377+
}
378+
365379
private encodeMap(object: Record<string, unknown>, depth: number) {
366-
const keys = Object.keys(object);
380+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
381+
const keys = Object.keys(object).map((key) => (this.forceNumericMapKeys && this.isNumber(key) ? Number(key) : key));
367382
if (this.sortKeys) {
368-
keys.sort();
383+
if (keys.filter((k) => typeof k === "number").length > 0) {
384+
keys.sort().sort((a, b) => +a - +b);
385+
} else {
386+
keys.sort();
387+
}
369388
}
370389

371-
const size = this.ignoreUndefined ? this.countWithoutUndefined(object, keys) : keys.length;
390+
const size = this.ignoreUndefined ? this.countWithoutUndefined(object, Object.keys(object)) : keys.length;
372391

373392
if (size < 16) {
374393
// fixmap
@@ -389,7 +408,11 @@ export class Encoder<ContextType = undefined> {
389408
const value = object[key];
390409

391410
if (!(this.ignoreUndefined && value === undefined)) {
392-
this.encodeString(key);
411+
if (typeof key === "string") {
412+
this.encodeString(key);
413+
} else {
414+
this.encodeNumber(key);
415+
}
393416
this.doEncode(value, depth + 1);
394417
}
395418
}
+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/* eslint-disable @typescript-eslint/naming-convention */
2+
import { deepStrictEqual, equal } from "assert";
3+
import { encode } from "src/encode";
4+
import { decode } from "src/decode";
5+
6+
const exampleMap = {
7+
1: "1",
8+
"102": "102",
9+
a: "a",
10+
20: "20",
11+
} as Record<number | string, string>;
12+
13+
function getExpectedMsgPack(key1: Uint8Array, value1: Uint8Array, key2: Uint8Array, value2: Uint8Array, key3: Uint8Array, value3: Uint8Array, key4: Uint8Array, value4: Uint8Array,) {
14+
return new Uint8Array([
15+
// fixmap of length 4: https://github.com/msgpack/msgpack/blob/master/spec.md#map-format-family
16+
0b10000100,
17+
...key1,
18+
...value1,
19+
...key2,
20+
...value2,
21+
...key3,
22+
...value3,
23+
...key4,
24+
...value4
25+
])
26+
}
27+
28+
describe("map-with-number-keys", () => {
29+
it(`encodes numeric keys as numbers when forced`, () => {
30+
const expected = getExpectedMsgPack(
31+
// This is the order Object.keys returns
32+
encode(1),
33+
encode("1"),
34+
encode(20),
35+
encode("20"),
36+
encode(102),
37+
encode("102"),
38+
encode("a"),
39+
encode("a"),
40+
)
41+
42+
const encoded = encode(exampleMap, {forceNumericMapKeys: true});
43+
44+
equal(Buffer.from(encoded).toString("hex"), Buffer.from(expected).toString("hex"));
45+
deepStrictEqual(decode(encoded), exampleMap)
46+
});
47+
48+
it(`encodes numeric keys as strings when not forced`, () => {
49+
const expected = getExpectedMsgPack(
50+
// This is the order Object.keys returns
51+
encode("1"),
52+
encode("1"),
53+
encode("20"),
54+
encode("20"),
55+
encode("102"),
56+
encode("102"),
57+
encode("a"),
58+
encode("a"),
59+
)
60+
61+
const encoded = encode(exampleMap);
62+
63+
equal(Buffer.from(encoded).toString("hex"), Buffer.from(expected).toString("hex"));
64+
deepStrictEqual(decode(encoded), exampleMap)
65+
});
66+
67+
it(`encodes numeric keys as strings with sorting`, () => {
68+
const expected = getExpectedMsgPack(
69+
encode("1"),
70+
encode("1"),
71+
encode("102"),
72+
encode("102"),
73+
encode("20"),
74+
encode("20"),
75+
encode("a"),
76+
encode("a"),
77+
)
78+
79+
const encoded = encode(exampleMap, {sortKeys: true});
80+
81+
equal(Buffer.from(encoded).toString("hex"), Buffer.from(expected).toString("hex"));
82+
deepStrictEqual(decode(encoded), exampleMap)
83+
});
84+
85+
it(`encodes numeric keys as numbers with sorting`, () => {
86+
const expected = getExpectedMsgPack(
87+
encode(1),
88+
encode("1"),
89+
encode(20),
90+
encode("20"),
91+
encode(102),
92+
encode("102"),
93+
encode("a"),
94+
encode("a"),
95+
)
96+
97+
const encoded = encode(exampleMap, {sortKeys: true, forceNumericMapKeys: true});
98+
99+
equal(Buffer.from(encoded).toString("hex"), Buffer.from(expected).toString("hex"));
100+
deepStrictEqual(decode(encoded), exampleMap)
101+
});
102+
});

0 commit comments

Comments
 (0)