Skip to content

Commit 3df1083

Browse files
committed
feat: Added ability to enforce numeric map keys are encoded as numbers rather than strings
1 parent 0d6e172 commit 3df1083

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
@@ -121,6 +121,7 @@ console.log(buffer);
121121
| sortKeys | boolean | false |
122122
| forceFloat32 | boolean | false |
123123
| forceIntegerToFloat | boolean | false |
124+
| forceNumericMapKeys | boolean | false |
124125
| ignoreUndefined | boolean | false |
125126

126127
To skip UTF-8 decoding of strings, `useRawBinaryStrings` can be set to `true`. In this case, strings are decoded into `Uint8Array`.

src/Encoder.ts

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

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

8694
private pos: number;
8795
private view: DataView;
@@ -98,6 +106,7 @@ export class Encoder<ContextType = undefined> {
98106
this.forceFloat32 = options?.forceFloat32 ?? false;
99107
this.ignoreUndefined = options?.ignoreUndefined ?? false;
100108
this.forceIntegerToFloat = options?.forceIntegerToFloat ?? false;
109+
this.forceNumericMapKeys = options?.forceNumericMapKeys ?? false;
101110

102111
this.pos = 0;
103112
this.view = new DataView(new ArrayBuffer(this.initialBufferSize));
@@ -383,13 +392,23 @@ export class Encoder<ContextType = undefined> {
383392
return count;
384393
}
385394

395+
private isNumber(value: string | number) {
396+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
397+
return !isNaN(value as any) && !isNaN(parseFloat(value as any));
398+
}
399+
386400
private encodeMap(object: Record<string, unknown>, depth: number) {
387-
const keys = Object.keys(object);
401+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
402+
const keys = Object.keys(object).map((key) => (this.forceNumericMapKeys && this.isNumber(key) ? Number(key) : key));
388403
if (this.sortKeys) {
389-
keys.sort();
404+
if (keys.filter((k) => typeof k === "number").length > 0) {
405+
keys.sort().sort((a, b) => +a - +b);
406+
} else {
407+
keys.sort();
408+
}
390409
}
391410

392-
const size = this.ignoreUndefined ? this.countWithoutUndefined(object, keys) : keys.length;
411+
const size = this.ignoreUndefined ? this.countWithoutUndefined(object, Object.keys(object)) : keys.length;
393412

394413
if (size < 16) {
395414
// fixmap
@@ -410,7 +429,11 @@ export class Encoder<ContextType = undefined> {
410429
const value = object[key];
411430

412431
if (!(this.ignoreUndefined && value === undefined)) {
413-
this.encodeString(key);
432+
if (typeof key === "string") {
433+
this.encodeString(key);
434+
} else {
435+
this.encodeNumber(key);
436+
}
414437
this.doEncode(value, depth + 1);
415438
}
416439
}
+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)