// Records what @atproto/lexicon, atproto's reference, makes of values // checked against lexicons, into lexicon.json beside this: each case's // lexicons, the def it is checked against (a record's or an object's, or a // method's parameters, input or message), the value, and the reference's // verdict. The cases are lexicon.js's (github.com/bigmoves/lexicon.js, // test/inputs/{data,format,e2e}), and a few of this crate's own below. // // node lexicon.mjs > lexicon.json import { pathToFileURL } from 'node:url' import { readFileSync } from 'node:fs' import { join } from 'node:path' const [inputsDir, refDir] = process.argv.slice(2) const pkg = join(refDir, 'node_modules/@atproto/lexicon') const { Lexicons, jsonToLex } = await import(pathToFileURL(join(pkg, 'dist/index.js')).href) const version = JSON.parse(readFileSync(join(pkg, 'package.json'))).version const inputs = async (kind) => { const mod = await import(pathToFileURL(join(inputsDir, 'test/inputs', kind, 'index.js')).href) return Object.values(mod).filter(Array.isArray).flat() } // Bytes a case holds as a Uint8Array are written as JSON writes them. const toJson = (v) => JSON.parse( JSON.stringify(v, (_, x) => x instanceof Uint8Array ? { $bytes: Buffer.from(x).toString('base64').replace(/=+$/, '') } : x, ), ) // What the def a case names is checked as: a record or an object, or a // method's parameters, input or message. const kindOf = (lexicons, ref) => { const [nsid, name = 'main'] = ref.split('#') const def = lexicons.find((l) => l.id === nsid)?.defs?.[name] switch (def?.type) { case 'query': return 'params' case 'procedure': return def.input ? 'input' : 'params' case 'subscription': return 'params' default: return 'record' } } const verdict = (lexicons, ref, kind, value) => { try { const lex = new Lexicons() for (const doc of lexicons) lex.add(structuredClone(doc)) const lexValue = jsonToLex(structuredClone(value)) if (kind === 'params') lex.assertValidXrpcParams(ref, lexValue) else if (kind === 'input') lex.assertValidXrpcInput(ref, lexValue) else { const res = lex.validate(ref, lexValue) if (!res.success) return { valid: false, error: res.error.message } } return { valid: true } } catch (e) { return { valid: false, error: e.message } } } const doc = (id, main, defs = {}) => ({ lexicon: 1, id, defs: { main, ...defs } }) const record = (properties, extra = {}) => ({ type: 'record', key: 'tid', record: { type: 'object', properties, ...extra }, }) const one = (name, field, value, extra) => ({ name, lexicons: [doc('test.own.case', record({ v: field }, extra))], collection: 'test.own.case', record: value === undefined ? {} : { v: value }, }) const RAW = 'bafkreie5737gdxlw5i64vzichcalba3z2v5n6icifvx5xytvske7mr3hpm' const CBOR = 'bafyreie5737gdxlw5i64vzichcalba3z2v5n6icifvx5xytvske7mr3hpm' // This crate's own: what the cases above leave out. const own = [ one('own-valid-utf8-bytes-within', { type: 'string', maxLength: 6 }, 'ééé'), one('own-invalid-utf8-bytes-over', { type: 'string', maxLength: 5 }, 'ééé'), one('own-invalid-utf8-bytes-under', { type: 'string', minLength: 4 }, 'éa'), one('own-valid-graphemes-emoji', { type: 'string', maxGraphemes: 2 }, '👨‍👩‍👧‍👦🏳️‍🌈'), one('own-invalid-graphemes-emoji', { type: 'string', maxGraphemes: 1 }, '👨‍👩‍👧‍👦🏳️‍🌈'), one('own-valid-graphemes-min', { type: 'string', minGraphemes: 2 }, 'éa'), one('own-invalid-graphemes-min-combining', { type: 'string', minGraphemes: 2 }, 'é'), one('own-valid-string-const', { type: 'string', const: 'x' }, 'x'), one('own-invalid-string-const', { type: 'string', const: 'x' }, 'y'), one('own-valid-integer-const', { type: 'integer', const: 3 }, 3), one('own-invalid-integer-const', { type: 'integer', const: 3 }, 4), one('own-valid-integer-enum', { type: 'integer', enum: [1, 2] }, 2), one('own-invalid-integer-enum', { type: 'integer', enum: [1, 2] }, 5), one('own-invalid-integer-above', { type: 'integer', maximum: 10 }, 11), one('own-invalid-integer-float', { type: 'integer' }, 1.5), one('own-valid-boolean-const', { type: 'boolean', const: true }, true), one('own-invalid-boolean-const', { type: 'boolean', const: true }, false), one('own-invalid-boolean-string', { type: 'boolean' }, 'true'), one('own-valid-known-values-not-enforced', { type: 'string', knownValues: ['a'] }, 'b'), one('own-invalid-array-too-long', { type: 'array', items: { type: 'integer' }, maxLength: 1 }, [1, 2]), one('own-invalid-array-too-short', { type: 'array', items: { type: 'integer' }, minLength: 2 }, [1]), one('own-invalid-array-item', { type: 'array', items: { type: 'integer' } }, [1, 'x']), one('own-valid-nullable', { type: 'string' }, null, { nullable: ['v'] }), one('own-invalid-null-not-nullable', { type: 'string' }, null), one('own-invalid-required-missing', { type: 'string' }, undefined, { required: ['v'] }), one('own-invalid-required-null', { type: 'string' }, null, { required: ['v'] }), one('own-valid-required-null-nullable', { type: 'string' }, null, { required: ['v'], nullable: ['v'] }), one('own-valid-unknown-array', { type: 'unknown' }, [1]), one('own-valid-unknown-bytes', { type: 'unknown' }, { $bytes: 'AAEC' }), one('own-valid-bytes', { type: 'bytes', maxLength: 3 }, { $bytes: 'AAEC' }), one('own-invalid-bytes-too-long', { type: 'bytes', maxLength: 2 }, { $bytes: 'AAEC' }), one('own-invalid-bytes-extra-field', { type: 'bytes' }, { $bytes: 'AAEC', x: 1 }), one('own-valid-bytes-padded', { type: 'bytes' }, { $bytes: 'AAE=' }), one('own-valid-cid-link-raw', { type: 'cid-link' }, { $link: RAW }), one('own-invalid-cid-link-extra-field', { type: 'cid-link' }, { $link: RAW, x: 1 }), one('own-invalid-cid-link-string', { type: 'cid-link' }, RAW), one('own-invalid-cid-link-truncated', { type: 'cid-link' }, { $link: RAW.slice(0, -4) }), one('own-valid-cid-link-v0', { type: 'cid-link' }, { $link: 'QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR' }), one('own-valid-blob', { type: 'blob' }, { $type: 'blob', ref: { $link: RAW }, mimeType: 'image/png', size: 10 }), one('own-valid-blob-cbor-cid', { type: 'blob' }, { $type: 'blob', ref: { $link: CBOR }, mimeType: 'image/png', size: 10 }), one('own-valid-blob-legacy', { type: 'blob' }, { cid: RAW, mimeType: 'image/png' }), one('own-invalid-blob-extra-field', { type: 'blob' }, { $type: 'blob', ref: { $link: RAW }, mimeType: 'image/png', size: 10, x: 1 }), one('own-invalid-blob-ref-string', { type: 'blob' }, { $type: 'blob', ref: RAW, mimeType: 'image/png', size: 10 }), one('own-invalid-blob-size-string', { type: 'blob' }, { $type: 'blob', ref: { $link: RAW }, mimeType: 'image/png', size: '10' }), one('own-invalid-token-field', { type: 'ref', ref: 'test.own.tokens#t' }, 'test.own.tokens#t'), { name: 'own-valid-open-union-unlisted-type', lexicons: [ doc('test.own.union', record({ v: { type: 'union', refs: ['#a'] } }), { a: { type: 'object', properties: { x: { type: 'integer' } } }, }), ], collection: 'test.own.union', record: { v: { $type: 'test.own.other#b', x: 'not checked' } }, }, { name: 'own-invalid-union-member', lexicons: [ doc('test.own.union', record({ v: { type: 'union', refs: ['#a'] } }), { a: { type: 'object', properties: { x: { type: 'integer' } } }, }), ], collection: 'test.own.union', record: { v: { $type: 'test.own.union#a', x: 'no' } }, }, { name: 'own-valid-union-main-by-nsid', lexicons: [ doc('test.own.union', record({ v: { type: 'union', refs: ['test.own.member'] } })), doc('test.own.member', { type: 'object', properties: { x: { type: 'integer' } } }), ], collection: 'test.own.union', record: { v: { $type: 'test.own.member#main', x: 1 } }, }, { name: 'own-invalid-closed-union', lexicons: [ doc('test.own.union', record({ v: { type: 'union', refs: ['#a'], closed: true } }), { a: { type: 'object', properties: {} }, }), ], collection: 'test.own.union', record: { v: { $type: 'test.own.union#b' } }, }, { name: 'own-valid-object-def', lexicons: [doc('test.own.object', { type: 'object', required: ['x'], properties: { x: { type: 'integer' } } })], collection: 'test.own.object', record: { x: 1 }, }, { name: 'own-invalid-input-path', lexicons: [ doc('test.own.call', { type: 'procedure', input: { encoding: 'application/json', schema: { type: 'ref', ref: '#body' } }, }, { body: { type: 'object', properties: { roles: { type: 'array', items: { type: 'ref', ref: '#role' } } } }, role: { type: 'object', required: ['role'], properties: { role: { type: 'string', maxLength: 3 } } }, }), ], collection: 'test.own.call', record: { roles: [{ role: 'a' }, { role: 'b' }, { role: 'long' }] }, }, { name: 'own-invalid-params-array-item', lexicons: [ doc('test.own.query', { type: 'query', parameters: { type: 'params', properties: { n: { type: 'array', items: { type: 'integer', maximum: 3 } } } }, }), ], collection: 'test.own.query', record: { n: [1, 4] }, }, ] own.forEach((c) => { if (c.name.startsWith('own-invalid-token')) { c.lexicons.push(doc('test.own.tokens', { type: 'token', description: 'unused' }, { t: { type: 'token' } })) } }) const cases = [...(await inputs('data')), ...(await inputs('format')), ...(await inputs('e2e')), ...own] const seen = new Set() const vectors = [] for (const c of cases) { if (seen.has(c.name)) continue // a set exported twice seen.add(c.name) const value = toJson(c.record) const kind = kindOf(c.lexicons, c.collection) vectors.push({ name: c.name, lexicons: c.lexicons, ref: c.collection, kind, value, ...verdict(c.lexicons, c.collection, kind, value), }) } console.log(JSON.stringify({ generatedFrom: `@atproto/lexicon ${version}`, vectors }, null, 2))