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