@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.
- package/dist/blob.d.ts +16 -0
- package/dist/blob.d.ts.map +1 -0
- package/dist/blob.js +73 -0
- package/dist/blob.js.map +1 -0
- package/dist/cid.d.ts +12 -0
- package/dist/cid.d.ts.map +1 -0
- package/dist/cid.js +47 -0
- package/dist/cid.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/language.d.ts +18 -0
- package/dist/language.d.ts.map +1 -0
- package/dist/language.js +30 -0
- package/dist/language.js.map +1 -0
- package/dist/lex-equals.d.ts +3 -0
- package/dist/lex-equals.d.ts.map +1 -0
- package/dist/lex-equals.js +78 -0
- package/dist/lex-equals.js.map +1 -0
- package/dist/lex.d.ts +18 -0
- package/dist/lex.d.ts.map +1 -0
- package/dist/lex.js +83 -0
- package/dist/lex.js.map +1 -0
- package/dist/lib/nodejs-buffer.d.ts +15 -0
- package/dist/lib/nodejs-buffer.d.ts.map +1 -0
- package/dist/lib/nodejs-buffer.js +12 -0
- package/dist/lib/nodejs-buffer.js.map +1 -0
- package/dist/object.d.ts +3 -0
- package/dist/object.d.ts.map +1 -0
- package/dist/object.js +22 -0
- package/dist/object.js.map +1 -0
- package/dist/uint8array-from-base64.d.ts +16 -0
- package/dist/uint8array-from-base64.d.ts.map +1 -0
- package/dist/uint8array-from-base64.js +60 -0
- package/dist/uint8array-from-base64.js.map +1 -0
- package/dist/uint8array-to-base64.d.ts +16 -0
- package/dist/uint8array-to-base64.d.ts.map +1 -0
- package/dist/uint8array-to-base64.js +30 -0
- package/dist/uint8array-to-base64.js.map +1 -0
- package/dist/uint8array.d.ts +21 -0
- package/dist/uint8array.d.ts.map +1 -0
- package/dist/uint8array.js +57 -0
- package/dist/uint8array.js.map +1 -0
- package/dist/utf8-grapheme-len.d.ts +3 -0
- package/dist/utf8-grapheme-len.d.ts.map +1 -0
- package/dist/utf8-grapheme-len.js +23 -0
- package/dist/utf8-grapheme-len.js.map +1 -0
- package/dist/utf8-len.d.ts +3 -0
- package/dist/utf8-len.d.ts.map +1 -0
- package/dist/utf8-len.js +50 -0
- package/dist/utf8-len.js.map +1 -0
- package/dist/utf8.d.ts +3 -0
- package/dist/utf8.d.ts.map +1 -0
- package/dist/utf8.js +12 -0
- package/dist/utf8.js.map +1 -0
- package/package.json +51 -0
- package/src/blob.test.ts +186 -0
- package/src/blob.ts +99 -0
- package/src/cid.ts +50 -0
- package/src/index.ts +8 -0
- package/src/language.test.ts +87 -0
- package/src/language.ts +39 -0
- package/src/lex-equals.test.ts +153 -0
- package/src/lex-equals.ts +85 -0
- package/src/lex.test.ts +124 -0
- package/src/lex.ts +78 -0
- package/src/lib/nodejs-buffer.ts +27 -0
- package/src/object.test.ts +78 -0
- package/src/object.ts +21 -0
- package/src/uint8array-from-base64.test.ts +113 -0
- package/src/uint8array-from-base64.ts +85 -0
- package/src/uint8array-to-base64.ts +45 -0
- package/src/uint8array.ts +78 -0
- package/src/utf8-grapheme-len.test.ts +37 -0
- package/src/utf8-grapheme-len.ts +21 -0
- package/src/utf8-len.test.ts +31 -0
- package/src/utf8-len.ts +51 -0
- package/src/utf8.ts +14 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +7 -0
- 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,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
|
+
})
|
package/src/language.ts
ADDED
|
@@ -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
|
+
}
|
package/src/lex.test.ts
ADDED
|
@@ -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
|
+
})
|