@atproto/lex-data 0.0.0

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.
Files changed (82) hide show
  1. package/dist/blob.d.ts +16 -0
  2. package/dist/blob.d.ts.map +1 -0
  3. package/dist/blob.js +73 -0
  4. package/dist/blob.js.map +1 -0
  5. package/dist/cid.d.ts +12 -0
  6. package/dist/cid.d.ts.map +1 -0
  7. package/dist/cid.js +47 -0
  8. package/dist/cid.js.map +1 -0
  9. package/dist/index.d.ts +9 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +12 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/language.d.ts +18 -0
  14. package/dist/language.d.ts.map +1 -0
  15. package/dist/language.js +30 -0
  16. package/dist/language.js.map +1 -0
  17. package/dist/lex-equals.d.ts +3 -0
  18. package/dist/lex-equals.d.ts.map +1 -0
  19. package/dist/lex-equals.js +78 -0
  20. package/dist/lex-equals.js.map +1 -0
  21. package/dist/lex.d.ts +18 -0
  22. package/dist/lex.d.ts.map +1 -0
  23. package/dist/lex.js +83 -0
  24. package/dist/lex.js.map +1 -0
  25. package/dist/lib/nodejs-buffer.d.ts +15 -0
  26. package/dist/lib/nodejs-buffer.d.ts.map +1 -0
  27. package/dist/lib/nodejs-buffer.js +12 -0
  28. package/dist/lib/nodejs-buffer.js.map +1 -0
  29. package/dist/object.d.ts +3 -0
  30. package/dist/object.d.ts.map +1 -0
  31. package/dist/object.js +22 -0
  32. package/dist/object.js.map +1 -0
  33. package/dist/uint8array-from-base64.d.ts +16 -0
  34. package/dist/uint8array-from-base64.d.ts.map +1 -0
  35. package/dist/uint8array-from-base64.js +60 -0
  36. package/dist/uint8array-from-base64.js.map +1 -0
  37. package/dist/uint8array-to-base64.d.ts +16 -0
  38. package/dist/uint8array-to-base64.d.ts.map +1 -0
  39. package/dist/uint8array-to-base64.js +30 -0
  40. package/dist/uint8array-to-base64.js.map +1 -0
  41. package/dist/uint8array.d.ts +21 -0
  42. package/dist/uint8array.d.ts.map +1 -0
  43. package/dist/uint8array.js +57 -0
  44. package/dist/uint8array.js.map +1 -0
  45. package/dist/utf8-grapheme-len.d.ts +3 -0
  46. package/dist/utf8-grapheme-len.d.ts.map +1 -0
  47. package/dist/utf8-grapheme-len.js +23 -0
  48. package/dist/utf8-grapheme-len.js.map +1 -0
  49. package/dist/utf8-len.d.ts +3 -0
  50. package/dist/utf8-len.d.ts.map +1 -0
  51. package/dist/utf8-len.js +50 -0
  52. package/dist/utf8-len.js.map +1 -0
  53. package/dist/utf8.d.ts +3 -0
  54. package/dist/utf8.d.ts.map +1 -0
  55. package/dist/utf8.js +12 -0
  56. package/dist/utf8.js.map +1 -0
  57. package/package.json +51 -0
  58. package/src/blob.test.ts +186 -0
  59. package/src/blob.ts +99 -0
  60. package/src/cid.ts +50 -0
  61. package/src/index.ts +8 -0
  62. package/src/language.test.ts +87 -0
  63. package/src/language.ts +39 -0
  64. package/src/lex-equals.test.ts +153 -0
  65. package/src/lex-equals.ts +85 -0
  66. package/src/lex.test.ts +124 -0
  67. package/src/lex.ts +78 -0
  68. package/src/lib/nodejs-buffer.ts +27 -0
  69. package/src/object.test.ts +78 -0
  70. package/src/object.ts +21 -0
  71. package/src/uint8array-from-base64.test.ts +113 -0
  72. package/src/uint8array-from-base64.ts +85 -0
  73. package/src/uint8array-to-base64.ts +45 -0
  74. package/src/uint8array.ts +78 -0
  75. package/src/utf8-grapheme-len.test.ts +37 -0
  76. package/src/utf8-grapheme-len.ts +21 -0
  77. package/src/utf8-len.test.ts +31 -0
  78. package/src/utf8-len.ts +51 -0
  79. package/src/utf8.ts +14 -0
  80. package/tsconfig.build.json +12 -0
  81. package/tsconfig.json +7 -0
  82. package/tsconfig.tests.json +9 -0
