@atproto/lexicon 0.0.1

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.
Files changed (42) hide show
  1. package/README.md +31 -0
  2. package/build.js +22 -0
  3. package/dist/index.d.ts +126 -0
  4. package/dist/index.js +3897 -0
  5. package/dist/index.js.map +7 -0
  6. package/dist/src/index.d.ts +2 -0
  7. package/dist/src/lexicons.d.ts +15 -0
  8. package/dist/src/record/index.d.ts +4 -0
  9. package/dist/src/record/schema.d.ts +9 -0
  10. package/dist/src/record/schemas.d.ts +10 -0
  11. package/dist/src/record/util.d.ts +1 -0
  12. package/dist/src/record/validation.d.ts +24 -0
  13. package/dist/src/record/validator.d.ts +17 -0
  14. package/dist/src/record-validator.d.ts +17 -0
  15. package/dist/src/schema.d.ts +9 -0
  16. package/dist/src/schemas.d.ts +10 -0
  17. package/dist/src/types.d.ts +30268 -0
  18. package/dist/src/util.d.ts +6 -0
  19. package/dist/src/validation.d.ts +6 -0
  20. package/dist/src/validators/blob.d.ts +6 -0
  21. package/dist/src/validators/complex.d.ts +5 -0
  22. package/dist/src/validators/primitives.d.ts +9 -0
  23. package/dist/src/validators/xrpc.d.ts +3 -0
  24. package/dist/src/view-validator.d.ts +13 -0
  25. package/dist/tsconfig.build.tsbuildinfo +1 -0
  26. package/dist/types.d.ts +73 -0
  27. package/dist/types.js +35 -0
  28. package/jest.config.js +6 -0
  29. package/package.json +21 -0
  30. package/src/index.ts +2 -0
  31. package/src/lexicons.ts +203 -0
  32. package/src/types.ts +318 -0
  33. package/src/util.ts +107 -0
  34. package/src/validation.ts +48 -0
  35. package/src/validators/blob.ts +57 -0
  36. package/src/validators/complex.ts +152 -0
  37. package/src/validators/primitives.ts +300 -0
  38. package/src/validators/xrpc.ts +50 -0
  39. package/tests/_scaffolds/lexicons.ts +379 -0
  40. package/tests/general.test.ts +611 -0
  41. package/tsconfig.build.json +4 -0
  42. package/tsconfig.json +11 -0
