@atproto/lex-json 0.0.15 β 0.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/json-bytes-decoder.d.ts +24 -0
- package/dist/json-bytes-decoder.d.ts.map +1 -0
- package/dist/json-bytes-decoder.js +600 -0
- package/dist/json-bytes-decoder.js.map +1 -0
- package/dist/lex-json.d.ts +7 -3
- package/dist/lex-json.d.ts.map +1 -1
- package/dist/lex-json.js +20 -33
- package/dist/lex-json.js.map +1 -1
- package/package.json +1 -1
- package/src/json-bytes-decoder.bench.ts +252 -0
- package/src/json-bytes-decoder.test.ts +889 -0
- package/src/json-bytes-decoder.ts +672 -0
- package/src/lex-json.bench.ts +125 -0
- package/src/lex-json.test.ts +368 -0
- package/src/lex-json.ts +19 -33
- package/tsconfig.build.json +1 -1
- package/tsconfig.tests.json +1 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { bench, describe } from 'vitest'
|
|
2
|
+
import { JsonValue } from './json.js'
|
|
3
|
+
import {
|
|
4
|
+
LexParseOptions,
|
|
5
|
+
jsonToLex,
|
|
6
|
+
lexParse,
|
|
7
|
+
parseSpecialJsonObject,
|
|
8
|
+
} from './lex-json.js'
|
|
9
|
+
|
|
10
|
+
// This benchmark compares the performance of two implementations of
|
|
11
|
+
// lexParse:
|
|
12
|
+
// - One that uses a reviver function with JSON.parse to directly parse special
|
|
13
|
+
// objects and handle numbers (lexParse with reviver)
|
|
14
|
+
// - One that first parses JSON to a plain JS object and then converts it to
|
|
15
|
+
// LexValue using jsonToLex (lexParse with jsonToLex)
|
|
16
|
+
|
|
17
|
+
describe('small object', () => {
|
|
18
|
+
benchData({
|
|
19
|
+
$type: 'app.bsky.feed.post',
|
|
20
|
+
text: 'Hello world! π',
|
|
21
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('simple mixed structure', () => {
|
|
26
|
+
benchData({
|
|
27
|
+
cid: {
|
|
28
|
+
$link: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
|
|
29
|
+
},
|
|
30
|
+
bytes: {
|
|
31
|
+
$bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',
|
|
32
|
+
},
|
|
33
|
+
blob: {
|
|
34
|
+
$type: 'blob',
|
|
35
|
+
ref: {
|
|
36
|
+
$link: 'bafkreig77vqcdozl2wyk6z3cscaj5q5fggi53aoh64fewkdiri3cdauyn4',
|
|
37
|
+
},
|
|
38
|
+
mimeType: 'image/jpeg',
|
|
39
|
+
size: 10000,
|
|
40
|
+
},
|
|
41
|
+
nested: {
|
|
42
|
+
array: [
|
|
43
|
+
{
|
|
44
|
+
number: 42,
|
|
45
|
+
string: 'hello world',
|
|
46
|
+
bool: true,
|
|
47
|
+
null: null,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
string: 'Hello δΈη! πππ ΓoΓ±o',
|
|
51
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('large payload', () => {
|
|
57
|
+
benchData({
|
|
58
|
+
items: Array.from({ length: 25 }, (_, i) => ({
|
|
59
|
+
id: i,
|
|
60
|
+
name: `Item ${i}`,
|
|
61
|
+
longUnicode:
|
|
62
|
+
'Lorem ipsum dolor sit amet, consectetur adipiscing elit π€©.\n'.repeat(
|
|
63
|
+
2,
|
|
64
|
+
),
|
|
65
|
+
tags: ['tag1', 'tag2', 'tag3'],
|
|
66
|
+
bytes: {
|
|
67
|
+
$bytes: Buffer.from(`This is some byte data for item ${i}`).toString(
|
|
68
|
+
'base64',
|
|
69
|
+
),
|
|
70
|
+
},
|
|
71
|
+
cid: {
|
|
72
|
+
$link: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
|
|
73
|
+
},
|
|
74
|
+
metadata: {
|
|
75
|
+
created: '2024-01-01T00:00:00Z',
|
|
76
|
+
count: i * 10,
|
|
77
|
+
nested: {
|
|
78
|
+
flag: i % 2 === 0,
|
|
79
|
+
values: [i, i * 2, i * 3],
|
|
80
|
+
},
|
|
81
|
+
items: Array.from({ length: 5 }, (_, j) => ({
|
|
82
|
+
id: `${i}-${j}`,
|
|
83
|
+
value: `Value ${i}-${j}`,
|
|
84
|
+
})),
|
|
85
|
+
},
|
|
86
|
+
})),
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
function benchData(data: unknown, options?: LexParseOptions) {
|
|
91
|
+
const jsonString = JSON.stringify(data)
|
|
92
|
+
|
|
93
|
+
const withReviver: typeof lexParse = (input, options = { strict: true }) => {
|
|
94
|
+
return JSON.parse(input, (key: string, value: JsonValue) => {
|
|
95
|
+
switch (typeof value) {
|
|
96
|
+
case 'object':
|
|
97
|
+
if (value === null) return null
|
|
98
|
+
if (Array.isArray(value)) return value
|
|
99
|
+
return parseSpecialJsonObject(value, options) ?? value
|
|
100
|
+
case 'number':
|
|
101
|
+
if (Number.isSafeInteger(value)) return value
|
|
102
|
+
if (options && options.strict === false) return value
|
|
103
|
+
throw new TypeError(`Invalid non-integer number: ${value}`)
|
|
104
|
+
default:
|
|
105
|
+
return value
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const naiveParse: typeof lexParse = (input, options) => {
|
|
111
|
+
return jsonToLex(JSON.parse(input), options) as any
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
bench('current', () => {
|
|
115
|
+
lexParse(jsonString, options)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
bench('with-reviver', () => {
|
|
119
|
+
withReviver(jsonString, options)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
bench('naive', () => {
|
|
123
|
+
naiveParse(jsonString, options)
|
|
124
|
+
})
|
|
125
|
+
}
|
package/src/lex-json.test.ts
CHANGED
|
@@ -336,6 +336,24 @@ export const acceptableVectors: Array<{
|
|
|
336
336
|
$link: 'bafkreiccldh766hwcnuxnf2wh6jgzepf2nlu2lvcllt63eww5p6chi4ity',
|
|
337
337
|
},
|
|
338
338
|
},
|
|
339
|
+
{
|
|
340
|
+
note: 'blob with CBOR CID ref',
|
|
341
|
+
json: {
|
|
342
|
+
$type: 'blob',
|
|
343
|
+
ref: {
|
|
344
|
+
$link: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
|
|
345
|
+
},
|
|
346
|
+
mimeType: 'image/png',
|
|
347
|
+
size: 1,
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
note: 'object with empty $type',
|
|
352
|
+
json: {
|
|
353
|
+
$type: '',
|
|
354
|
+
foo: 'bar',
|
|
355
|
+
},
|
|
356
|
+
},
|
|
339
357
|
]
|
|
340
358
|
|
|
341
359
|
export const invalidVectors: Array<{
|
|
@@ -621,3 +639,353 @@ describe('lex > json > lex', () => {
|
|
|
621
639
|
}
|
|
622
640
|
})
|
|
623
641
|
})
|
|
642
|
+
|
|
643
|
+
describe('lexParseJsonBytes strict mode error parity with lexParse', () => {
|
|
644
|
+
describe('invalid JSON input throws SyntaxError containing "Unexpected token"', () => {
|
|
645
|
+
test('lexParse throws with Unexpected token', () => {
|
|
646
|
+
expect(() => lexParse('not valid json', { strict: true })).toThrow(
|
|
647
|
+
/Unexpected token/,
|
|
648
|
+
)
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
test('lexParseJsonBytes throws with Unexpected token', () => {
|
|
652
|
+
expect(() =>
|
|
653
|
+
lexParseJsonBytes(Buffer.from('not valid json'), { strict: true }),
|
|
654
|
+
).toThrow(/Unexpected token/)
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
test('lexParseJsonBytes non-strict also throws with Unexpected token for invalid JSON', () => {
|
|
658
|
+
expect(() =>
|
|
659
|
+
lexParseJsonBytes(Buffer.from('not valid json'), { strict: false }),
|
|
660
|
+
).toThrow(/Unexpected token/)
|
|
661
|
+
})
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
describe('float numbers: strict throws TypeError, non-strict accepts', () => {
|
|
665
|
+
const jsonStr = '{"value":1.5}'
|
|
666
|
+
|
|
667
|
+
test('lexParse strict throws TypeError with value in message', () => {
|
|
668
|
+
expect(() => lexParse(jsonStr, { strict: true })).toThrow(TypeError)
|
|
669
|
+
expect(() => lexParse(jsonStr, { strict: true })).toThrow(
|
|
670
|
+
'Invalid non-integer number: 1.5',
|
|
671
|
+
)
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
test('lexParseJsonBytes strict throws same TypeError', () => {
|
|
675
|
+
const bytes = Buffer.from(jsonStr)
|
|
676
|
+
expect(() => lexParseJsonBytes(bytes, { strict: true })).toThrow(
|
|
677
|
+
TypeError,
|
|
678
|
+
)
|
|
679
|
+
expect(() => lexParseJsonBytes(bytes, { strict: true })).toThrow(
|
|
680
|
+
'Invalid non-integer number: 1.5',
|
|
681
|
+
)
|
|
682
|
+
})
|
|
683
|
+
|
|
684
|
+
test('lexParse non-strict accepts float', () => {
|
|
685
|
+
expect(() => lexParse(jsonStr, { strict: false })).not.toThrow()
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
test('lexParseJsonBytes non-strict accepts float', () => {
|
|
689
|
+
expect(() =>
|
|
690
|
+
lexParseJsonBytes(Buffer.from(jsonStr), { strict: false }),
|
|
691
|
+
).not.toThrow()
|
|
692
|
+
})
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
describe('exponent notation: safe integers accepted, unsafe integers rejected', () => {
|
|
696
|
+
test('lexParse strict accepts 1e10 (safe integer)', () => {
|
|
697
|
+
expect(lexParse('1e10', { strict: true })).toBe(1e10)
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
test('lexParseJsonBytes strict accepts 1e10 (safe integer)', () => {
|
|
701
|
+
expect(lexParseJsonBytes(Buffer.from('1e10'), { strict: true })).toBe(
|
|
702
|
+
1e10,
|
|
703
|
+
)
|
|
704
|
+
})
|
|
705
|
+
|
|
706
|
+
test('lexParse strict rejects 1e20 (unsafe integer)', () => {
|
|
707
|
+
expect(() => lexParse('1e20', { strict: true })).toThrow(TypeError)
|
|
708
|
+
})
|
|
709
|
+
|
|
710
|
+
test('lexParseJsonBytes strict rejects 1e20 (unsafe integer)', () => {
|
|
711
|
+
expect(() =>
|
|
712
|
+
lexParseJsonBytes(Buffer.from('1e20'), { strict: true }),
|
|
713
|
+
).toThrow(TypeError)
|
|
714
|
+
})
|
|
715
|
+
})
|
|
716
|
+
|
|
717
|
+
describe('invalid blob: strict throws TypeError, non-strict returns plain object', () => {
|
|
718
|
+
const invalidBlobJson = '{"$type":"blob"}'
|
|
719
|
+
|
|
720
|
+
test('lexParse strict throws TypeError with "Invalid blob object"', () => {
|
|
721
|
+
expect(() => lexParse(invalidBlobJson, { strict: true })).toThrow(
|
|
722
|
+
TypeError,
|
|
723
|
+
)
|
|
724
|
+
expect(() => lexParse(invalidBlobJson, { strict: true })).toThrow(
|
|
725
|
+
'Invalid blob object',
|
|
726
|
+
)
|
|
727
|
+
})
|
|
728
|
+
|
|
729
|
+
test('lexParseJsonBytes strict throws same TypeError', () => {
|
|
730
|
+
const bytes = Buffer.from(invalidBlobJson)
|
|
731
|
+
expect(() => lexParseJsonBytes(bytes, { strict: true })).toThrow(
|
|
732
|
+
TypeError,
|
|
733
|
+
)
|
|
734
|
+
expect(() => lexParseJsonBytes(bytes, { strict: true })).toThrow(
|
|
735
|
+
'Invalid blob object',
|
|
736
|
+
)
|
|
737
|
+
})
|
|
738
|
+
|
|
739
|
+
test('lexParse non-strict returns plain object', () => {
|
|
740
|
+
expect(() => lexParse(invalidBlobJson, { strict: false })).not.toThrow()
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
test('lexParseJsonBytes non-strict returns plain object', () => {
|
|
744
|
+
expect(() =>
|
|
745
|
+
lexParseJsonBytes(Buffer.from(invalidBlobJson), { strict: false }),
|
|
746
|
+
).not.toThrow()
|
|
747
|
+
})
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
describe('blob with CBOR CID: strict throws TypeError, non-strict returns plain object', () => {
|
|
751
|
+
const blobWithCborCidJson = JSON.stringify({
|
|
752
|
+
$type: 'blob',
|
|
753
|
+
ref: {
|
|
754
|
+
$link: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
|
|
755
|
+
},
|
|
756
|
+
mimeType: 'image/png',
|
|
757
|
+
size: 1,
|
|
758
|
+
})
|
|
759
|
+
|
|
760
|
+
test('lexParse strict throws TypeError with "Invalid blob object"', () => {
|
|
761
|
+
expect(() => lexParse(blobWithCborCidJson, { strict: true })).toThrow(
|
|
762
|
+
TypeError,
|
|
763
|
+
)
|
|
764
|
+
expect(() => lexParse(blobWithCborCidJson, { strict: true })).toThrow(
|
|
765
|
+
'Invalid blob object',
|
|
766
|
+
)
|
|
767
|
+
})
|
|
768
|
+
|
|
769
|
+
test('lexParseJsonBytes strict throws same TypeError', () => {
|
|
770
|
+
const bytes = Buffer.from(blobWithCborCidJson)
|
|
771
|
+
expect(() => lexParseJsonBytes(bytes, { strict: true })).toThrow(
|
|
772
|
+
TypeError,
|
|
773
|
+
)
|
|
774
|
+
expect(() => lexParseJsonBytes(bytes, { strict: true })).toThrow(
|
|
775
|
+
'Invalid blob object',
|
|
776
|
+
)
|
|
777
|
+
})
|
|
778
|
+
|
|
779
|
+
test('lexParse non-strict returns plain object', () => {
|
|
780
|
+
expect(() =>
|
|
781
|
+
lexParse(blobWithCborCidJson, { strict: false }),
|
|
782
|
+
).not.toThrow()
|
|
783
|
+
})
|
|
784
|
+
|
|
785
|
+
test('lexParseJsonBytes non-strict returns plain object', () => {
|
|
786
|
+
expect(() =>
|
|
787
|
+
lexParseJsonBytes(Buffer.from(blobWithCborCidJson), { strict: false }),
|
|
788
|
+
).not.toThrow()
|
|
789
|
+
})
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
describe('invalid $link: strict throws TypeError, non-strict returns plain object', () => {
|
|
793
|
+
const invalidLinkJson = '{"$link":"."}'
|
|
794
|
+
|
|
795
|
+
test('lexParse strict throws TypeError with "Invalid $link object"', () => {
|
|
796
|
+
expect(() => lexParse(invalidLinkJson, { strict: true })).toThrow(
|
|
797
|
+
TypeError,
|
|
798
|
+
)
|
|
799
|
+
expect(() => lexParse(invalidLinkJson, { strict: true })).toThrow(
|
|
800
|
+
'Invalid $link object',
|
|
801
|
+
)
|
|
802
|
+
})
|
|
803
|
+
|
|
804
|
+
test('lexParseJsonBytes strict throws same TypeError', () => {
|
|
805
|
+
const bytes = Buffer.from(invalidLinkJson)
|
|
806
|
+
expect(() => lexParseJsonBytes(bytes, { strict: true })).toThrow(
|
|
807
|
+
TypeError,
|
|
808
|
+
)
|
|
809
|
+
expect(() => lexParseJsonBytes(bytes, { strict: true })).toThrow(
|
|
810
|
+
'Invalid $link object',
|
|
811
|
+
)
|
|
812
|
+
})
|
|
813
|
+
|
|
814
|
+
test('lexParse non-strict returns plain object', () => {
|
|
815
|
+
expect(() => lexParse(invalidLinkJson, { strict: false })).not.toThrow()
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
test('lexParseJsonBytes non-strict returns plain object', () => {
|
|
819
|
+
expect(() =>
|
|
820
|
+
lexParseJsonBytes(Buffer.from(invalidLinkJson), { strict: false }),
|
|
821
|
+
).not.toThrow()
|
|
822
|
+
})
|
|
823
|
+
})
|
|
824
|
+
|
|
825
|
+
describe('$link with extra fields: strict throws TypeError, non-strict returns plain object', () => {
|
|
826
|
+
const linkWithExtraJson =
|
|
827
|
+
'{"$link":"bafkreiccldh766hwcnuxnf2wh6jgzepf2nlu2lvcllt63eww5p6chi4ity","extra":"field"}'
|
|
828
|
+
|
|
829
|
+
test('lexParse strict throws TypeError with "Invalid $link object"', () => {
|
|
830
|
+
expect(() => lexParse(linkWithExtraJson, { strict: true })).toThrow(
|
|
831
|
+
TypeError,
|
|
832
|
+
)
|
|
833
|
+
expect(() => lexParse(linkWithExtraJson, { strict: true })).toThrow(
|
|
834
|
+
'Invalid $link object',
|
|
835
|
+
)
|
|
836
|
+
})
|
|
837
|
+
|
|
838
|
+
test('lexParseJsonBytes strict throws same TypeError', () => {
|
|
839
|
+
const bytes = Buffer.from(linkWithExtraJson)
|
|
840
|
+
expect(() => lexParseJsonBytes(bytes, { strict: true })).toThrow(
|
|
841
|
+
TypeError,
|
|
842
|
+
)
|
|
843
|
+
expect(() => lexParseJsonBytes(bytes, { strict: true })).toThrow(
|
|
844
|
+
'Invalid $link object',
|
|
845
|
+
)
|
|
846
|
+
})
|
|
847
|
+
|
|
848
|
+
test('lexParse non-strict returns plain object', () => {
|
|
849
|
+
expect(() => lexParse(linkWithExtraJson, { strict: false })).not.toThrow()
|
|
850
|
+
})
|
|
851
|
+
|
|
852
|
+
test('lexParseJsonBytes non-strict returns plain object', () => {
|
|
853
|
+
expect(() =>
|
|
854
|
+
lexParseJsonBytes(Buffer.from(linkWithExtraJson), { strict: false }),
|
|
855
|
+
).not.toThrow()
|
|
856
|
+
})
|
|
857
|
+
})
|
|
858
|
+
|
|
859
|
+
describe('invalid $bytes: strict throws TypeError, non-strict returns plain object', () => {
|
|
860
|
+
const invalidBytesJson = '{"$bytes":"π»"}'
|
|
861
|
+
|
|
862
|
+
test('lexParse strict throws TypeError with "Invalid $bytes object"', () => {
|
|
863
|
+
expect(() => lexParse(invalidBytesJson, { strict: true })).toThrow(
|
|
864
|
+
TypeError,
|
|
865
|
+
)
|
|
866
|
+
expect(() => lexParse(invalidBytesJson, { strict: true })).toThrow(
|
|
867
|
+
'Invalid $bytes object',
|
|
868
|
+
)
|
|
869
|
+
})
|
|
870
|
+
|
|
871
|
+
test('lexParseJsonBytes strict throws same TypeError', () => {
|
|
872
|
+
const bytes = Buffer.from(invalidBytesJson)
|
|
873
|
+
expect(() => lexParseJsonBytes(bytes, { strict: true })).toThrow(
|
|
874
|
+
TypeError,
|
|
875
|
+
)
|
|
876
|
+
expect(() => lexParseJsonBytes(bytes, { strict: true })).toThrow(
|
|
877
|
+
'Invalid $bytes object',
|
|
878
|
+
)
|
|
879
|
+
})
|
|
880
|
+
|
|
881
|
+
test('lexParse non-strict returns plain object', () => {
|
|
882
|
+
expect(() => lexParse(invalidBytesJson, { strict: false })).not.toThrow()
|
|
883
|
+
})
|
|
884
|
+
|
|
885
|
+
test('lexParseJsonBytes non-strict returns plain object', () => {
|
|
886
|
+
expect(() =>
|
|
887
|
+
lexParseJsonBytes(Buffer.from(invalidBytesJson), { strict: false }),
|
|
888
|
+
).not.toThrow()
|
|
889
|
+
})
|
|
890
|
+
})
|
|
891
|
+
|
|
892
|
+
describe('$bytes with extra fields: strict throws TypeError, non-strict returns plain object', () => {
|
|
893
|
+
const bytesWithExtraJson =
|
|
894
|
+
'{"$bytes":"nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0","extra":"field"}'
|
|
895
|
+
|
|
896
|
+
test('lexParse strict throws TypeError with "Invalid $bytes object"', () => {
|
|
897
|
+
expect(() => lexParse(bytesWithExtraJson, { strict: true })).toThrow(
|
|
898
|
+
TypeError,
|
|
899
|
+
)
|
|
900
|
+
expect(() => lexParse(bytesWithExtraJson, { strict: true })).toThrow(
|
|
901
|
+
'Invalid $bytes object',
|
|
902
|
+
)
|
|
903
|
+
})
|
|
904
|
+
|
|
905
|
+
test('lexParseJsonBytes strict throws same TypeError', () => {
|
|
906
|
+
const bytes = Buffer.from(bytesWithExtraJson)
|
|
907
|
+
expect(() => lexParseJsonBytes(bytes, { strict: true })).toThrow(
|
|
908
|
+
TypeError,
|
|
909
|
+
)
|
|
910
|
+
expect(() => lexParseJsonBytes(bytes, { strict: true })).toThrow(
|
|
911
|
+
'Invalid $bytes object',
|
|
912
|
+
)
|
|
913
|
+
})
|
|
914
|
+
|
|
915
|
+
test('lexParse non-strict returns plain object', () => {
|
|
916
|
+
expect(() =>
|
|
917
|
+
lexParse(bytesWithExtraJson, { strict: false }),
|
|
918
|
+
).not.toThrow()
|
|
919
|
+
})
|
|
920
|
+
|
|
921
|
+
test('lexParseJsonBytes non-strict returns plain object', () => {
|
|
922
|
+
expect(() =>
|
|
923
|
+
lexParseJsonBytes(Buffer.from(bytesWithExtraJson), { strict: false }),
|
|
924
|
+
).not.toThrow()
|
|
925
|
+
})
|
|
926
|
+
})
|
|
927
|
+
|
|
928
|
+
describe('empty $type: strict throws TypeError, non-strict returns plain object', () => {
|
|
929
|
+
const emptyTypeJson = '{"$type":"","foo":"bar"}'
|
|
930
|
+
|
|
931
|
+
test('lexParse strict throws TypeError with "Empty $type property"', () => {
|
|
932
|
+
expect(() => lexParse(emptyTypeJson, { strict: true })).toThrow(TypeError)
|
|
933
|
+
expect(() => lexParse(emptyTypeJson, { strict: true })).toThrow(
|
|
934
|
+
'Empty $type property',
|
|
935
|
+
)
|
|
936
|
+
})
|
|
937
|
+
|
|
938
|
+
test('lexParseJsonBytes strict throws same TypeError', () => {
|
|
939
|
+
const bytes = Buffer.from(emptyTypeJson)
|
|
940
|
+
expect(() => lexParseJsonBytes(bytes, { strict: true })).toThrow(
|
|
941
|
+
TypeError,
|
|
942
|
+
)
|
|
943
|
+
expect(() => lexParseJsonBytes(bytes, { strict: true })).toThrow(
|
|
944
|
+
'Empty $type property',
|
|
945
|
+
)
|
|
946
|
+
})
|
|
947
|
+
|
|
948
|
+
test('lexParse non-strict returns plain object', () => {
|
|
949
|
+
expect(() => lexParse(emptyTypeJson, { strict: false })).not.toThrow()
|
|
950
|
+
})
|
|
951
|
+
|
|
952
|
+
test('lexParseJsonBytes non-strict returns plain object', () => {
|
|
953
|
+
expect(() =>
|
|
954
|
+
lexParseJsonBytes(Buffer.from(emptyTypeJson), { strict: false }),
|
|
955
|
+
).not.toThrow()
|
|
956
|
+
})
|
|
957
|
+
})
|
|
958
|
+
|
|
959
|
+
describe('non-string $type: strict throws TypeError, non-strict returns plain object', () => {
|
|
960
|
+
const nonStringTypeJson = '{"$type":123,"foo":"bar"}'
|
|
961
|
+
|
|
962
|
+
test('lexParse strict throws TypeError with type name in message', () => {
|
|
963
|
+
expect(() => lexParse(nonStringTypeJson, { strict: true })).toThrow(
|
|
964
|
+
TypeError,
|
|
965
|
+
)
|
|
966
|
+
expect(() => lexParse(nonStringTypeJson, { strict: true })).toThrow(
|
|
967
|
+
'Invalid $type property (number)',
|
|
968
|
+
)
|
|
969
|
+
})
|
|
970
|
+
|
|
971
|
+
test('lexParseJsonBytes strict throws same TypeError', () => {
|
|
972
|
+
const bytes = Buffer.from(nonStringTypeJson)
|
|
973
|
+
expect(() => lexParseJsonBytes(bytes, { strict: true })).toThrow(
|
|
974
|
+
TypeError,
|
|
975
|
+
)
|
|
976
|
+
expect(() => lexParseJsonBytes(bytes, { strict: true })).toThrow(
|
|
977
|
+
'Invalid $type property (number)',
|
|
978
|
+
)
|
|
979
|
+
})
|
|
980
|
+
|
|
981
|
+
test('lexParse non-strict returns plain object', () => {
|
|
982
|
+
expect(() => lexParse(nonStringTypeJson, { strict: false })).not.toThrow()
|
|
983
|
+
})
|
|
984
|
+
|
|
985
|
+
test('lexParseJsonBytes non-strict returns plain object', () => {
|
|
986
|
+
expect(() =>
|
|
987
|
+
lexParseJsonBytes(Buffer.from(nonStringTypeJson), { strict: false }),
|
|
988
|
+
).not.toThrow()
|
|
989
|
+
})
|
|
990
|
+
})
|
|
991
|
+
})
|
package/src/lex-json.ts
CHANGED
|
@@ -100,41 +100,26 @@ export function lexParse<T extends LexValue = LexValue>(
|
|
|
100
100
|
input: string,
|
|
101
101
|
options: LexParseOptions = { strict: false },
|
|
102
102
|
): T {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (value === null) return null
|
|
107
|
-
if (Array.isArray(value)) return value
|
|
108
|
-
return parseSpecialJsonObject(value, options) ?? value
|
|
109
|
-
case 'number':
|
|
110
|
-
if (Number.isSafeInteger(value)) return value
|
|
111
|
-
if (options.strict) {
|
|
112
|
-
throw new TypeError(`Invalid non-integer number: ${value}`)
|
|
113
|
-
}
|
|
114
|
-
// fallthrough
|
|
115
|
-
default:
|
|
116
|
-
return value
|
|
117
|
-
}
|
|
118
|
-
})
|
|
103
|
+
// @NOTE see ./lex-json.bench.ts for performance comparison of implementation
|
|
104
|
+
// that uses a reviver function in JSON.parse vs. the current implementation.
|
|
105
|
+
return jsonToLex(JSON.parse(input), options) as T
|
|
119
106
|
}
|
|
120
107
|
|
|
121
108
|
/**
|
|
122
|
-
* Parses a
|
|
109
|
+
* Parses a JSON string from a byte array into Lex values.
|
|
123
110
|
*/
|
|
124
111
|
export function lexParseJsonBytes(
|
|
125
|
-
|
|
112
|
+
bytes: Uint8Array,
|
|
126
113
|
options?: LexParseOptions,
|
|
127
114
|
): LexValue {
|
|
128
|
-
// @
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
const jsonString = utf8FromBytes(jsonBytes)
|
|
137
|
-
return lexParse(jsonString, options)
|
|
115
|
+
// @NOTE see ./json-bytes-decoder.bench.ts for performance comparison of
|
|
116
|
+
// implementation that uses a decoder class that operates directly on bytes
|
|
117
|
+
// vs. the current implementation that first decodes bytes to string and then
|
|
118
|
+
// parses JSON. For more common cases, it seems that the trivial
|
|
119
|
+
// implementation works better than the decoder based solution, while having a
|
|
120
|
+
// small overhead for slower cases (~2% difference). Because of this, we keep
|
|
121
|
+
// the trivial implementation:
|
|
122
|
+
return lexParse(utf8FromBytes(bytes), options)
|
|
138
123
|
}
|
|
139
124
|
|
|
140
125
|
/**
|
|
@@ -181,10 +166,8 @@ export function jsonToLex(
|
|
|
181
166
|
}
|
|
182
167
|
case 'number':
|
|
183
168
|
if (Number.isSafeInteger(value)) return value
|
|
184
|
-
if (options.strict)
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
// fallthrough
|
|
169
|
+
if (options.strict === false) return value
|
|
170
|
+
throw new TypeError(`Invalid non-integer number: ${value}`)
|
|
188
171
|
case 'boolean':
|
|
189
172
|
case 'string':
|
|
190
173
|
return value
|
|
@@ -327,7 +310,10 @@ function encodeLexMap(input: LexMap): JsonObject {
|
|
|
327
310
|
return copy ?? (input as JsonObject)
|
|
328
311
|
}
|
|
329
312
|
|
|
330
|
-
|
|
313
|
+
/**
|
|
314
|
+
* @internal
|
|
315
|
+
*/
|
|
316
|
+
export function parseSpecialJsonObject(
|
|
331
317
|
input: LexMap,
|
|
332
318
|
options: LexParseOptions,
|
|
333
319
|
): Cid | Uint8Array | BlobRef | undefined {
|
package/tsconfig.build.json
CHANGED
package/tsconfig.tests.json
CHANGED