@atproto/lexicon 0.4.6 → 0.4.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/lexicon",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "license": "MIT",
5
5
  "description": "atproto Lexicon schema language library",
6
6
  "keywords": [
@@ -20,7 +20,7 @@
20
20
  "multiformats": "^9.9.0",
21
21
  "zod": "^3.23.8",
22
22
  "@atproto/common-web": "^0.4.0",
23
- "@atproto/syntax": "^0.3.2"
23
+ "@atproto/syntax": "^0.3.4"
24
24
  },
25
25
  "devDependencies": {
26
26
  "jest": "^28.1.2",
package/src/lexicons.ts CHANGED
@@ -6,7 +6,6 @@ import {
6
6
  LexiconDoc,
7
7
  ValidationError,
8
8
  ValidationResult,
9
- hasProp,
10
9
  isObj,
11
10
  } from './types'
12
11
  import { toLexUri } from './util'
@@ -17,7 +16,7 @@ import {
17
16
  assertValidXrpcOutput,
18
17
  assertValidXrpcParams,
19
18
  } from './validation'
20
- import * as ComplexValidators from './validators/complex'
19
+ import { object as validateObject } from './validators/complex'
21
20
 
22
21
  /**
23
22
  * A collection of compiled lexicons.
@@ -127,15 +126,17 @@ export class Lexicons implements Iterable<LexiconDoc> {
127
126
  * Validate a record or object.
128
127
  */
129
128
  validate(lexUri: string, value: unknown): ValidationResult {
130
- lexUri = toLexUri(lexUri)
131
- const def = this.getDefOrThrow(lexUri, ['record', 'object'])
132
129
  if (!isObj(value)) {
133
130
  throw new ValidationError(`Value must be an object`)
134
131
  }
132
+
133
+ const lexUriNormalized = toLexUri(lexUri)
134
+ const def = this.getDefOrThrow(lexUriNormalized, ['record', 'object'])
135
+
135
136
  if (def.type === 'record') {
136
- return ComplexValidators.object(this, 'Record', def.record, value)
137
+ return validateObject(this, 'Record', def.record, value)
137
138
  } else if (def.type === 'object') {
138
- return ComplexValidators.object(this, 'Object', def, value)
139
+ return validateObject(this, 'Object', def, value)
139
140
  } else {
140
141
  // shouldn't happen
141
142
  throw new InvalidLexiconError('Definition must be a record or object')
@@ -146,20 +147,25 @@ export class Lexicons implements Iterable<LexiconDoc> {
146
147
  * Validate a record and throw on any error.
147
148
  */
148
149
  assertValidRecord(lexUri: string, value: unknown) {
149
- lexUri = toLexUri(lexUri)
150
- const def = this.getDefOrThrow(lexUri, ['record'])
151
150
  if (!isObj(value)) {
152
151
  throw new ValidationError(`Record must be an object`)
153
152
  }
154
- if (!hasProp(value, '$type') || typeof value.$type !== 'string') {
153
+ if (!('$type' in value)) {
155
154
  throw new ValidationError(`Record/$type must be a string`)
156
155
  }
157
- const $type = (value as Record<string, string>).$type || ''
158
- if (toLexUri($type) !== lexUri) {
156
+ const { $type } = value
157
+ if (typeof $type !== 'string') {
158
+ throw new ValidationError(`Record/$type must be a string`)
159
+ }
160
+
161
+ const lexUriNormalized = toLexUri(lexUri)
162
+ if (toLexUri($type) !== lexUriNormalized) {
159
163
  throw new ValidationError(
160
- `Invalid $type: must be ${lexUri}, got ${$type}`,
164
+ `Invalid $type: must be ${lexUriNormalized}, got ${$type}`,
161
165
  )
162
166
  }
167
+
168
+ const def = this.getDefOrThrow(lexUriNormalized, ['record'])
163
169
  return assertValidRecord(this, def as LexRecord, value)
164
170
  }
165
171
 
package/src/types.ts CHANGED
@@ -447,23 +447,13 @@ export function isValidLexiconDoc(v: unknown): v is LexiconDoc {
447
447
  return lexiconDoc.safeParse(v).success
448
448
  }
449
449
 
450
- export function isObj(obj: unknown): obj is Record<string, unknown> {
451
- return obj !== null && typeof obj === 'object'
450
+ export function isObj<V>(v: V): v is V & object {
451
+ return v != null && typeof v === 'object'
452
452
  }
453
453
 
454
- export function hasProp<K extends PropertyKey>(
455
- data: object,
456
- prop: K,
457
- ): data is Record<K, unknown> {
458
- return prop in data
459
- }
460
-
461
- export const discriminatedObject = z.object({ $type: z.string() })
462
- export type DiscriminatedObject = z.infer<typeof discriminatedObject>
463
- export function isDiscriminatedObject(
464
- value: unknown,
465
- ): value is DiscriminatedObject {
466
- return discriminatedObject.safeParse(value).success
454
+ export type DiscriminatedObject = { $type: string }
455
+ export function isDiscriminatedObject(v: unknown): v is DiscriminatedObject {
456
+ return isObj(v) && '$type' in v && typeof v.$type === 'string'
467
457
  }
468
458
 
469
459
  export function parseLexiconDoc(v: unknown): LexiconDoc {
@@ -471,10 +461,10 @@ export function parseLexiconDoc(v: unknown): LexiconDoc {
471
461
  return v as LexiconDoc
472
462
  }
473
463
 
474
- export type ValidationResult =
464
+ export type ValidationResult<V = unknown> =
475
465
  | {
476
466
  success: true
477
- value: unknown
467
+ value: V
478
468
  }
479
469
  | {
480
470
  success: false
package/src/util.ts CHANGED
@@ -1,6 +1,4 @@
1
1
  import { z } from 'zod'
2
- import { Lexicons } from './lexicons'
3
- import { LexRefVariant, LexUserType } from './types'
4
2
 
5
3
  export function toLexUri(str: string, baseUri?: string): string {
6
4
  if (str.split('#').length > 2) {
@@ -19,19 +17,6 @@ export function toLexUri(str: string, baseUri?: string): string {
19
17
  return `lex:${str}`
20
18
  }
21
19
 
22
- export function toConcreteTypes(
23
- lexicons: Lexicons,
24
- def: LexRefVariant | LexUserType,
25
- ): LexUserType[] {
26
- if (def.type === 'ref') {
27
- return [lexicons.getDefOrThrow(def.ref)]
28
- } else if (def.type === 'union') {
29
- return def.refs.map((ref) => lexicons.getDefOrThrow(ref)).flat()
30
- } else {
31
- return [def]
32
- }
33
- }
34
-
35
20
  export function requiredPropertiesRefinement<
36
21
  ObjectType extends {
37
22
  required?: string[]
@@ -1,16 +1,16 @@
1
1
  import { Lexicons } from '../lexicons'
2
2
  import {
3
3
  LexArray,
4
- LexObject,
5
4
  LexRefVariant,
6
5
  LexUserType,
7
6
  ValidationError,
8
7
  ValidationResult,
9
8
  isDiscriminatedObject,
9
+ isObj,
10
10
  } from '../types'
11
- import { toConcreteTypes, toLexUri } from '../util'
11
+ import { toLexUri } from '../util'
12
12
  import { blob } from './blob'
13
- import { boolean, bytes, cidLink, integer, string, unknown } from './primitives'
13
+ import { validate as validatePrimitive } from './primitives'
14
14
 
15
15
  export function validate(
16
16
  lexicons: Lexicons,
@@ -19,18 +19,6 @@ export function validate(
19
19
  value: unknown,
20
20
  ): ValidationResult {
21
21
  switch (def.type) {
22
- case 'boolean':
23
- return boolean(lexicons, path, def, value)
24
- case 'integer':
25
- return integer(lexicons, path, def, value)
26
- case 'string':
27
- return string(lexicons, path, def, value)
28
- case 'bytes':
29
- return bytes(lexicons, path, def, value)
30
- case 'cid-link':
31
- return cidLink(lexicons, path, def, value)
32
- case 'unknown':
33
- return unknown(lexicons, path, def, value)
34
22
  case 'object':
35
23
  return object(lexicons, path, def, value)
36
24
  case 'array':
@@ -38,10 +26,7 @@ export function validate(
38
26
  case 'blob':
39
27
  return blob(lexicons, path, def, value)
40
28
  default:
41
- return {
42
- success: false,
43
- error: new ValidationError(`Unexpected lexicon type: ${def.type}`),
44
- }
29
+ return validatePrimitive(lexicons, path, def, value)
45
30
  }
46
31
  }
47
32
 
@@ -103,35 +88,31 @@ export function object(
103
88
  def: LexUserType,
104
89
  value: unknown,
105
90
  ): ValidationResult {
106
- def = def as LexObject
107
-
108
91
  // type
109
- if (!value || typeof value !== 'object') {
92
+ if (!isObj(value)) {
110
93
  return {
111
94
  success: false,
112
95
  error: new ValidationError(`${path} must be an object`),
113
96
  }
114
97
  }
115
98
 
116
- const requiredProps = new Set(def.required)
117
- const nullableProps = new Set(def.nullable)
118
-
119
99
  // properties
120
100
  let resultValue = value
121
- if (typeof def.properties === 'object') {
101
+ if ('properties' in def && def.properties != null) {
122
102
  for (const key in def.properties) {
123
- if (value[key] === null && nullableProps.has(key)) {
103
+ const keyValue = value[key]
104
+ if (keyValue === null && def.nullable?.includes(key)) {
124
105
  continue
125
106
  }
126
107
  const propDef = def.properties[key]
127
- if (typeof value[key] === 'undefined' && !requiredProps.has(key)) {
108
+ if (keyValue === undefined && !def.required?.includes(key)) {
128
109
  // Fast path for non-required undefined props.
129
110
  if (
130
111
  propDef.type === 'integer' ||
131
112
  propDef.type === 'boolean' ||
132
113
  propDef.type === 'string'
133
114
  ) {
134
- if (typeof propDef.default === 'undefined') {
115
+ if (propDef.default === undefined) {
135
116
  continue
136
117
  }
137
118
  } else {
@@ -140,20 +121,27 @@ export function object(
140
121
  }
141
122
  }
142
123
  const propPath = `${path}/${key}`
143
- const validated = validateOneOf(lexicons, propPath, propDef, value[key])
144
- const propValue = validated.success ? validated.value : value[key]
145
- const propIsUndefined = typeof propValue === 'undefined'
124
+ const validated = validateOneOf(lexicons, propPath, propDef, keyValue)
125
+ const propValue = validated.success ? validated.value : keyValue
126
+
146
127
  // Return error for bad validation, giving required rule precedence
147
- if (propIsUndefined && requiredProps.has(key)) {
148
- return {
149
- success: false,
150
- error: new ValidationError(`${path} must have the property "${key}"`),
128
+ if (propValue === undefined) {
129
+ if (def.required?.includes(key)) {
130
+ return {
131
+ success: false,
132
+ error: new ValidationError(
133
+ `${path} must have the property "${key}"`,
134
+ ),
135
+ }
136
+ }
137
+ } else {
138
+ if (!validated.success) {
139
+ return validated
151
140
  }
152
- } else if (!propIsUndefined && !validated.success) {
153
- return validated
154
141
  }
142
+
155
143
  // Adjust value based on e.g. applied defaults, cloning shallowly if there was a changed value
156
- if (propValue !== value[key]) {
144
+ if (propValue !== keyValue) {
157
145
  if (resultValue === value) {
158
146
  // Lazy shallow clone
159
147
  resultValue = { ...value }
@@ -173,9 +161,8 @@ export function validateOneOf(
173
161
  value: unknown,
174
162
  mustBeObj = false, // this is the only type constraint we need currently (used by xrpc body schema validators)
175
163
  ): ValidationResult {
176
- let error
164
+ let concreteDef: LexUserType
177
165
 
178
- let concreteDefs
179
166
  if (def.type === 'union') {
180
167
  if (!isDiscriminatedObject(value)) {
181
168
  return {
@@ -196,33 +183,17 @@ export function validateOneOf(
196
183
  }
197
184
  return { success: true, value }
198
185
  } else {
199
- concreteDefs = toConcreteTypes(lexicons, {
200
- type: 'ref',
201
- ref: value.$type,
202
- })
186
+ concreteDef = lexicons.getDefOrThrow(value.$type)
203
187
  }
188
+ } else if (def.type === 'ref') {
189
+ concreteDef = lexicons.getDefOrThrow(def.ref)
204
190
  } else {
205
- concreteDefs = toConcreteTypes(lexicons, def)
191
+ concreteDef = def
206
192
  }
207
193
 
208
- for (const concreteDef of concreteDefs) {
209
- const result = mustBeObj
210
- ? object(lexicons, path, concreteDef, value)
211
- : validate(lexicons, path, concreteDef, value)
212
- if (result.success) {
213
- return result
214
- }
215
- error ??= result.error
216
- }
217
- if (concreteDefs.length > 1) {
218
- return {
219
- success: false,
220
- error: new ValidationError(
221
- `${path} did not match any of the expected definitions`,
222
- ),
223
- }
224
- }
225
- return { success: false, error }
194
+ return mustBeObj
195
+ ? object(lexicons, path, concreteDef, value)
196
+ : validate(lexicons, path, concreteDef, value)
226
197
  }
227
198
 
228
199
  // to avoid bugs like #0189 this needs to handle both
@@ -234,8 +205,8 @@ const refsContainType = (refs: string[], type: string) => {
234
205
  }
235
206
 
236
207
  if (lexUri.endsWith('#main')) {
237
- return refs.includes(lexUri.replace('#main', ''))
208
+ return refs.includes(lexUri.slice(0, -5))
238
209
  } else {
239
- return refs.includes(lexUri + '#main')
210
+ return !lexUri.includes('#') && refs.includes(`${lexUri}#main`)
240
211
  }
241
212
  }
@@ -7,7 +7,7 @@ import {
7
7
  ensureValidHandle,
8
8
  ensureValidNsid,
9
9
  ensureValidRecordKey,
10
- ensureValidTid,
10
+ isValidTid,
11
11
  } from '@atproto/syntax'
12
12
  import { ValidationError, ValidationResult } from '../types'
13
13
 
@@ -131,17 +131,14 @@ export function language(path: string, value: string): ValidationResult {
131
131
  }
132
132
 
133
133
  export function tid(path: string, value: string): ValidationResult {
134
- try {
135
- ensureValidTid(value)
136
- } catch {
137
- return {
138
- success: false,
139
- error: new ValidationError(
140
- `${path} must be a valid TID (timestamp identifier)`,
141
- ),
142
- }
134
+ if (isValidTid(value)) {
135
+ return { success: true, value }
136
+ }
137
+
138
+ return {
139
+ success: false,
140
+ error: new ValidationError(`${path} must be a valid TID`),
143
141
  }
144
- return { success: true, value }
145
142
  }
146
143
 
147
144
  export function recordKey(path: string, value: string): ValidationResult {
@@ -39,7 +39,7 @@ export function validate(
39
39
  }
40
40
  }
41
41
 
42
- export function boolean(
42
+ function boolean(
43
43
  lexicons: Lexicons,
44
44
  path: string,
45
45
  def: LexUserType,
@@ -77,7 +77,7 @@ export function boolean(
77
77
  return { success: true, value }
78
78
  }
79
79
 
80
- export function integer(
80
+ function integer(
81
81
  lexicons: Lexicons,
82
82
  path: string,
83
83
  def: LexUserType,
@@ -151,7 +151,7 @@ export function integer(
151
151
  return { success: true, value }
152
152
  }
153
153
 
154
- export function string(
154
+ function string(
155
155
  lexicons: Lexicons,
156
156
  path: string,
157
157
  def: LexUserType,
@@ -340,7 +340,7 @@ export function string(
340
340
  return { success: true, value }
341
341
  }
342
342
 
343
- export function bytes(
343
+ function bytes(
344
344
  lexicons: Lexicons,
345
345
  path: string,
346
346
  def: LexUserType,
@@ -382,7 +382,7 @@ export function bytes(
382
382
  return { success: true, value }
383
383
  }
384
384
 
385
- export function cidLink(
385
+ function cidLink(
386
386
  lexicons: Lexicons,
387
387
  path: string,
388
388
  def: LexUserType,
@@ -398,7 +398,7 @@ export function cidLink(
398
398
  return { success: true, value }
399
399
  }
400
400
 
401
- export function unknown(
401
+ function unknown(
402
402
  lexicons: Lexicons,
403
403
  path: string,
404
404
  def: LexUserType,