a collection of lightweight TypeScript packages for AT Protocol, the protocol powering Bluesky
atproto bluesky typescript npm
101
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(lexicon-doc): include lexicon ref parsing utilities

Mary 2c386c71 330317ba

+173 -79
+5
.changeset/real-gifts-learn.md
··· 1 + --- 2 + '@atcute/lexicon-doc': minor 3 + --- 4 + 5 + include lexicon ref parsing utilities
+10 -24
packages/lexicons/lexicon-doc/lib/builder.ts
··· 3 3 import { isWithinGraphemeBounds, isWithinUtf8Bounds } from './internal/utils.js'; 4 4 import { DELIMITED_MIME_TYPE_RE, KEY_RE, MIME_TYPE_RE, validateStringFormat } from './internal/validation.js'; 5 5 import type * as t from './types.js'; 6 + import { formatLexiconRef, type ParsedLexiconRef } from './utils/refs.js'; 6 7 7 8 // #region Utilities 8 - type LexPath = { 9 - nsid: string; 10 - defId: string; 11 - }; 12 - 13 - const toLexUri = (path: LexPath, from?: LexPath): string => { 14 - const { nsid, defId } = path; 15 - 16 - if (from !== undefined && from.nsid === nsid) { 17 - return `#${defId}`; 18 - } 19 - 20 - return defId === 'main' ? nsid : `${nsid}#${defId}`; 21 - }; 22 - 23 9 type BuildContext = { 24 - toplevelDefs: Map<DefType | MainType, LexPath>; 25 - lexPath: LexPath; 10 + toplevelDefs: Map<DefType | MainType, ParsedLexiconRef>; 11 + lexPath: ParsedLexiconRef; 26 12 dotPath: string; 27 13 }; 28 14 ··· 38 24 39 25 return { 40 26 type: 'ref', 41 - ref: toLexUri(defPath, ctx.lexPath), 27 + ref: formatLexiconRef(defPath, ctx.lexPath.nsid), 42 28 }; 43 29 }; 44 30 ··· 459 445 } 460 446 461 447 // don't use the relative path here 462 - return toLexUri(defPath); 448 + return formatLexiconRef(defPath); 463 449 }; 464 450 465 451 const buildStringSchema = (ctx: BuildContext, def: LexStringBuilder): t.LexString => { ··· 921 907 throw new Error(`${ctx.dotPath}/refs/${index}: must be defined as a top-level definition`); 922 908 } 923 909 924 - return toLexUri(defPath, ctx.lexPath); 910 + return formatLexiconRef(defPath, ctx.lexPath.nsid); 925 911 }), 926 912 type: 'union', 927 913 }; ··· 1360 1346 throw new Error(`${ctx.dotPath}/collection/${index}: must be defined as a top-level definition`); 1361 1347 } 1362 1348 1363 - return toLexUri(defPath); 1349 + return formatLexiconRef(defPath); 1364 1350 }); 1365 1351 1366 1352 return { ··· 1383 1369 throw new Error(`${ctx.dotPath}/lxm/${index}: must be defined as a top-level definition`); 1384 1370 } 1385 1371 1386 - return toLexUri(defPath); 1372 + return formatLexiconRef(defPath); 1387 1373 }); 1388 1374 1389 1375 if (aud === 'inherit') { ··· 1672 1658 return doc; 1673 1659 }; 1674 1660 1675 - const collectToplevelDefs = (documents: LexDocumentBuilder[]): Map<DefType | MainType, LexPath> => { 1676 - const map = new Map<DefType | MainType, LexPath>(); 1661 + const collectToplevelDefs = (documents: LexDocumentBuilder[]): Map<DefType | MainType, ParsedLexiconRef> => { 1662 + const map = new Map<DefType | MainType, ParsedLexiconRef>(); 1677 1663 1678 1664 for (const doc of documents) { 1679 1665 for (const [defId, defValue] of Object.entries(doc.defs)) {
+1 -8
packages/lexicons/lexicon-doc/lib/refinements.ts
··· 1119 1119 * @returns validation issues found 1120 1120 */ 1121 1121 export const refineLexiconDoc = (spec: t.LexiconDoc, deep: boolean = false): RefineIssue[] => { 1122 - const { id, defs } = spec; 1122 + const { defs } = spec; 1123 1123 const issues: RefineIssue[] = []; 1124 - 1125 - if (!isNsid(id)) { 1126 - issues.push({ 1127 - message: `must be valid NSID`, 1128 - path: ['id'], 1129 - }); 1130 - } 1131 1124 1132 1125 for (const prop in defs) { 1133 1126 const def = defs[prop];
+5 -1
packages/lexicons/lexicon-doc/lib/typedefs.ts
··· 1 + import { isNsid, type Nsid } from '@atcute/lexicons/syntax'; 2 + 1 3 import * as v from '@badrap/valita'; 2 4 3 5 import * as t from './types.js'; ··· 5 7 const integer = v 6 8 .number() 7 9 .assert((input) => input >= 0 && Number.isSafeInteger(input), `expected non-negative integer`); 10 + 11 + const nsid: v.Type<Nsid> = v.string().assert(isNsid, `expected valid nsid`); 8 12 9 13 // #region Concrete types 10 14 export const lexBoolean: v.Type<t.LexBoolean> = v.object({ ··· 264 268 265 269 export const lexiconDoc: v.Type<t.LexiconDoc> = v.object({ 266 270 lexicon: v.literal(1), 267 - id: v.string(), 271 + id: nsid, 268 272 revision: integer.optional(), 269 273 description: v.string().optional(), 270 274 defs: v.record(lexUserType),
+3 -1
packages/lexicons/lexicon-doc/lib/types.ts
··· 1 + import type { Nsid } from '@atcute/lexicons'; 2 + 1 3 // #region Concrete types 2 4 /** 3 5 * definition for a boolean field ··· 378 380 /** indicates lexicon language version; fixed value of 1 for this version */ 379 381 lexicon: 1; 380 382 /** the NSID of this lexicon */ 381 - id: string; 383 + id: Nsid; 382 384 /** optional revision number for versioning */ 383 385 revision?: number; 384 386 /** short overview of the lexicon, usually one or two sentences */
+68 -1
packages/lexicons/lexicon-doc/lib/utils/refs.test.ts
··· 2 2 3 3 import type { LexiconDoc } from '../types.js'; 4 4 5 - import { findExternalReferences } from './refs.js'; 5 + import { findExternalReferences, formatLexiconRef, parseLexiconRef } from './refs.js'; 6 + 7 + describe('formatLexiconRef', () => { 8 + it('formats nsid with main defId as bare nsid', () => { 9 + const result = formatLexiconRef({ nsid: 'com.example.lexicon', defId: 'main' }); 10 + expect(result).toBe('com.example.lexicon'); 11 + }); 12 + 13 + it('formats nsid with non-main defId as nsid#defId', () => { 14 + const result = formatLexiconRef({ nsid: 'com.example.lexicon', defId: 'viewerState' }); 15 + expect(result).toBe('com.example.lexicon#viewerState'); 16 + }); 17 + 18 + it('formats as relative ref when context matches nsid', () => { 19 + const result = formatLexiconRef( 20 + { nsid: 'com.example.lexicon', defId: 'viewerState' }, 21 + 'com.example.lexicon', 22 + ); 23 + expect(result).toBe('#viewerState'); 24 + }); 25 + 26 + it('formats as absolute ref when context does not match', () => { 27 + const result = formatLexiconRef( 28 + { nsid: 'com.example.lexicon', defId: 'viewerState' }, 29 + 'com.example.other', 30 + ); 31 + expect(result).toBe('com.example.lexicon#viewerState'); 32 + }); 33 + 34 + it('formats main defId as relative when context matches', () => { 35 + const result = formatLexiconRef({ nsid: 'com.example.lexicon', defId: 'main' }, 'com.example.lexicon'); 36 + expect(result).toBe('#main'); 37 + }); 38 + }); 39 + 40 + describe('parseLexiconRef', () => { 41 + it('parses a bare NSID with defId defaulting to main', () => { 42 + const result = parseLexiconRef('com.example.lexicon'); 43 + expect(result).toEqual({ nsid: 'com.example.lexicon', defId: 'main' }); 44 + }); 45 + 46 + it('parses an NSID with fragment', () => { 47 + const result = parseLexiconRef('com.example.lexicon#viewerState'); 48 + expect(result).toEqual({ nsid: 'com.example.lexicon', defId: 'viewerState' }); 49 + }); 50 + 51 + it('parses a relative ref with context', () => { 52 + const result = parseLexiconRef('#viewerState', 'com.example.lexicon'); 53 + expect(result).toEqual({ nsid: 'com.example.lexicon', defId: 'viewerState' }); 54 + }); 55 + 56 + it('throws on relative ref without context', () => { 57 + expect(() => parseLexiconRef('#viewerState')).toThrow('relative ref requires context nsid'); 58 + }); 59 + 60 + it('throws on invalid nsid', () => { 61 + expect(() => parseLexiconRef('invalid')).toThrow('invalid nsid'); 62 + }); 63 + 64 + it('throws on invalid nsid in ref with fragment', () => { 65 + expect(() => parseLexiconRef('invalid#main')).toThrow('invalid nsid in ref'); 66 + }); 67 + 68 + it('handles NSID with explicit #main fragment', () => { 69 + const result = parseLexiconRef('app.bsky.feed.post#main'); 70 + expect(result).toEqual({ nsid: 'app.bsky.feed.post', defId: 'main' }); 71 + }); 72 + }); 6 73 7 74 describe('findExternalReferences', () => { 8 75 it('returns empty set for document with no references', () => {
+66
packages/lexicons/lexicon-doc/lib/utils/refs.ts
··· 1 + import { isNsid, type Nsid } from '@atcute/lexicons/syntax'; 2 + 1 3 import type { LexiconDoc, LexRefVariant, LexUserType } from '../types.js'; 4 + 5 + /** 6 + * represents a lexicon definition reference 7 + * - full NSID: `com.example.lexicon` (refers to #main) 8 + * - NSID with fragment: `com.example.lexicon#defId` 9 + * - relative ref: `#defId` (requires context NSID to resolve) 10 + */ 11 + export type LexiconRef = Nsid | `${Nsid}#${string}` | `#${string}`; 12 + 13 + export interface ParsedLexiconRef { 14 + nsid: Nsid; 15 + defId: string; 16 + } 17 + 18 + /** 19 + * formats a parsed lexicon reference back to a string 20 + * @param parsed the parsed reference 21 + * @param context if provided and matches parsed.nsid, outputs a relative ref (#defId) 22 + * @returns formatted lexicon reference string 23 + */ 24 + export const formatLexiconRef = (parsed: ParsedLexiconRef, context?: Nsid): string => { 25 + const { nsid, defId } = parsed; 26 + 27 + if (context !== undefined && context === nsid) { 28 + return `#${defId}`; 29 + } 30 + 31 + return defId === 'main' ? nsid : `${nsid}#${defId}`; 32 + }; 33 + 34 + /** 35 + * parses a lexicon definition reference into its components 36 + * @param ref the lexicon reference to parse 37 + * @param context context NSID, required for relative refs (e.g., `#defId`) 38 + * @returns parsed reference with nsid and defId (defId defaults to 'main' if not specified) 39 + * @throws if the ref is invalid or a relative ref is passed without context 40 + */ 41 + export const parseLexiconRef = (ref: string, context?: Nsid): ParsedLexiconRef => { 42 + const hashIndex = ref.indexOf('#'); 43 + 44 + if (hashIndex === 0) { 45 + if (context === undefined) { 46 + throw new SyntaxError(`relative ref requires context nsid: ${ref}`); 47 + } 48 + 49 + return { nsid: context, defId: ref.slice(1) }; 50 + } 51 + 52 + if (hashIndex === -1) { 53 + if (!isNsid(ref)) { 54 + throw new SyntaxError(`invalid nsid: ${ref}`); 55 + } 56 + 57 + return { nsid: ref, defId: 'main' }; 58 + } 59 + 60 + const nsid = ref.slice(0, hashIndex); 61 + const defId = ref.slice(hashIndex + 1); 62 + if (!isNsid(nsid)) { 63 + throw new SyntaxError(`invalid nsid in ref: ${nsid}`); 64 + } 65 + 66 + return { nsid: nsid, defId }; 67 + }; 2 68 3 69 type SchemaValue = LexUserType | LexRefVariant; 4 70
+15 -44
packages/lexicons/lexicon-doc/lib/validations.ts
··· 15 15 type RefineIssue, 16 16 } from './refinements.js'; 17 17 import type * as t from './types.js'; 18 + import { formatLexiconRef, parseLexiconRef, type ParsedLexiconRef } from './utils/refs.js'; 18 19 19 20 export interface RecordValidatorInput { 20 21 key: string | null; ··· 38 39 39 40 const def = getDefinition(ctx, null, path); 40 41 if (def.type !== 'record') { 41 - throw new Error(`${toLexUri(path)} is not a record definition (got ${def.type})`); 42 + throw new Error(`${formatLexiconRef(path)} is not a record definition (got ${def.type})`); 42 43 } 43 44 44 45 const validator = v.object({ ··· 83 84 }; 84 85 }; 85 86 86 - interface LexPath { 87 - nsid: string; 88 - defId: string; 87 + interface LexPath extends ParsedLexiconRef { 89 88 dotPath: string; 90 89 } 91 90 ··· 94 93 cache: WeakMap<t.LexUserType, Cell<v.BaseSchema> | null>; 95 94 } 96 95 97 - const toLexUri = (path: LexPath) => { 98 - const { nsid, defId } = path; 99 - 100 - return defId === 'main' ? nsid : `${nsid}#${defId}`; 101 - }; 102 - 103 96 const formatPath = (path: LexPath) => { 104 - return toLexUri(path) + path.dotPath; 97 + return formatLexiconRef(path) + path.dotPath; 105 98 }; 106 99 107 100 const resolvePath = (path: LexPath, ref: string): LexPath => { 108 - const index = ref.indexOf('#'); 109 - 110 - // nsid 111 - if (index === -1) { 112 - return { 113 - nsid: ref, 114 - defId: 'main', 115 - dotPath: '', 116 - }; 117 - } 118 - 119 - // #defId 120 - if (index === 0) { 121 - return { 122 - nsid: path.nsid, 123 - defId: ref.slice(1), 124 - dotPath: '', 125 - }; 126 - } 127 - 128 - // nsid#defId 129 - return { 130 - nsid: ref.slice(0, index), 131 - defId: ref.slice(index + 1), 132 - dotPath: '', 133 - }; 101 + const parsed = parseLexiconRef(ref, path.nsid); 102 + return { nsid: parsed.nsid, defId: parsed.defId, dotPath: '' }; 134 103 }; 135 104 136 105 const getDefinition = (ctx: BuildContext, from: LexPath | null, path: LexPath): t.LexUserType => { ··· 140 109 throw new Error(`can't find document: ${path.nsid}`); 141 110 } 142 111 143 - throw new Error(`${toLexUri(from)} tried to reference a nonexistent document: ${path.nsid}`); 112 + throw new Error(`${formatLexiconRef(from)} tried to reference a nonexistent document: ${path.nsid}`); 144 113 } 145 114 146 115 const def = doc.defs[path.defId]; 147 116 if (def === undefined) { 148 117 if (from === null) { 149 - throw new Error(`can't find definition: ${toLexUri(path)}`); 118 + throw new Error(`can't find definition: ${formatLexiconRef(path)}`); 150 119 } 151 120 152 - throw new Error(`${toLexUri(from)} tried to reference a nonexistent definition: ${toLexUri(path)}`); 121 + throw new Error( 122 + `${formatLexiconRef(from)} tried to reference a nonexistent definition: ${formatLexiconRef(path)}`, 123 + ); 153 124 } 154 125 155 126 return def; ··· 426 397 return cell; 427 398 } 428 399 429 - let schema: v.BaseSchema = v.literal(toLexUri(path)); 400 + let schema: v.BaseSchema = v.literal(formatLexiconRef(path)); 430 401 431 402 cell = eager(schema); 432 403 ctx.cache.set(spec, cell); ··· 512 483 } 513 484 514 485 throw new Error( 515 - `${formatPath(path)}/refs/${idx}: unsupported ref target (${toLexUri(refPath)} -> ${refSpec.type})`, 486 + `${formatPath(path)}/refs/${idx}: unsupported ref target (${formatLexiconRef(refPath)} -> ${refSpec.type})`, 516 487 ); 517 488 }); 518 489 ··· 678 649 679 650 switch (writeType) { 680 651 case 'optional': { 681 - obj.$type = v.optional(v.literal(toLexUri(path))); 652 + obj.$type = v.optional(v.literal(formatLexiconRef(path))); 682 653 break; 683 654 } 684 655 case 'required': { 685 - obj.$type = v.literal(toLexUri(path)); 656 + obj.$type = v.literal(formatLexiconRef(path)); 686 657 break; 687 658 } 688 659 }