package/src/blob.ts ADDED
@@ -0,0 +1,99 @@
1
+ import { CID, RAW_BIN_MULTICODEC, SHA2_256_MULTIHASH_CODE } from './cid.js'
2
+ import { isPlainObject } from './object.js'
3
+
4
+ export type BlobRef = {
5
+ $type: 'blob'
6
+ mimeType: string
7
+ ref: CID
8
+ size: number
9
+ }
10
+
11
+ export function isBlobRef(
12
+ input: unknown,
13
+ options?: { strict?: boolean },
14
+ ): input is BlobRef {
15
+ if (!isPlainObject(input)) {
16
+ return false
17
+ }
18
+
19
+ if (input?.$type !== 'blob') {
20
+ return false
21
+ }
22
+
23
+ const { mimeType, size, ref } = input
24
+ if (typeof mimeType !== 'string') {
25
+ return false
26
+ }
27
+
28
+ if (typeof size !== 'number' || size < 0 || !Number.isInteger(size)) {
29
+ return false
30
+ }
31
+
32
+ if (typeof ref !== 'object' || ref === null) {
33
+ return false
34
+ }
35
+
36
+ for (const key in input) {
37
+ if (
38
+ key !== '$type' &&
39
+ key !== 'mimeType' &&
40
+ key !== 'ref' &&
41
+ key !== 'size'
42
+ ) {
43
+ return false
44
+ }
45
+ }
46
+
47
+ const cid = CID.asCID(ref)
48
+ if (!cid) {
49
+ return false
50
+ }
51
+
52
+ if (options?.strict) {
53
+ if (cid.version !== 1) {
54
+ return false
55
+ }
56
+ if (cid.code !== RAW_BIN_MULTICODEC) {
57
+ return false
58
+ }
59
+ if (cid.multihash.code !== SHA2_256_MULTIHASH_CODE) {
60
+ return false
61
+ }
62
+ }
63
+
64
+ return true
65
+ }
66
+
67
+ export type LegacyBlobRef = {
68
+ cid: string
69
+ mimeType: string
70
+ }
71
+
72
+ export function isLegacyBlobRef(input: unknown): input is LegacyBlobRef {
73
+ if (!isPlainObject(input)) {
74
+ return false
75
+ }
76
+
77
+ const { cid, mimeType } = input
78
+ if (typeof cid !== 'string') {
79
+ return false
80
+ }
81
+
82
+ if (typeof mimeType !== 'string') {
83
+ return false
84
+ }
85
+
86
+ for (const key in input) {
87
+ if (key !== 'cid' && key !== 'mimeType') {
88
+ return false
89
+ }
90
+ }
91
+
92
+ try {
93
+ CID.parse(cid)
94
+ } catch {
95
+ return false
96
+ }
97
+
98
+ return true
99
+ }
package/src/cid.ts ADDED
@@ -0,0 +1,50 @@
1
+ import { CID } from 'multiformats/cid'
2
+
3
+ export const DAG_CBOR_MULTICODEC = 0x71
4
+ export const RAW_BIN_MULTICODEC = 0x55
5
+
6
+ export const SHA2_256_MULTIHASH_CODE = 0x12
7
+
8
+ export { CID }
9
+
10
+ export function isCid(
11
+ value: unknown,
12
+ options?: { strict?: boolean },
13
+ ): value is CID {
14
+ const cid = CID.asCID(value)
15
+ if (!cid) {
16
+ return false
17
+ }
18
+
19
+ if (options?.strict) {
20
+ if (cid.version !== 1) {
21
+ return false
22
+ }
23
+ if (cid.code !== RAW_BIN_MULTICODEC && cid.code !== DAG_CBOR_MULTICODEC) {
24
+ return false
25
+ }
26
+ if (cid.multihash.code !== SHA2_256_MULTIHASH_CODE) {
27
+ return false
28
+ }
29
+ }
30
+
31
+ return true
32
+ }
33
+
34
+ export function validateCidString(input: string): boolean {
35
+ return parseCidString(input)?.toString() === input
36
+ }
37
+
38
+ export function parseCidString(input: string): CID | undefined {
39
+ try {
40
+ return CID.parse(input)
41
+ } catch {
42
+ return undefined
43
+ }
44
+ }
45
+
46
+ export function ensureValidCidString(input: string): void {
47
+ if (!validateCidString(input)) {
48
+ throw new Error(`Invalid CID string`)
49
+ }
50
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from './blob.js'
2
+ export * from './cid.js'
3
+ export * from './language.js'
4
+ export * from './lex-equals.js'
5
+ export * from './lex.js'
6
+ export * from './object.js'
7
+ export * from './uint8array.js'
8
+ export * from './utf8.js'
@@ -0,0 +1,87 @@
1
+ import { isLanguage, parseLanguage } from './language'
2
+
3
+ describe('string', () => {
4
+ describe('languages', () => {
5
+ it('validates BCP 47', () => {
6
+ // valid
7
+ expect(isLanguage('de')).toEqual(true)
8
+ expect(isLanguage('de-CH')).toEqual(true)
9
+ expect(isLanguage('de-DE-1901')).toEqual(true)
10
+ expect(isLanguage('es-419')).toEqual(true)
11
+ expect(isLanguage('sl-IT-nedis')).toEqual(true)
12
+ expect(isLanguage('mn-Cyrl-MN')).toEqual(true)
13
+ expect(isLanguage('x-fr-CH')).toEqual(true)
14
+ expect(isLanguage('en-GB-boont-r-extended-sequence-x-private')).toEqual(
15
+ true,
16
+ )
17
+ expect(isLanguage('sr-Cyrl')).toEqual(true)
18
+ expect(isLanguage('hy-Latn-IT-arevela')).toEqual(true)
19
+ expect(isLanguage('i-klingon')).toEqual(true)
20
+ // invalid
21
+ expect(isLanguage('')).toEqual(false)
22
+ expect(isLanguage('x')).toEqual(false)
23
+ expect(isLanguage('de-CH-')).toEqual(false)
24
+ expect(isLanguage('i-bad-grandfathered')).toEqual(false)
25
+ })
26
+
27
+ it('parses BCP 47', () => {
28
+ // valid
29
+ expect(parseLanguage('de')).toEqual({
30
+ language: 'de',
31
+ })
32
+ expect(parseLanguage('de-CH')).toEqual({
33
+ language: 'de',
34
+ region: 'CH',
35
+ })
36
+ expect(parseLanguage('de-DE-1901')).toEqual({
37
+ language: 'de',
38
+ region: 'DE',
39
+ variant: '1901',
40
+ })
41
+ expect(parseLanguage('es-419')).toEqual({
42
+ language: 'es',
43
+ region: '419',
44
+ })
45
+ expect(parseLanguage('sl-IT-nedis')).toEqual({
46
+ language: 'sl',
47
+ region: 'IT',
48
+ variant: 'nedis',
49
+ })
50
+ expect(parseLanguage('mn-Cyrl-MN')).toEqual({
51
+ language: 'mn',
52
+ script: 'Cyrl',
53
+ region: 'MN',
54
+ })
55
+ expect(parseLanguage('x-fr-CH')).toEqual({
56
+ privateUse: 'x-fr-CH',
57
+ })
58
+ expect(
59
+ parseLanguage('en-GB-boont-r-extended-sequence-x-private'),
60
+ ).toEqual({
61
+ language: 'en',
62
+ region: 'GB',
63
+ variant: 'boont',
64
+ extension: 'r-extended-sequence',
65
+ privateUse: 'x-private',
66
+ })
67
+ expect(parseLanguage('sr-Cyrl')).toEqual({
68
+ language: 'sr',
69
+ script: 'Cyrl',
70
+ })
71
+ expect(parseLanguage('hy-Latn-IT-arevela')).toEqual({
72
+ language: 'hy',
73
+ script: 'Latn',
74
+ region: 'IT',
75
+ variant: 'arevela',
76
+ })
77
+ expect(parseLanguage('i-klingon')).toEqual({
78
+ grandfathered: 'i-klingon',
79
+ })
80
+ // invalid
81
+ expect(parseLanguage('')).toEqual(null)
82
+ expect(parseLanguage('x')).toEqual(null)
83
+ expect(parseLanguage('de-CH-')).toEqual(null)
84
+ expect(parseLanguage('i-bad-grandfathered')).toEqual(null)
85
+ })
86
+ })
87
+ })
@@ -0,0 +1,39 @@
1
+ const BCP47_REGEXP =
2
+ /^((?<grandfathered>(en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang))|((?<language>([A-Za-z]{2,3}(-(?<extlang>[A-Za-z]{3}(-[A-Za-z]{3}){0,2}))?)|[A-Za-z]{4}|[A-Za-z]{5,8})(-(?<script>[A-Za-z]{4}))?(-(?<region>[A-Za-z]{2}|[0-9]{3}))?(-(?<variant>[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(-(?<extension>[0-9A-WY-Za-wy-z](-[A-Za-z0-9]{2,8})+))*(-(?<privateUseA>x(-[A-Za-z0-9]{1,8})+))?)|(?<privateUseB>x(-[A-Za-z0-9]{1,8})+))$/
3
+
4
+ export type LanguageTag = {
5
+ grandfathered?: string
6
+ language?: string
7
+ extlang?: string
8
+ script?: string
9
+ region?: string
10
+ variant?: string
11
+ extension?: string
12
+ privateUse?: string
13
+ }
14
+
15
+ export function parseLanguage(input: string): LanguageTag | null {
16
+ const parsed = input.match(BCP47_REGEXP)
17
+ if (!parsed?.groups) return null
18
+
19
+ const { groups } = parsed
20
+ return {
21
+ grandfathered: groups.grandfathered,
22
+ language: groups.language,
23
+ extlang: groups.extlang,
24
+ script: groups.script,
25
+ region: groups.region,
26
+ variant: groups.variant,
27
+ extension: groups.extension,
28
+ privateUse: groups.privateUseA || groups.privateUseB,
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Validates well-formed BCP 47 syntax
34
+ *
35
+ * @see {@link https://www.rfc-editor.org/rfc/rfc5646.html#section-2.1}
36
+ */
37
+ export function isLanguage(input: string): boolean {
38
+ return BCP47_REGEXP.test(input)
39
+ }
@@ -0,0 +1,153 @@
1
+ import { CID } from './cid.js'
2
+ import { lexEquals } from './lex-equals.js'
3
+ import { LexValue } from './lex.js'
4
+
5
+ function expectLexEqual(a: LexValue, b: LexValue, expected: boolean) {
6
+ expect(lexEquals(a, b)).toBe(expected)
7
+ expect(lexEquals(b, a)).toBe(expected)
8
+ }
9
+
10
+ describe('lexEquals', () => {
11
+ it('compares primitive values', () => {
12
+ expectLexEqual(null, null, true)
13
+ expectLexEqual(true, true, true)
14
+ expectLexEqual(false, false, true)
15
+ expectLexEqual(42, 42, true)
16
+ expectLexEqual('hello', 'hello', true)
17
+
18
+ expectLexEqual(null, false, false)
19
+ expectLexEqual(false, null, false)
20
+ expectLexEqual(true, false, false)
21
+ expectLexEqual(false, true, false)
22
+ expectLexEqual(42, 43, false)
23
+ expectLexEqual('hello', 'world', false)
24
+ })
25
+
26
+ it('compares NaN and Infinity correctly', () => {
27
+ expectLexEqual(NaN, NaN, true)
28
+ expectLexEqual(Infinity, Infinity, true)
29
+ expectLexEqual(-Infinity, -Infinity, true)
30
+
31
+ expectLexEqual(NaN, 0, false)
32
+ expectLexEqual(NaN, null, false)
33
+ expectLexEqual(Infinity, -Infinity, false)
34
+ })
35
+
36
+ it('compares arrays', () => {
37
+ expectLexEqual([1, 2, 3], [1, 2, 3], true)
38
+ expectLexEqual([1, 2, 3], [1, 2, 4], false)
39
+ expectLexEqual([1, 2, 3], [1, 2], false)
40
+ expectLexEqual([1, 2, 3], 'not an array', false)
41
+ })
42
+
43
+ it('compares Uint8Arrays', () => {
44
+ expectLexEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3]), true)
45
+ expectLexEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 4]), false)
46
+ expectLexEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2]), false)
47
+ expectLexEqual(new Uint8Array([1, 2, 3]), 'not a Uint8Array', false)
48
+ })
49
+
50
+ it('compares CIDs', () => {
51
+ const cid1 = CID.parse(
52
+ 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
53
+ )
54
+ const cid2 = CID.parse(cid1.toString())
55
+ const cid3 = CID.parse(cid1.toString())
56
+
57
+ expectLexEqual(cid1, cid2, true)
58
+ expectLexEqual(cid1, cid3, true)
59
+ expectLexEqual(cid2, cid3, true)
60
+
61
+ expectLexEqual(cid1, cid1.toString(), false)
62
+ })
63
+
64
+ it('compares objects', () => {
65
+ expectLexEqual({ a: 1, b: 2 }, { a: 1, b: 2 }, true)
66
+ expectLexEqual(
67
+ { a: 1, b: { unicode: 'a~öñ©⽘☎𓋓😀👨‍👩‍👧‍👧' } },
68
+ { a: 1, b: { unicode: 'a~öñ©⽘☎𓋓😀👨‍👩‍👧‍👧' } },
69
+ true,
70
+ )
71
+
72
+ expectLexEqual({ a: 1, b: 2 }, { a: 1, b: 3 }, false)
73
+ expectLexEqual({ a: 1, b: 2 }, { a: 1 }, false)
74
+ expectLexEqual({ a: 1, b: 2 }, 'not an object', false)
75
+ expectLexEqual({ a: 1, b: 2 }, null, false)
76
+ })
77
+
78
+ it('compares nested structures', () => {
79
+ const lex1 = {
80
+ foo: [1, 2, { bar: new Uint8Array([3, 4, 5]) }],
81
+ baz: CID.parse(
82
+ 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
83
+ ),
84
+ }
85
+ const lex2 = {
86
+ foo: [1, 2, { bar: new Uint8Array([3, 4, 5]) }],
87
+ baz: CID.parse(
88
+ 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
89
+ ),
90
+ }
91
+ const lex3 = {
92
+ foo: [1, 2, { bar: new Uint8Array([3, 4, 5 + 1]) }],
93
+ baz: CID.parse(
94
+ 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
95
+ ),
96
+ }
97
+
98
+ expectLexEqual(lex1, lex2, true)
99
+ expectLexEqual(lex1, lex3, false)
100
+ expectLexEqual(lex2, lex3, false)
101
+ })
102
+
103
+ it('allows comparing invalid numbers (floats, NaN, Infinity)', () => {
104
+ expectLexEqual(3.14, 2.71, false)
105
+ expectLexEqual(NaN, 0, false)
106
+ expectLexEqual(Infinity, -Infinity, false)
107
+ })
108
+
109
+ describe('reference equality', () => {
110
+ for (const value of [3.14, NaN, Infinity, -Infinity]) {
111
+ it(`returns true for identical references of ${String(value)}`, () => {
112
+ expectLexEqual(value, value, true)
113
+ expectLexEqual([value], [value], true)
114
+ expectLexEqual({ foo: value }, { foo: value }, true)
115
+ expectLexEqual([{ foo: value }], [{ foo: value }], true)
116
+ })
117
+ }
118
+ })
119
+
120
+ it('returns true for identical references', () => {
121
+ const arr = [1, 2, 3]
122
+ expectLexEqual(arr, arr, true)
123
+
124
+ const obj = { a: 1, b: 2 }
125
+ expectLexEqual(obj, obj, true)
126
+
127
+ const u8 = new Uint8Array([1, 2, 3])
128
+ expectLexEqual(u8, u8, true)
129
+
130
+ const cid = CID.parse(
131
+ 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
132
+ )
133
+ expectLexEqual(cid, cid, true)
134
+ })
135
+
136
+ it('throws when comparing plain object with non-allowed class instance', () => {
137
+ // @ts-expect-error
138
+ expect(() => lexEquals({}, new Map())).toThrow()
139
+ // @ts-expect-error
140
+ expect(() => lexEquals(new Map(), {})).toThrow()
141
+ // @ts-expect-error
142
+ expect(() => lexEquals({ foo: {} }, { foo: new Map() })).toThrow()
143
+ // @ts-expect-error
144
+ expect(() => lexEquals({ foo: new Map() }, { foo: {} })).toThrow()
145
+
146
+ expect(() => lexEquals({ foo: {} }, { foo: new (class {})() })).toThrow()
147
+ expect(() => lexEquals({ foo: new (class {})() }, { foo: {} })).toThrow()
148
+
149
+ expect(() =>
150
+ lexEquals({ foo: {} }, { foo: new (class Object {})() }),
151
+ ).toThrow()
152
+ })
153
+ })
@@ -0,0 +1,85 @@
1
+ import { CID, isCid } from './cid.js'
2
+ import { LexValue } from './lex.js'
3
+ import { isPlainObject } from './object.js'
4
+ import { ui8Equals } from './uint8array.js'
5
+
6
+ export function lexEquals(a: LexValue, b: LexValue): boolean {
7
+ if (Object.is(a, b)) {
8
+ return true
9
+ }
10
+
11
+ if (
12
+ a == null ||
13
+ b == null ||
14
+ typeof a !== 'object' ||
15
+ typeof b !== 'object'
16
+ ) {
17
+ return false
18
+ }
19
+
20
+ if (Array.isArray(a)) {
21
+ if (!Array.isArray(b)) {
22
+ return false
23
+ }
24
+ if (a.length !== b.length) {
25
+ return false
26
+ }
27
+ for (let i = 0; i < a.length; i++) {
28
+ if (!lexEquals(a[i], b[i])) {
29
+ return false
30
+ }
31
+ }
32
+ return true
33
+ } else if (Array.isArray(b)) {
34
+ return false
35
+ }
36
+
37
+ if (ArrayBuffer.isView(a)) {
38
+ if (!ArrayBuffer.isView(b)) return false
39
+ return ui8Equals(a, b)
40
+ } else if (ArrayBuffer.isView(b)) {
41
+ return false
42
+ }
43
+
44
+ if (isCid(a)) {
45
+ // @NOTE CID.equals returns its argument when it is falsy (e.g. null or
46
+ // undefined) so we need to explicitly check that the output is "true".
47
+ return CID.asCID(a)!.equals(CID.asCID(b)) === true
48
+ } else if (isCid(b)) {
49
+ return false
50
+ }
51
+
52
+ if (!isPlainObject(a) || !isPlainObject(b)) {
53
+ // Foolproof (should never happen)
54
+ throw new TypeError(
55
+ 'Invalid LexValue (expected CID, Uint8Array, or LexMap)',
56
+ )
57
+ }
58
+
59
+ const aKeys = Object.keys(a)
60
+ const bKeys = Object.keys(b)
61
+
62
+ if (aKeys.length !== bKeys.length) {
63
+ return false
64
+ }
65
+
66
+ for (const key of aKeys) {
67
+ const aVal = a[key]
68
+ const bVal = b[key]
69
+
70
+ // Needed because of the optional index signature in the Lex object type
71
+ // though, in practice, aVal should never be undefined here.
72
+ if (aVal === undefined) {
73
+ if (bVal === undefined && bKeys.includes(key)) continue
74
+ return false
75
+ } else if (bVal === undefined) {
76
+ return false
77
+ }
78
+
79
+ if (!lexEquals(aVal, bVal)) {
80
+ return false
81
+ }
82
+ }
83
+
84
+ return true
85
+ }
@@ -0,0 +1,124 @@
1
+ import { isTypedLexMap } from './lex'
2
+
3
+ describe('isLexMap', () => {
4
+ it('returns true for valid LexMap', () => {
5
+ const record = {
6
+ a: 123,
7
+ b: 'blah',
8
+ c: true,
9
+ d: null,
10
+ e: new Uint8Array([1, 2, 3]),
11
+ f: {
12
+ nested: 'value',
13
+ },
14
+ g: [1, 2, 3],
15
+ }
16
+ expect(isTypedLexMap(record)).toBe(false)
17
+ })
18
+
19
+ it('returns false for non-records', () => {
20
+ const values = [
21
+ 123,
22
+ 'blah',
23
+ true,
24
+ null,
25
+ new Uint8Array([1, 2, 3]),
26
+ [1, 2, 3],
27
+ ]
28
+ for (const value of values) {
29
+ expect(isTypedLexMap(value)).toBe(false)
30
+ }
31
+ })
32
+
33
+ it('returns false for records with non-Lex values', () => {
34
+ expect(
35
+ // @ts-expect-error
36
+ isTypedLexMap({
37
+ a: 123,
38
+ b: () => {},
39
+ }),
40
+ ).toBe(false)
41
+ expect(
42
+ isTypedLexMap({
43
+ a: 123,
44
+ b: undefined,
45
+ }),
46
+ ).toBe(false)
47
+ })
48
+ })
49
+
50
+ describe('isTypedLexMap', () => {
51
+ describe('valid records', () => {
52
+ for (const { note, json } of [
53
+ {
54
+ note: 'trivial record',
55
+ json: {
56
+ $type: 'com.example.blah',
57
+ a: 123,
58
+ b: 'blah',
59
+ },
60
+ },
61
+ {
62
+ note: 'float, but integer-like',
63
+ json: {
64
+ $type: 'com.example.blah',
65
+ a: 123.0,
66
+ b: 'blah',
67
+ },
68
+ },
69
+ {
70
+ note: 'empty list and object',
71
+ json: {
72
+ $type: 'com.example.blah',
73
+ a: [],
74
+ b: {},
75
+ },
76
+ },
77
+ ]) {
78
+ it(note, () => {
79
+ expect(isTypedLexMap(json)).toBe(true)
80
+ })
81
+ }
82
+ })
83
+
84
+ describe('invalid records', () => {
85
+ for (const { note, json } of [
86
+ {
87
+ note: 'float',
88
+ json: {
89
+ $type: 'com.example.blah',
90
+ a: 123.456,
91
+ b: 'blah',
92
+ },
93
+ },
94
+ {
95
+ note: 'record with $type null',
96
+ json: {
97
+ $type: null,
98
+ a: 123,
99
+ b: 'blah',
100
+ },
101
+ },
102
+ {
103
+ note: 'record with $type wrong type',
104
+ json: {
105
+ $type: 123,
106
+ a: 123,
107
+ b: 'blah',
108
+ },
109
+ },
110
+ {
111
+ note: 'record with empty $type string',
112
+ json: {
113
+ $type: '',
114
+ a: 123,
115
+ b: 'blah',
116
+ },
117
+ },
118
+ ]) {
119
+ it(note, () => {
120
+ expect(isTypedLexMap(json)).toBe(false)
121
+ })
122
+ }
123
+ })
124
+ })