Skip to content

Commit bf9c15a

Browse files
authored
Merge pull request #266 from PejmanNik/adding-support-for-nonstandard-map-key-in-the-decoder
adding support for nonstandard map key in the decoder
2 parents 588354f + 59057ee commit bf9c15a

File tree

3 files changed

+44
-17
lines changed

3 files changed

+44
-17
lines changed

README.md

+14-11
Original file line numberDiff line numberDiff line change
@@ -145,17 +145,20 @@ NodeJS `Buffer` is also acceptable because it is a subclass of `Uint8Array`.
145145

146146
#### `DecoderOptions`
147147

148-
| Name | Type | Default |
149-
| -------------- | -------------- | ----------------------------- |
150-
| extensionCodec | ExtensionCodec | `ExtensionCodec.defaultCodec` |
151-
| context | user-defined | - |
152-
| useBigInt64 | boolean | false |
153-
| rawStrings | boolean | false |
154-
| maxStrLength | number | `4_294_967_295` (UINT32_MAX) |
155-
| maxBinLength | number | `4_294_967_295` (UINT32_MAX) |
156-
| maxArrayLength | number | `4_294_967_295` (UINT32_MAX) |
157-
| maxMapLength | number | `4_294_967_295` (UINT32_MAX) |
158-
| maxExtLength | number | `4_294_967_295` (UINT32_MAX) |
148+
| Name | Type | Default |
149+
| --------------- | ------------------- | ---------------------------------------------- |
150+
| extensionCodec | ExtensionCodec | `ExtensionCodec.defaultCodec` |
151+
| context | user-defined | - |
152+
| useBigInt64 | boolean | false |
153+
| rawStrings | boolean | false |
154+
| maxStrLength | number | `4_294_967_295` (UINT32_MAX) |
155+
| maxBinLength | number | `4_294_967_295` (UINT32_MAX) |
156+
| maxArrayLength | number | `4_294_967_295` (UINT32_MAX) |
157+
| maxMapLength | number | `4_294_967_295` (UINT32_MAX) |
158+
| maxExtLength | number | `4_294_967_295` (UINT32_MAX) |
159+
| mapKeyConverter | MapKeyConverterType | throw exception if key is not string or number |
160+
161+
`MapKeyConverterType` is defined as `(key: unknown) => string | number`.
159162

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

src/Decoder.ts

+15-6
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,13 @@ export type DecoderOptions<ContextType = undefined> = Readonly<
6868
* `null` is a special value to disable the use of the key decoder at all.
6969
*/
7070
keyDecoder: KeyDecoder | null;
71+
72+
/**
73+
* A function to convert decoded map key to a valid JS key type.
74+
*
75+
* Defaults to a function that throws an error if the key is not a string or a number.
76+
*/
77+
mapKeyConverter: (key: unknown) => MapKeyType;
7178
}>
7279
> &
7380
ContextOf<ContextType>;
@@ -78,8 +85,11 @@ const STATE_MAP_VALUE = "map_value";
7885

7986
type MapKeyType = string | number;
8087

81-
const isValidMapKeyType = (key: unknown): key is MapKeyType => {
82-
return typeof key === "string" || typeof key === "number";
88+
const mapKeyConverter = (key: unknown): MapKeyType => {
89+
if (typeof key === "string" || typeof key === "number") {
90+
return key;
91+
}
92+
throw new DecodeError("The type of key must be string or number but " + typeof key);
8393
};
8494

8595
type StackMapState = {
@@ -213,6 +223,7 @@ export class Decoder<ContextType = undefined> {
213223
private readonly maxMapLength: number;
214224
private readonly maxExtLength: number;
215225
private readonly keyDecoder: KeyDecoder | null;
226+
private readonly mapKeyConverter: (key: unknown) => MapKeyType;
216227

217228
private totalPos = 0;
218229
private pos = 0;
@@ -236,6 +247,7 @@ export class Decoder<ContextType = undefined> {
236247
this.maxMapLength = options?.maxMapLength ?? UINT32_MAX;
237248
this.maxExtLength = options?.maxExtLength ?? UINT32_MAX;
238249
this.keyDecoder = options?.keyDecoder !== undefined ? options.keyDecoder : sharedCachedKeyDecoder;
250+
this.mapKeyConverter = options?.mapKeyConverter ?? mapKeyConverter;
239251
}
240252

241253
private clone(): Decoder<ContextType> {
@@ -631,14 +643,11 @@ export class Decoder<ContextType = undefined> {
631643
continue DECODE;
632644
}
633645
} else if (state.type === STATE_MAP_KEY) {
634-
if (!isValidMapKeyType(object)) {
635-
throw new DecodeError("The type of key must be string or number but " + typeof object);
636-
}
637646
if (object === "__proto__") {
638647
throw new DecodeError("The key __proto__ is not allowed");
639648
}
640649

641-
state.key = object;
650+
state.key = this.mapKeyConverter(object);
642651
state.type = STATE_MAP_VALUE;
643652
continue DECODE;
644653
} else {

test/decodeAsync.test.ts

+15
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,21 @@ describe("decodeAsync", () => {
3636
assert.deepStrictEqual(object, { "foo": "bar" });
3737
});
3838

39+
it("decodes fixmap {'[1, 2]': 'baz'} with custom map key converter", async () => {
40+
const createStream = async function* () {
41+
yield [0x81]; // fixmap size=1
42+
yield encode([1, 2]);
43+
yield encode("baz");
44+
};
45+
46+
const object = await decodeAsync(createStream(), {
47+
mapKeyConverter: (key) => JSON.stringify(key),
48+
});
49+
50+
const key = JSON.stringify([1, 2]);
51+
assert.deepStrictEqual(object, { [key]: "baz" });
52+
});
53+
3954
it("decodes multi-byte integer byte-by-byte", async () => {
4055
const createStream = async function* () {
4156
yield [0xcd]; // uint 16

0 commit comments

Comments
 (0)