package/src/types.ts ADDED
@@ -0,0 +1,318 @@
1
+ import { z } from 'zod'
2
+ import { NSID } from '@atproto/nsid'
3
+
4
+ // primitives
5
+ // =
6
+
7
+ export const lexBoolean = z.object({
8
+ type: z.literal('boolean'),
9
+ description: z.string().optional(),
10
+ default: z.boolean().optional(),
11
+ const: z.boolean().optional(),
12
+ })
13
+ export type LexBoolean = z.infer<typeof lexBoolean>
14
+
15
+ export const lexNumber = z.object({
16
+ type: z.literal('number'),
17
+ description: z.string().optional(),
18
+ default: z.number().optional(),
19
+ minimum: z.number().optional(),
20
+ maximum: z.number().optional(),
21
+ enum: z.number().array().optional(),
22
+ const: z.number().optional(),
23
+ })
24
+ export type LexNumber = z.infer<typeof lexNumber>
25
+
26
+ export const lexInteger = z.object({
27
+ type: z.literal('integer'),
28
+ description: z.string().optional(),
29
+ default: z.number().int().optional(),
30
+ minimum: z.number().int().optional(),
31
+ maximum: z.number().int().optional(),
32
+ enum: z.number().int().array().optional(),
33
+ const: z.number().int().optional(),
34
+ })
35
+ export type LexInteger = z.infer<typeof lexInteger>
36
+
37
+ export const lexString = z.object({
38
+ type: z.literal('string'),
39
+ description: z.string().optional(),
40
+ default: z.string().optional(),
41
+ minLength: z.number().int().optional(),
42
+ maxLength: z.number().int().optional(),
43
+ enum: z.string().array().optional(),
44
+ const: z.string().optional(),
45
+ knownValues: z.string().array().optional(),
46
+ })
47
+ export type LexString = z.infer<typeof lexString>
48
+
49
+ export const lexDatetime = z.object({
50
+ type: z.literal('datetime'),
51
+ description: z.string().optional(),
52
+ })
53
+ export type LexDatetime = z.infer<typeof lexDatetime>
54
+
55
+ export const lexUnknown = z.object({
56
+ type: z.literal('unknown'),
57
+ description: z.string().optional(),
58
+ })
59
+ export type LexUnknown = z.infer<typeof lexUnknown>
60
+
61
+ export const lexPrimitive = z.union([
62
+ lexBoolean,
63
+ lexNumber,
64
+ lexInteger,
65
+ lexString,
66
+ lexDatetime,
67
+ lexUnknown,
68
+ ])
69
+ export type LexPrimitive = z.infer<typeof lexPrimitive>
70
+
71
+ // references
72
+ // =
73
+
74
+ export const lexRef = z.object({
75
+ type: z.literal('ref'),
76
+ description: z.string().optional(),
77
+ ref: z.string(),
78
+ })
79
+ export type LexRef = z.infer<typeof lexRef>
80
+
81
+ export const lexRefUnion = z.object({
82
+ type: z.literal('union'),
83
+ description: z.string().optional(),
84
+ refs: z.string().array(),
85
+ closed: z.boolean().optional(),
86
+ })
87
+ export type LexRefUnion = z.infer<typeof lexRefUnion>
88
+
89
+ export const lexRefVariant = z.union([lexRef, lexRefUnion])
90
+ export type LexRefVariant = z.infer<typeof lexRefVariant>
91
+
92
+ // blobs
93
+ // =
94
+
95
+ export const lexBlob = z.object({
96
+ type: z.literal('blob'),
97
+ description: z.string().optional(),
98
+ accept: z.string().array().optional(),
99
+ maxSize: z.number().optional(),
100
+ })
101
+ export type LexBlob = z.infer<typeof lexBlob>
102
+
103
+ export const lexImage = z.object({
104
+ type: z.literal('image'),
105
+ description: z.string().optional(),
106
+ accept: z.string().array().optional(),
107
+ maxSize: z.number().optional(),
108
+ maxWidth: z.number().int().optional(),
109
+ maxHeight: z.number().int().optional(),
110
+ })
111
+ export type LexImage = z.infer<typeof lexImage>
112
+
113
+ export const lexVideo = z.object({
114
+ type: z.literal('video'),
115
+ description: z.string().optional(),
116
+ accept: z.string().array().optional(),
117
+ maxSize: z.number().optional(),
118
+ maxWidth: z.number().int().optional(),
119
+ maxHeight: z.number().int().optional(),
120
+ maxLength: z.number().int().optional(),
121
+ })
122
+ export type LexVideo = z.infer<typeof lexVideo>
123
+
124
+ export const lexAudio = z.object({
125
+ type: z.literal('audio'),
126
+ description: z.string().optional(),
127
+ accept: z.string().array().optional(),
128
+ maxSize: z.number().optional(),
129
+ maxLength: z.number().int().optional(),
130
+ })
131
+ export type LexAudio = z.infer<typeof lexAudio>
132
+
133
+ export const lexBlobVariant = z.union([lexBlob, lexImage, lexVideo, lexAudio])
134
+ export type LexBlobVariant = z.infer<typeof lexBlobVariant>
135
+
136
+ // complex types
137
+ // =
138
+
139
+ export const lexArray = z.object({
140
+ type: z.literal('array'),
141
+ description: z.string().optional(),
142
+ items: z.union([lexPrimitive, lexBlobVariant, lexRefVariant]),
143
+ minLength: z.number().int().optional(),
144
+ maxLength: z.number().int().optional(),
145
+ })
146
+ export type LexArray = z.infer<typeof lexArray>
147
+
148
+ export const lexToken = z.object({
149
+ type: z.literal('token'),
150
+ description: z.string().optional(),
151
+ })
152
+ export type LexToken = z.infer<typeof lexToken>
153
+
154
+ export const lexObject = z.object({
155
+ type: z.literal('object'),
156
+ description: z.string().optional(),
157
+ required: z.string().array().optional(),
158
+ properties: z
159
+ .record(z.union([lexRefVariant, lexArray, lexBlobVariant, lexPrimitive]))
160
+ .optional(),
161
+ })
162
+ export type LexObject = z.infer<typeof lexObject>
163
+
164
+ // xrpc
165
+ // =
166
+
167
+ export const lexXrpcParameters = z.object({
168
+ type: z.literal('params'),
169
+ description: z.string().optional(),
170
+ required: z.string().array().optional(),
171
+ properties: z.record(lexPrimitive),
172
+ })
173
+ export type LexXrpcParameters = z.infer<typeof lexXrpcParameters>
174
+
175
+ export const lexXrpcBody = z.object({
176
+ description: z.string().optional(),
177
+ encoding: z.string(),
178
+ schema: z.union([lexRefVariant, lexObject]).optional(),
179
+ })
180
+ export type LexXrpcBody = z.infer<typeof lexXrpcBody>
181
+
182
+ export const lexXrpcError = z.object({
183
+ name: z.string(),
184
+ description: z.string().optional(),
185
+ })
186
+ export type LexXrpcError = z.infer<typeof lexXrpcError>
187
+
188
+ export const lexXrpcQuery = z.object({
189
+ type: z.literal('query'),
190
+ description: z.string().optional(),
191
+ parameters: lexXrpcParameters.optional(),
192
+ output: lexXrpcBody.optional(),
193
+ errors: lexXrpcError.array().optional(),
194
+ })
195
+ export type LexXrpcQuery = z.infer<typeof lexXrpcQuery>
196
+
197
+ export const lexXrpcProcedure = z.object({
198
+ type: z.literal('procedure'),
199
+ description: z.string().optional(),
200
+ parameters: lexXrpcParameters.optional(),
201
+ input: lexXrpcBody.optional(),
202
+ output: lexXrpcBody.optional(),
203
+ errors: lexXrpcError.array().optional(),
204
+ })
205
+ export type LexXrpcProcedure = z.infer<typeof lexXrpcProcedure>
206
+
207
+ // database
208
+ // =
209
+
210
+ export const lexRecord = z.object({
211
+ type: z.literal('record'),
212
+ description: z.string().optional(),
213
+ key: z.string().optional(),
214
+ record: lexObject,
215
+ })
216
+ export type LexRecord = z.infer<typeof lexRecord>
217
+
218
+ // core
219
+ // =
220
+
221
+ export const lexUserType = z.union([
222
+ lexRecord,
223
+
224
+ lexXrpcQuery,
225
+ lexXrpcProcedure,
226
+
227
+ lexBlob,
228
+ lexImage,
229
+ lexVideo,
230
+ lexAudio,
231
+
232
+ lexArray,
233
+ lexToken,
234
+ lexObject,
235
+
236
+ lexBoolean,
237
+ lexNumber,
238
+ lexInteger,
239
+ lexString,
240
+ lexDatetime,
241
+ lexUnknown,
242
+ ])
243
+ export type LexUserType = z.infer<typeof lexUserType>
244
+
245
+ export const lexiconDoc = z
246
+ .object({
247
+ lexicon: z.literal(1),
248
+ id: z.string().refine((v: string) => NSID.isValid(v), {
249
+ message: 'Must be a valid NSID',
250
+ }),
251
+ revision: z.number().optional(),
252
+ description: z.string().optional(),
253
+ defs: z.record(lexUserType),
254
+ })
255
+ .superRefine((doc: LexiconDoc, ctx) => {
256
+ for (const defId in doc.defs) {
257
+ const def = doc.defs[defId]
258
+ if (
259
+ defId !== 'main' &&
260
+ (def.type === 'record' ||
261
+ def.type === 'procedure' ||
262
+ def.type === 'query')
263
+ ) {
264
+ ctx.addIssue({
265
+ code: z.ZodIssueCode.custom,
266
+ message: `Records, procedures, and queries must be the main definition.`,
267
+ })
268
+ }
269
+ }
270
+ })
271
+ export type LexiconDoc = z.infer<typeof lexiconDoc>
272
+
273
+ // helpers
274
+ // =
275
+
276
+ export function isValidLexiconDoc(v: unknown): v is LexiconDoc {
277
+ return lexiconDoc.safeParse(v).success
278
+ }
279
+
280
+ export function isObj(obj: unknown): obj is Record<string, unknown> {
281
+ return !!obj && typeof obj === 'object'
282
+ }
283
+
284
+ export function hasProp<K extends PropertyKey>(
285
+ data: object,
286
+ prop: K,
287
+ ): data is Record<K, unknown> {
288
+ return prop in data
289
+ }
290
+
291
+ export const discriminatedObject = z.object({ $type: z.string() })
292
+ export type DiscriminatedObject = z.infer<typeof discriminatedObject>
293
+ export function isDiscriminatedObject(
294
+ value: unknown,
295
+ ): value is DiscriminatedObject {
296
+ return discriminatedObject.safeParse(value).success
297
+ }
298
+
299
+ export class LexiconDocMalformedError extends Error {
300
+ constructor(
301
+ message: string,
302
+ public schemaDef: unknown,
303
+ public issues?: z.ZodIssue[],
304
+ ) {
305
+ super(message)
306
+ this.schemaDef = schemaDef
307
+ this.issues = issues
308
+ }
309
+ }
310
+
311
+ export interface ValidationResult {
312
+ success: boolean
313
+ error?: ValidationError
314
+ }
315
+
316
+ export class ValidationError extends Error {}
317
+ export class InvalidLexiconError extends Error {}
318
+ export class LexiconDefNotFoundError extends Error {}
package/src/util.ts ADDED
@@ -0,0 +1,107 @@
1
+ import { Lexicons } from './lexicons'
2
+ import * as ComplexValidators from './validators/complex'
3
+ import {
4
+ LexUserType,
5
+ LexRefVariant,
6
+ ValidationError,
7
+ ValidationResult,
8
+ isDiscriminatedObject,
9
+ } from './types'
10
+
11
+ export function toLexUri(str: string, baseUri?: string): string {
12
+ if (str.startsWith('lex:')) {
13
+ return str
14
+ }
15
+ if (str.startsWith('#')) {
16
+ if (!baseUri) {
17
+ throw new Error(`Unable to resolve uri without anchor: ${str}`)
18
+ }
19
+ return `${baseUri}${str}`
20
+ }
21
+ return `lex:${str}`
22
+ }
23
+
24
+ export function validateOneOf(
25
+ lexicons: Lexicons,
26
+ path: string,
27
+ def: LexRefVariant | LexUserType,
28
+ value: unknown,
29
+ mustBeObj = false, // this is the only type constraint we need currently (used by xrpc body schema validators)
30
+ ): ValidationResult {
31
+ let error
32
+
33
+ let concreteDefs
34
+ if (def.type === 'union') {
35
+ if (!isDiscriminatedObject(value)) {
36
+ return {
37
+ success: false,
38
+ error: new ValidationError(
39
+ `${path} must be an object which includes the "$type" property`,
40
+ ),
41
+ }
42
+ }
43
+ if (!def.refs.includes(toLexUri(value.$type))) {
44
+ if (def.closed) {
45
+ return {
46
+ success: false,
47
+ error: new ValidationError(
48
+ `${path} $type must be one of ${def.refs.join(', ')}`,
49
+ ),
50
+ }
51
+ }
52
+ return { success: true }
53
+ } else {
54
+ concreteDefs = toConcreteTypes(lexicons, {
55
+ type: 'ref',
56
+ ref: value.$type,
57
+ })
58
+ }
59
+ } else {
60
+ concreteDefs = toConcreteTypes(lexicons, def)
61
+ }
62
+
63
+ for (const concreteDef of concreteDefs) {
64
+ const result = mustBeObj
65
+ ? ComplexValidators.object(lexicons, path, concreteDef, value)
66
+ : ComplexValidators.validate(lexicons, path, concreteDef, value)
67
+ if (result.success) {
68
+ return result
69
+ }
70
+ error ??= result.error
71
+ }
72
+ if (concreteDefs.length > 1) {
73
+ return {
74
+ success: false,
75
+ error: new ValidationError(
76
+ `${path} did not match any of the expected definitions`,
77
+ ),
78
+ }
79
+ }
80
+ return { success: false, error }
81
+ }
82
+
83
+ export function assertValidOneOf(
84
+ lexicons: Lexicons,
85
+ path: string,
86
+ def: LexRefVariant | LexUserType,
87
+ value: unknown,
88
+ mustBeObj = false,
89
+ ) {
90
+ const res = validateOneOf(lexicons, path, def, value, mustBeObj)
91
+ if (!res.success) {
92
+ throw res.error
93
+ }
94
+ }
95
+
96
+ export function toConcreteTypes(
97
+ lexicons: Lexicons,
98
+ def: LexRefVariant | LexUserType,
99
+ ): LexUserType[] {
100
+ if (def.type === 'ref') {
101
+ return [lexicons.getDefOrThrow(def.ref)]
102
+ } else if (def.type === 'union') {
103
+ return def.refs.map((ref) => lexicons.getDefOrThrow(ref)).flat()
104
+ } else {
105
+ return [def]
106
+ }
107
+ }
@@ -0,0 +1,48 @@
1
+ import { Lexicons } from './lexicons'
2
+ import { LexRecord, LexXrpcProcedure, LexXrpcQuery } from './types'
3
+ import { assertValidOneOf } from './util'
4
+
5
+ import * as ComplexValidators from './validators/complex'
6
+ import * as XrpcValidators from './validators/xrpc'
7
+
8
+ export function assertValidRecord(
9
+ lexicons: Lexicons,
10
+ def: LexRecord,
11
+ value: unknown,
12
+ ) {
13
+ const res = ComplexValidators.object(lexicons, 'Record', def.record, value)
14
+ if (!res.success) throw res.error
15
+ }
16
+
17
+ export function assertValidXrpcParams(
18
+ lexicons: Lexicons,
19
+ def: LexXrpcProcedure | LexXrpcQuery,
20
+ value: unknown,
21
+ ) {
22
+ if (def.parameters) {
23
+ const res = XrpcValidators.params(lexicons, 'Params', def.parameters, value)
24
+ if (!res.success) throw res.error
25
+ }
26
+ }
27
+
28
+ export function assertValidXrpcInput(
29
+ lexicons: Lexicons,
30
+ def: LexXrpcProcedure,
31
+ value: unknown,
32
+ ) {
33
+ if (def.input?.schema) {
34
+ // loop: all input schema definitions
35
+ assertValidOneOf(lexicons, 'Input', def.input.schema, value, true)
36
+ }
37
+ }
38
+
39
+ export function assertValidXrpcOutput(
40
+ lexicons: Lexicons,
41
+ def: LexXrpcProcedure | LexXrpcQuery,
42
+ value: unknown,
43
+ ) {
44
+ if (def.output?.schema) {
45
+ // loop: all output schema definitions
46
+ assertValidOneOf(lexicons, 'Output', def.output.schema, value, true)
47
+ }
48
+ }
@@ -0,0 +1,57 @@
1
+ import { Lexicons } from '../lexicons'
2
+ import { LexUserType, ValidationResult, ValidationError } from '../types'
3
+ import { isObj, hasProp } from '../types'
4
+
5
+ export function blob(
6
+ lexicons: Lexicons,
7
+ path: string,
8
+ def: LexUserType,
9
+ value: unknown,
10
+ ): ValidationResult {
11
+ if (!isObj(value)) {
12
+ return {
13
+ success: false,
14
+ error: new ValidationError(`${path} should be an object`),
15
+ }
16
+ }
17
+ if (!hasProp(value, 'cid') || typeof value.cid !== 'string') {
18
+ return {
19
+ success: false,
20
+ error: new ValidationError(`${path}/cid should be a string`),
21
+ }
22
+ }
23
+ if (!hasProp(value, 'mimeType') || typeof value.mimeType !== 'string') {
24
+ return {
25
+ success: false,
26
+ error: new ValidationError(`${path}/mimeType should be a string`),
27
+ }
28
+ }
29
+ return { success: true }
30
+ }
31
+
32
+ export function image(
33
+ lexicons: Lexicons,
34
+ path: string,
35
+ def: LexUserType,
36
+ value: unknown,
37
+ ): ValidationResult {
38
+ return blob(lexicons, path, def, value)
39
+ }
40
+
41
+ export function video(
42
+ lexicons: Lexicons,
43
+ path: string,
44
+ def: LexUserType,
45
+ value: unknown,
46
+ ): ValidationResult {
47
+ return blob(lexicons, path, def, value)
48
+ }
49
+
50
+ export function audio(
51
+ lexicons: Lexicons,
52
+ path: string,
53
+ def: LexUserType,
54
+ value: unknown,
55
+ ): ValidationResult {
56
+ return blob(lexicons, path, def, value)
57
+ }
@@ -0,0 +1,152 @@
1
+ import { Lexicons } from '../lexicons'
2
+ import {
3
+ LexArray,
4
+ LexObject,
5
+ LexUserType,
6
+ ValidationResult,
7
+ ValidationError,
8
+ } from '../types'
9
+ import { validateOneOf } from '../util'
10
+
11
+ import * as Primitives from './primitives'
12
+ import * as Blob from './blob'
13
+
14
+ export function validate(
15
+ lexicons: Lexicons,
16
+ path: string,
17
+ def: LexUserType,
18
+ value: unknown,
19
+ ): ValidationResult {
20
+ switch (def.type) {
21
+ case 'boolean':
22
+ return Primitives.boolean(lexicons, path, def, value)
23
+ case 'number':
24
+ return Primitives.number(lexicons, path, def, value)
25
+ case 'integer':
26
+ return Primitives.integer(lexicons, path, def, value)
27
+ case 'string':
28
+ return Primitives.string(lexicons, path, def, value)
29
+ case 'datetime':
30
+ return Primitives.datetime(lexicons, path, def, value)
31
+ case 'unknown':
32
+ return Primitives.unknown(lexicons, path, def, value)
33
+ case 'object':
34
+ return object(lexicons, path, def, value)
35
+ case 'array':
36
+ return array(lexicons, path, def, value)
37
+ case 'blob':
38
+ return Blob.blob(lexicons, path, def, value)
39
+ case 'image':
40
+ return Blob.image(lexicons, path, def, value)
41
+ case 'video':
42
+ return Blob.video(lexicons, path, def, value)
43
+ case 'audio':
44
+ return Blob.audio(lexicons, path, def, value)
45
+ default:
46
+ return {
47
+ success: false,
48
+ error: new ValidationError(`Unexpected lexicon type: ${def.type}`),
49
+ }
50
+ }
51
+ }
52
+
53
+ export function array(
54
+ lexicons: Lexicons,
55
+ path: string,
56
+ def: LexUserType,
57
+ value: unknown,
58
+ ): ValidationResult {
59
+ def = def as LexArray
60
+
61
+ // type
62
+ if (!Array.isArray(value)) {
63
+ return {
64
+ success: false,
65
+ error: new ValidationError(`${path} must be an array`),
66
+ }
67
+ }
68
+
69
+ // maxLength
70
+ if (typeof def.maxLength === 'number') {
71
+ if ((value as Array<unknown>).length > def.maxLength) {
72
+ return {
73
+ success: false,
74
+ error: new ValidationError(
75
+ `${path} must not have more than ${def.maxLength} elements`,
76
+ ),
77
+ }
78
+ }
79
+ }
80
+
81
+ // minLength
82
+ if (typeof def.minLength === 'number') {
83
+ if ((value as Array<unknown>).length < def.minLength) {
84
+ return {
85
+ success: false,
86
+ error: new ValidationError(
87
+ `${path} must not have fewer than ${def.minLength} elements`,
88
+ ),
89
+ }
90
+ }
91
+ }
92
+
93
+ // items
94
+ const itemsDef = def.items
95
+ for (let i = 0; i < (value as Array<unknown>).length; i++) {
96
+ const itemValue = value[i]
97
+ const itemPath = `${path}/${i}`
98
+ const res = validateOneOf(lexicons, itemPath, itemsDef, itemValue)
99
+ if (!res.success) {
100
+ return res
101
+ }
102
+ }
103
+
104
+ return { success: true }
105
+ }
106
+
107
+ export function object(
108
+ lexicons: Lexicons,
109
+ path: string,
110
+ def: LexUserType,
111
+ value: unknown,
112
+ ): ValidationResult {
113
+ def = def as LexObject
114
+
115
+ // type
116
+ if (!value || typeof value !== 'object') {
117
+ return {
118
+ success: false,
119
+ error: new ValidationError(`${path} must be an object`),
120
+ }
121
+ }
122
+
123
+ // required
124
+ if (Array.isArray(def.required)) {
125
+ for (const key of def.required) {
126
+ if (!(key in value)) {
127
+ return {
128
+ success: false,
129
+ error: new ValidationError(`${path} must have the property "${key}"`),
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ // properties
136
+ if (typeof def.properties === 'object') {
137
+ for (const key in def.properties) {
138
+ const propValue = value[key]
139
+ if (typeof propValue === 'undefined') {
140
+ continue // skip- if required, will have already failed
141
+ }
142
+ const propDef = def.properties[key]
143
+ const propPath = `${path}/${key}`
144
+ const res = validateOneOf(lexicons, propPath, propDef, propValue)
145
+ if (!res.success) {
146
+ return res
147
+ }
148
+ }
149
+ }
150
+
151
+ return { success: true }
152
+ }