@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/CHANGELOG.md +18 -0
- package/LICENSE.txt +1 -1
- package/dist/lexicons.d.ts.map +1 -1
- package/dist/lexicons.js +14 -34
- package/dist/lexicons.js.map +1 -1
- package/dist/types.d.ts +993 -999
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -10
- package/dist/types.js.map +1 -1
- package/dist/util.d.ts +0 -3
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +0 -12
- package/dist/util.js.map +1 -1
- package/dist/validators/complex.d.ts.map +1 -1
- package/dist/validators/complex.js +32 -60
- package/dist/validators/complex.js.map +1 -1
- package/dist/validators/formats.d.ts.map +1 -1
- package/dist/validators/formats.js +6 -9
- package/dist/validators/formats.js.map +1 -1
- package/dist/validators/primitives.d.ts +0 -6
- package/dist/validators/primitives.d.ts.map +1 -1
- package/dist/validators/primitives.js +0 -6
- package/dist/validators/primitives.js.map +1 -1
- package/jest.config.js +1 -0
- package/package.json +2 -2
- package/src/lexicons.ts +18 -12
- package/src/types.ts +7 -17
- package/src/util.ts +0 -15
- package/src/validators/complex.ts +37 -66
- package/src/validators/formats.ts +8 -11
- package/src/validators/primitives.ts +6 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/lexicon",
|
|
3
|
-
"version": "0.4.
|
|
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.
|
|
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
|
|
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
|
|
137
|
+
return validateObject(this, 'Record', def.record, value)
|
|
137
138
|
} else if (def.type === 'object') {
|
|
138
|
-
return
|
|
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 (!
|
|
153
|
+
if (!('$type' in value)) {
|
|
155
154
|
throw new ValidationError(`Record/$type must be a string`)
|
|
156
155
|
}
|
|
157
|
-
const $type =
|
|
158
|
-
if (
|
|
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 ${
|
|
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(
|
|
451
|
-
return
|
|
450
|
+
export function isObj<V>(v: V): v is V & object {
|
|
451
|
+
return v != null && typeof v === 'object'
|
|
452
452
|
}
|
|
453
453
|
|
|
454
|
-
export
|
|
455
|
-
|
|
456
|
-
|
|
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:
|
|
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 {
|
|
11
|
+
import { toLexUri } from '../util'
|
|
12
12
|
import { blob } from './blob'
|
|
13
|
-
import {
|
|
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
|
|
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 (
|
|
101
|
+
if ('properties' in def && def.properties != null) {
|
|
122
102
|
for (const key in def.properties) {
|
|
123
|
-
|
|
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 (
|
|
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 (
|
|
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,
|
|
144
|
-
const propValue = validated.success ? validated.value :
|
|
145
|
-
|
|
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 (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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 !==
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
191
|
+
concreteDef = def
|
|
206
192
|
}
|
|
207
193
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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.
|
|
208
|
+
return refs.includes(lexUri.slice(0, -5))
|
|
238
209
|
} else {
|
|
239
|
-
return refs.includes(lexUri
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
401
|
+
function unknown(
|
|
402
402
|
lexicons: Lexicons,
|
|
403
403
|
path: string,
|
|
404
404
|
def: LexUserType,
|