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

Compare changes

Choose any two refs to compare.

Changed files
+37 -37
.changeset
packages
lexicons
lexicons
+5
.changeset/lucky-numbers-work.md
···
··· 1 + --- 2 + '@atcute/lexicons': major 3 + --- 4 + 5 + make at-uri parsing throw
+20 -21
packages/lexicons/lexicons/lib/syntax/at-uri.test.ts
··· 1 - import { assert, describe, expect, it } from 'vitest'; 2 3 import { 4 - isResourceUri, 5 - parseResourceUri, 6 isCanonicalResourceUri, 7 parseCanonicalResourceUri, 8 } from './at-uri.js'; 9 10 describe('resourceUri validation', () => { ··· 47 ]; 48 for (const str of validCases) { 49 expect(isResourceUri(str), str).toBe(true); 50 - 51 - expect(parseResourceUri(str).ok, str).toBe(true); 52 } 53 54 const invalidCases = [ ··· 106 ]; 107 for (const str of invalidCases) { 108 expect(isResourceUri(str), str).toBe(false); 109 - 110 - expect(parseResourceUri(str).ok, str).toBe(false); 111 } 112 113 expect(isResourceUri(null)).toBe(false); ··· 116 it('parses valid at-uris', () => { 117 const result = parseResourceUri('at://did:plc:asdf123/com.atproto.feed.post/record'); 118 119 - assert(result.ok); 120 - expect(result.value).toEqual({ 121 repo: 'did:plc:asdf123', 122 collection: 'com.atproto.feed.post', 123 rkey: 'record', ··· 127 128 it('parses at-uri with fragment', () => { 129 const result = parseResourceUri('at://did:plc:asdf123/com.atproto.feed.post/record#/fragment'); 130 - assert(result.ok); 131 - expect(result.value).toEqual({ 132 repo: 'did:plc:asdf123', 133 collection: 'com.atproto.feed.post', 134 rkey: 'record', ··· 137 }); 138 139 it('returns error for invalid at-uri', () => { 140 - const result = parseResourceUri('invalid-uri'); 141 - assert(!result.ok); 142 - expect(result.error).toContain('invalid at-uri'); 143 }); 144 }); 145 ··· 152 for (const str of validCases) { 153 expect(isCanonicalResourceUri(str), str).toBe(true); 154 155 - expect(parseCanonicalResourceUri(str).ok, str).toBe(true); 156 } 157 158 const invalidCases = [ ··· 164 for (const str of invalidCases) { 165 expect(isCanonicalResourceUri(str), str).toBe(false); 166 167 - expect(parseCanonicalResourceUri(str).ok, str).toBe(false); 168 } 169 170 expect(isCanonicalResourceUri(null)).toBe(false); ··· 172 173 it('parses valid canonical at-uris', () => { 174 const result = parseCanonicalResourceUri('at://did:plc:asdf123/com.atproto.feed.post/record'); 175 - assert(result.ok); 176 - expect(result.value).toEqual({ 177 repo: 'did:plc:asdf123', 178 collection: 'com.atproto.feed.post', 179 rkey: 'record', ··· 182 }); 183 184 it('returns error for invalid canonical at-uri', () => { 185 - const result = parseCanonicalResourceUri('at://user.bsky.social/com.atproto.feed.post/record'); 186 - assert(!result.ok); 187 - expect(result.error).toContain('invalid repo in canonical-at-uri'); 188 }); 189 });
··· 1 + import { describe, expect, it } from 'vitest'; 2 3 import { 4 isCanonicalResourceUri, 5 + isResourceUri, 6 parseCanonicalResourceUri, 7 + parseResourceUri, 8 } from './at-uri.js'; 9 10 describe('resourceUri validation', () => { ··· 47 ]; 48 for (const str of validCases) { 49 expect(isResourceUri(str), str).toBe(true); 50 + expect(() => parseResourceUri(str), str).not.toThrow(); 51 } 52 53 const invalidCases = [ ··· 105 ]; 106 for (const str of invalidCases) { 107 expect(isResourceUri(str), str).toBe(false); 108 + expect(() => parseResourceUri(str), str).toThrow(); 109 } 110 111 expect(isResourceUri(null)).toBe(false); ··· 114 it('parses valid at-uris', () => { 115 const result = parseResourceUri('at://did:plc:asdf123/com.atproto.feed.post/record'); 116 117 + expect(result).toEqual({ 118 repo: 'did:plc:asdf123', 119 collection: 'com.atproto.feed.post', 120 rkey: 'record', ··· 124 125 it('parses at-uri with fragment', () => { 126 const result = parseResourceUri('at://did:plc:asdf123/com.atproto.feed.post/record#/fragment'); 127 + 128 + expect(result).toEqual({ 129 repo: 'did:plc:asdf123', 130 collection: 'com.atproto.feed.post', 131 rkey: 'record', ··· 134 }); 135 136 it('returns error for invalid at-uri', () => { 137 + expect(() => parseResourceUri('invalid-uri')).toThrowErrorMatchingInlineSnapshot( 138 + `[SyntaxError: invalid at-uri: invalid-uri]`, 139 + ); 140 }); 141 }); 142 ··· 149 for (const str of validCases) { 150 expect(isCanonicalResourceUri(str), str).toBe(true); 151 152 + expect(() => parseCanonicalResourceUri(str), str).not.toThrow(); 153 } 154 155 const invalidCases = [ ··· 161 for (const str of invalidCases) { 162 expect(isCanonicalResourceUri(str), str).toBe(false); 163 164 + expect(() => parseCanonicalResourceUri(str), str).toThrow(); 165 } 166 167 expect(isCanonicalResourceUri(null)).toBe(false); ··· 169 170 it('parses valid canonical at-uris', () => { 171 const result = parseCanonicalResourceUri('at://did:plc:asdf123/com.atproto.feed.post/record'); 172 + 173 + expect(result).toEqual({ 174 repo: 'did:plc:asdf123', 175 collection: 'com.atproto.feed.post', 176 rkey: 'record', ··· 179 }); 180 181 it('returns error for invalid canonical at-uri', () => { 182 + expect(() => { 183 + return parseCanonicalResourceUri('at://user.bsky.social/com.atproto.feed.post/record'); 184 + }).toThrowErrorMatchingInlineSnapshot( 185 + `[SyntaxError: invalid repo in canonical-at-uri: user.bsky.social]`, 186 + ); 187 }); 188 });
+12 -14
packages/lexicons/lexicons/lib/syntax/at-uri.ts
··· 3 import { isNsid, type Nsid } from './nsid.js'; 4 import { isRecordKey, type RecordKey } from './record-key.js'; 5 6 - import { type Result } from '../utils.js'; 7 - 8 /** 9 * represents a general AT Protocol URI, representing either an entire 10 * repository, a specific collection within a repository, or a record. ··· 41 }; 42 43 // #__NO_SIDE_EFFECTS__ 44 - export const parseResourceUri = (input: string): Result<ParsedResourceUri, string> => { 45 const match = ATURI_RE.exec(input); 46 if (match === null) { 47 - return { ok: false, error: `invalid at-uri: ${input}` }; 48 } 49 50 const [, r, c, k, f] = match; 51 52 if (!isActorIdentifier(r)) { 53 - return { ok: false, error: `invalid repo in at-uri: ${r}` }; 54 } 55 56 if (c !== undefined && !isNsid(c)) { 57 - return { ok: false, error: `invalid collection in at-uri: ${c}` }; 58 } 59 60 if (k !== undefined && !isRecordKey(k)) { 61 - return { ok: false, error: `invalid rkey in at-uri: ${k}` }; 62 } 63 64 - return { ok: true, value: { repo: r, collection: c, rkey: k, fragment: f } }; 65 }; 66 67 /** ··· 97 }; 98 99 // #__NO_SIDE_EFFECTS__ 100 - export const parseCanonicalResourceUri = (input: string): Result<ParsedCanonicalResourceUri, string> => { 101 const match = ATURI_RE.exec(input); 102 if (match === null) { 103 - return { ok: false, error: `invalid canonical-at-uri: ${input}` }; 104 } 105 106 const [, r, c, k, f] = match; 107 108 if (!isDid(r)) { 109 - return { ok: false, error: `invalid repo in canonical-at-uri: ${r}` }; 110 } 111 112 if (!isNsid(c)) { 113 - return { ok: false, error: `invalid collection in canonical-at-uri: ${c}` }; 114 } 115 116 if (!isRecordKey(k)) { 117 - return { ok: false, error: `invalid rkey in canonical-at-uri: ${k}` }; 118 } 119 120 - return { ok: true, value: { repo: r, collection: c, rkey: k, fragment: f } }; 121 };
··· 3 import { isNsid, type Nsid } from './nsid.js'; 4 import { isRecordKey, type RecordKey } from './record-key.js'; 5 6 /** 7 * represents a general AT Protocol URI, representing either an entire 8 * repository, a specific collection within a repository, or a record. ··· 39 }; 40 41 // #__NO_SIDE_EFFECTS__ 42 + export const parseResourceUri = (input: string): ParsedResourceUri => { 43 const match = ATURI_RE.exec(input); 44 if (match === null) { 45 + throw new SyntaxError(`invalid at-uri: ${input}`); 46 } 47 48 const [, r, c, k, f] = match; 49 50 if (!isActorIdentifier(r)) { 51 + throw new SyntaxError(`invalid repo in at-uri: ${r}`); 52 } 53 54 if (c !== undefined && !isNsid(c)) { 55 + throw new SyntaxError(`invalid collection in at-uri: ${c}`); 56 } 57 58 if (k !== undefined && !isRecordKey(k)) { 59 + throw new SyntaxError(`invalid rkey in at-uri: ${k}`); 60 } 61 62 + return { repo: r, collection: c, rkey: k, fragment: f }; 63 }; 64 65 /** ··· 95 }; 96 97 // #__NO_SIDE_EFFECTS__ 98 + export const parseCanonicalResourceUri = (input: string): ParsedCanonicalResourceUri => { 99 const match = ATURI_RE.exec(input); 100 if (match === null) { 101 + throw new SyntaxError(`invalid canonical-at-uri: ${input}`); 102 } 103 104 const [, r, c, k, f] = match; 105 106 if (!isDid(r)) { 107 + throw new SyntaxError(`invalid repo in canonical-at-uri: ${r}`); 108 } 109 110 if (!isNsid(c)) { 111 + throw new SyntaxError(`invalid collection in canonical-at-uri: ${c}`); 112 } 113 114 if (!isRecordKey(k)) { 115 + throw new SyntaxError(`invalid rkey in canonical-at-uri: ${k}`); 116 } 117 118 + return { repo: r, collection: c, rkey: k, fragment: f }; 119 };
-2
packages/lexicons/lexicons/lib/utils.ts
··· 1 import { DEV } from 'esm-env'; 2 3 - export type Result<T, E> = { ok: true; value: T } | { ok: false; error: E }; 4 - 5 export const assert: { (condition: any, message?: string): asserts condition } = (condition, message) => { 6 if (!condition) { 7 if (DEV) {
··· 1 import { DEV } from 'esm-env'; 2 3 export const assert: { (condition: any, message?: string): asserts condition } = (condition, message) => { 4 if (!condition) { 5 if (DEV) {