justus 0.0.1 → 0.0.2

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,10 +1,20 @@
1
1
  {
2
2
  "name": "justus",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "A JavaScript validation library, with types!",
5
- "main": "dist/index.cjs",
6
- "types": "dist/index.d.ts",
7
- "module": "dist/index.mjs",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.mjs",
10
+ "require": "./dist/index.js"
11
+ },
12
+ "./dts-generator": {
13
+ "import": "./dist/dts-generator.mjs",
14
+ "require": "./dist/dts-generator.js"
15
+ }
16
+ },
17
+ "types": "./index.d.ts",
8
18
  "scripts": {
9
19
  "build": "build.sh",
10
20
  "dev": "nodemon -e ts -x build.sh -w src -w test -w test-d",
@@ -14,14 +24,14 @@
14
24
  "author": "Juit Developers <developers@juit.com>",
15
25
  "license": "MIT",
16
26
  "devDependencies": {
17
- "@microsoft/api-extractor": "^7.18.19",
27
+ "@microsoft/api-extractor": "^7.18.21",
18
28
  "@types/chai": "^4.2.22",
19
29
  "@types/mocha": "^9.0.0",
20
- "@typescript-eslint/eslint-plugin": "^5.5.0",
21
- "@typescript-eslint/parser": "^5.5.0",
30
+ "@typescript-eslint/eslint-plugin": "^5.6.0",
31
+ "@typescript-eslint/parser": "^5.6.0",
22
32
  "chai": "^4.3.4",
23
- "esbuild": "^0.14.1",
24
- "eslint": "^8.3.0",
33
+ "esbuild": "^0.14.2",
34
+ "eslint": "^8.4.0",
25
35
  "eslint-config-google": "^0.14.0",
26
36
  "mocha": "^9.1.3",
27
37
  "nodemon": "^2.0.15",
@@ -0,0 +1,274 @@
1
+ import ts from 'typescript'
2
+ import {
3
+ AllOfValidator,
4
+ AnyArrayValidator,
5
+ AnyNumberValidator,
6
+ AnyObjectValidator,
7
+ AnyStringValidator,
8
+ AnyValidator,
9
+ ArrayValidator,
10
+ assertSchema,
11
+ BooleanValidator,
12
+ ConstantValidator,
13
+ DateValidator,
14
+ getValidator,
15
+ NumberValidator,
16
+ ObjectValidator,
17
+ OneOfValidator,
18
+ StringValidator,
19
+ TupleValidator,
20
+ Validation,
21
+ Validator,
22
+ } from './index'
23
+
24
+ /* ========================================================================== *
25
+ * LOCAL TYPES *
26
+ * ========================================================================== */
27
+
28
+ /** A function taking a `Validator` and producing its `TypeNode`. */
29
+ type TypeGenerator<V extends Validator = Validator> = (
30
+ validator: V,
31
+ references: ReadonlyMap<Validator, string>
32
+ ) => ts.TypeNode
33
+
34
+ /** The generic constructor of a `Validator` instance. */
35
+ type ValidatorConstructor<V extends Validator = Validator> = { // <T = any> = {
36
+ new (...args: any[]): V
37
+ }
38
+
39
+ /* ========================================================================== *
40
+ * GENERATE TYPES FOR VALIDATORS *
41
+ * ========================================================================== */
42
+
43
+ /** Registry of all `Validator` constructors and related `TypeGenerator`s. */
44
+ const generators = new Map<Function, TypeGenerator<any>>()
45
+
46
+ /** Register a `TypeGenerator` function for a `Validator`. */
47
+ export function registerTypeGenerator<V extends Validator>(
48
+ validator: ValidatorConstructor<V>,
49
+ generator: TypeGenerator<V>,
50
+ ): void {
51
+ assertSchema(validator.prototype instanceof Validator, 'Not a `Validator` class')
52
+ generators.set(validator, generator)
53
+ }
54
+
55
+ /** Generate typings for the given `Validation`s. */
56
+ export function generateTypes(validations: Record<string, Validation>): string {
57
+ // Create two maps (one mapping "string -> validator" and another mapping
58
+ // "validator -> string"). The first map will serve as our "exports" map,
59
+ // while the second will make sure that any exported validator gets referenced
60
+ // in the generated DTS, rather than being re-generated
61
+ const validators = new Map<string, Validator>()
62
+ const references = new Map<Validator, string>()
63
+
64
+ Object.entries(validations).forEach(([ name, validation ]) => {
65
+ const validator = getValidator(validation)
66
+ // References will be added only once, first one takes precedence!
67
+ if (! references.has(validator)) references.set(validator, name)
68
+ validators.set(name, validator)
69
+ })
70
+
71
+ // Create the array of type alias declarations to be printed and exported
72
+ const types: ts.TypeAliasDeclaration[] = []
73
+ for (const [ name, validator ] of validators.entries()) {
74
+ // Clone our references map, and remove the validator being exported. This
75
+ // will make sure that we don't have any loops in our types
76
+ const referenceable = new Map(references)
77
+ if (referenceable.get(validator) === name) {
78
+ referenceable.delete(validator)
79
+ }
80
+
81
+ // Generate the type of the validator, with our stripped reference table
82
+ const type = generateTypeNode(validator, referenceable)
83
+
84
+ // Create a type alias declaration with the name of the export
85
+ const modifiers = [ ts.factory.createModifier(ts.SyntaxKind.ExportKeyword) ]
86
+ const decl = ts.factory.createTypeAliasDeclaration(undefined, modifiers, name, [], type)
87
+ types.push(decl)
88
+ }
89
+
90
+ // Print out all our type aliases
91
+ return ts.createPrinter().printList(
92
+ ts.ListFormat.SourceFileStatements,
93
+ ts.factory.createNodeArray(types),
94
+ ts.createSourceFile('types.d.ts', '', ts.ScriptTarget.Latest))
95
+ }
96
+
97
+ /* ========================================================================== *
98
+ * TYPE GENERATORS *
99
+ * ========================================================================== */
100
+
101
+ /** Generate a TypeScript `TypeNode` for the given validator instance. */
102
+ function generateTypeNode(
103
+ validator: Validator,
104
+ references: ReadonlyMap<Validator, string>,
105
+ ): ts.TypeNode {
106
+ const reference = references.get(validator)
107
+ if (reference) return ts.factory.createTypeReferenceNode(reference)
108
+
109
+ const generator = generators.get(validator.constructor)
110
+ assertSchema(!! generator, `Type generator for "${validator.constructor.name}" not found`)
111
+ return generator(validator, references)
112
+ }
113
+
114
+ /* ========================================================================== */
115
+
116
+ // Simple nodes
117
+
118
+ const anyType = ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)
119
+ const anyArrayType = ts.factory.createArrayTypeNode(anyType)
120
+ const booleanType = ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword)
121
+ const numberType = ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)
122
+ const neverType = ts.factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword)
123
+ const stringType = ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
124
+ const recordType = ts.factory.createMappedTypeNode(
125
+ undefined, // readonly
126
+ ts.factory.createTypeParameterDeclaration('key', stringType),
127
+ undefined, // name type
128
+ undefined, // question token
129
+ anyType, // type of the mapped key
130
+ undefined) // members
131
+
132
+ // Modifiers and tokens
133
+
134
+ const readonlyKeyword = [ ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword) ]
135
+ const optionalKeyword = ts.factory.createToken(ts.SyntaxKind.QuestionToken)
136
+
137
+
138
+ /* ========================================================================== */
139
+
140
+ // Simple generator returning nodes
141
+
142
+ registerTypeGenerator(AnyValidator, () => anyType)
143
+ registerTypeGenerator(AnyArrayValidator, () => anyArrayType)
144
+ registerTypeGenerator(AnyNumberValidator, () => numberType)
145
+ registerTypeGenerator(AnyObjectValidator, () => recordType)
146
+ registerTypeGenerator(AnyStringValidator, () => stringType)
147
+ registerTypeGenerator(BooleanValidator, () => booleanType)
148
+ registerTypeGenerator(DateValidator, () => ts.factory.createTypeReferenceNode('Date'))
149
+
150
+ /* ========================================================================== */
151
+
152
+ // Complex generator functions...
153
+
154
+ registerTypeGenerator(ArrayValidator, (validator, references) => {
155
+ const itemType = generateTypeNode(validator.items, references)
156
+ return ts.factory.createArrayTypeNode(itemType)
157
+ })
158
+
159
+ registerTypeGenerator(ConstantValidator, (validator) => {
160
+ const literal =
161
+ typeof validator.constant === 'number' ? ts.factory.createNumericLiteral(validator.constant) :
162
+ typeof validator.constant === 'string' ? ts.factory.createStringLiteral(validator.constant) :
163
+ validator.constant === false ? ts.factory.createFalse() :
164
+ validator.constant === true ? ts.factory.createTrue() :
165
+ validator.constant === null ? ts.factory.createNull() :
166
+ undefined
167
+
168
+ assertSchema(!! literal, `Invalid constant "${validator.constant}"`)
169
+ return ts.factory.createLiteralTypeNode(literal)
170
+ })
171
+
172
+ registerTypeGenerator(NumberValidator, (validator: NumberValidator) => {
173
+ if (! validator.brand) return numberType
174
+
175
+ const signature = ts.factory.createPropertySignature(undefined, `__brand_${validator.brand}`, undefined, neverType)
176
+ const literal = ts.factory.createTypeLiteralNode([ signature ])
177
+ return ts.factory.createIntersectionTypeNode([ numberType, literal ])
178
+ })
179
+
180
+ registerTypeGenerator(StringValidator, (validator: StringValidator) => {
181
+ if (! validator.brand) return stringType
182
+
183
+ const signature = ts.factory.createPropertySignature(undefined, `__brand_${validator.brand}`, undefined, neverType)
184
+ const literal = ts.factory.createTypeLiteralNode([ signature ])
185
+ return ts.factory.createIntersectionTypeNode([ stringType, literal ])
186
+ })
187
+
188
+ registerTypeGenerator(TupleValidator, (validator: TupleValidator<any>, references) => {
189
+ const members = validator.members
190
+
191
+ // count how many rest parameters do we have..
192
+ const { count, first, next } =
193
+ members.reduce(({ count, first, next }, { single }, i) => {
194
+ if (! single) {
195
+ if (i < first) first = i
196
+ next = i + 1
197
+ count += 1
198
+ }
199
+ return { count, first, next }
200
+ }, { count: 0, first: members.length, next: -1 })
201
+
202
+ // if we have zero or one rest parameter, things are easy...
203
+ if (count < 2) {
204
+ const types = members.map(({ single, validator }) => {
205
+ const memberType = generateTypeNode(validator, references)
206
+
207
+ if (single) return generateTypeNode(validator, references)
208
+
209
+ const arrayType = ts.factory.createArrayTypeNode(memberType)
210
+ return ts.factory.createRestTypeNode(arrayType)
211
+ })
212
+
213
+ return ts.factory.createTupleTypeNode(types)
214
+ }
215
+
216
+ // We have two or more rest parameters... we need combine everything between
217
+ // the first and the last one in a giant union!
218
+ const before = members.slice(0, first)
219
+ .map(({ validator }) => generateTypeNode(validator, references))
220
+ const types = members.slice(first, next)
221
+ .map(({ validator }) => generateTypeNode(validator, references))
222
+ const after = members.slice(next)
223
+ .map(({ validator }) => generateTypeNode(validator, references))
224
+
225
+ const union = ts.factory.createUnionTypeNode(types)
226
+ const array = ts.factory.createArrayTypeNode(union)
227
+ const rest = ts.factory.createRestTypeNode(array)
228
+
229
+ return ts.factory.createTupleTypeNode([ ...before, rest, ...after ])
230
+ })
231
+
232
+ registerTypeGenerator(AllOfValidator, (validator, references) => {
233
+ const members = validator.validators.map((validator) => generateTypeNode(validator, references))
234
+ return ts.factory.createIntersectionTypeNode(members)
235
+ })
236
+
237
+ registerTypeGenerator(OneOfValidator, (validator, references) => {
238
+ const members = validator.validators.map((validator) => generateTypeNode(validator, references))
239
+ return ts.factory.createUnionTypeNode(members)
240
+ })
241
+
242
+ registerTypeGenerator(ObjectValidator, (validator, references) => {
243
+ const properties: ts.PropertySignature[] = []
244
+
245
+ for (const [ key, property ] of validator.properties.entries()) {
246
+ const { validator, readonly, optional } = property || { optional: true }
247
+ const type = validator ? generateTypeNode(validator, references) : neverType
248
+
249
+ const signature = ts.factory.createPropertySignature(
250
+ readonly ? readonlyKeyword : undefined,
251
+ key,
252
+ optional ? optionalKeyword : undefined,
253
+ type)
254
+
255
+ properties.push(signature)
256
+ }
257
+
258
+ if (validator.additionalProperties) {
259
+ const extra = ts.factory.createMappedTypeNode(
260
+ undefined, // readonly
261
+ ts.factory.createTypeParameterDeclaration('key', stringType),
262
+ undefined, // name type
263
+ undefined, // question token
264
+ generateTypeNode(validator.additionalProperties, references),
265
+ undefined) // members
266
+
267
+ if (properties.length == 0) return extra
268
+
269
+ const type = ts.factory.createTypeLiteralNode(properties)
270
+ return ts.factory.createIntersectionTypeNode([ type, extra ])
271
+ } else {
272
+ return ts.factory.createTypeLiteralNode(properties)
273
+ }
274
+ })
package/src/index.ts CHANGED
@@ -11,13 +11,13 @@ export * from './utilities'
11
11
  // Validators
12
12
  export { allOf, oneOf, AllOfValidator, OneOfValidator } from './validators/union'
13
13
  export { any, AnyValidator } from './validators/any'
14
- export { array, arrayOf, ArrayValidator } from './validators/array'
14
+ export { array, arrayOf, AnyArrayValidator, ArrayValidator } from './validators/array'
15
15
  export { boolean, BooleanValidator } from './validators/boolean'
16
16
  export { constant, ConstantValidator } from './validators/constant'
17
17
  export { date, DateValidator } from './validators/date'
18
- export { number, NumberValidator } from './validators/number'
19
- export { object, ObjectValidator } from './validators/object'
20
- export { string, StringValidator } from './validators/string'
18
+ export { number, AnyNumberValidator, NumberValidator } from './validators/number'
19
+ export { object, AnyObjectValidator, ObjectValidator } from './validators/object'
20
+ export { string, AnyStringValidator, StringValidator } from './validators/string'
21
21
  export { tuple, TupleValidator } from './validators/tuple'
22
22
 
23
23
  /* ========================================================================== *
package/src/types.ts CHANGED
@@ -296,3 +296,12 @@ type InferNever<S extends Schema> =
296
296
  never
297
297
  ] : never
298
298
  }
299
+
300
+ /* ========================================================================== *
301
+ * TYPE BRANDING *
302
+ * ========================================================================== */
303
+
304
+ /** Utility type to infer primitive branding according to a string */
305
+ export type Branding<S extends string> = {
306
+ [ key in keyof { __brand: never } as `__brand_${S}` ] : never
307
+ }
@@ -20,9 +20,16 @@ export interface ArrayConstraints<V extends Validation> {
20
20
  items?: V,
21
21
  }
22
22
 
23
- /**
24
- * A validator for `Array` instances.
25
- */
23
+ /** Basic validator for `Array` instances. */
24
+ export class AnyArrayValidator<T = any> extends Validator<T[]> {
25
+ validate(value: unknown, options: ValidationOptions): T[] {
26
+ void options
27
+ assertValidation(Array.isArray(value), 'Value is not an "array"')
28
+ return [ ...value ]
29
+ }
30
+ }
31
+
32
+ /** A validator for `Array` instances with constraints. */
26
33
  export class ArrayValidator<T> extends Validator<T[]> {
27
34
  readonly maxItems: number
28
35
  readonly minItems: number
@@ -80,12 +87,7 @@ export class ArrayValidator<T> extends Validator<T[]> {
80
87
  }
81
88
  }
82
89
 
83
- const anyArrayValidator = new class extends Validator<any[]> {
84
- validate(value: unknown): any[] {
85
- assertValidation(Array.isArray(value), 'Value is not an "array"')
86
- return value
87
- }
88
- }
90
+ const anyArrayValidator = new AnyArrayValidator()
89
91
 
90
92
  /* -------------------------------------------------------------------------- */
91
93
 
@@ -1,6 +1,23 @@
1
- import { Validator } from '../types'
1
+ import { Branding, Validator } from '../types'
2
2
  import { assertSchema, assertValidation } from '../errors'
3
3
  import { makeTupleRestIterable } from './tuple'
4
+ import { ValidationError } from '..'
5
+
6
+ /* ========================================================================== */
7
+
8
+ const PRECISION = 6 // our default precision, in decimal digits
9
+ const MULTIPLIER = Math.pow(10, PRECISION) // multiplier for precision
10
+
11
+ function countDecimals(n: number): number {
12
+ // match the parts of the exponential form of the number
13
+ const match = n.toExponential().match(/^\d+(\.\d+)?e([+-]\d+)$/)
14
+ if (! match) throw new RangeError(`Can't calculate digits for number "${n}"`)
15
+ // number of digits in the absolute value, minus whatever is the exp
16
+ const digits = ((match[1] || '.').length - 1) - (parseInt(match[2]))
17
+ return digits < 0 ? 0 : digits
18
+ }
19
+
20
+ /* ========================================================================== */
4
21
 
5
22
  /** Constraints to validate a `number` with. */
6
23
  export interface NumberConstraints {
@@ -18,7 +35,22 @@ export interface NumberConstraints {
18
35
  allowNaN?: boolean,
19
36
  }
20
37
 
21
- /** A `Validator` validating `number`s. */
38
+ /** Constraints to validate a `number` with extra branding information. */
39
+ export interface BrandedNumberConstraints<B extends string> extends NumberConstraints {
40
+ /** The _brand_ of the string (will generate a `__brand_${B}` type property */
41
+ brand: B
42
+ }
43
+
44
+ /** A `Validator` validating any `number`. */
45
+ export class AnyNumberValidator extends Validator<number> {
46
+ validate(value: unknown): number {
47
+ assertValidation(typeof value == 'number', 'Value is not a "number"')
48
+ assertValidation(! isNaN(value), 'Number is "NaN"')
49
+ return value
50
+ }
51
+ }
52
+
53
+ /** A `Validator` validating `number`s with constaints. */
22
54
  export class NumberValidator<N extends number = number> extends Validator<N> {
23
55
  #isMultipleOf?: ((value: number) => boolean)
24
56
 
@@ -28,6 +60,7 @@ export class NumberValidator<N extends number = number> extends Validator<N> {
28
60
  readonly maximum: number
29
61
  readonly minimum: number
30
62
  readonly multipleOf?: number
63
+ readonly brand?: string
31
64
 
32
65
  constructor(constraints: NumberConstraints = {}) {
33
66
  super()
@@ -41,6 +74,8 @@ export class NumberValidator<N extends number = number> extends Validator<N> {
41
74
  multipleOf,
42
75
  } = constraints
43
76
 
77
+ if ('brand' in constraints) this.brand = (<any> constraints).brand
78
+
44
79
  assertSchema(maximum >= minimum, `Constraint "minimum" (${minimum}) is greater than "maximum" (${maximum})`)
45
80
 
46
81
  if (exclusiveMaximum !== undefined) {
@@ -60,18 +95,21 @@ export class NumberValidator<N extends number = number> extends Validator<N> {
60
95
 
61
96
  if (multipleOf !== undefined) {
62
97
  assertSchema(multipleOf > 0, `Constraint "multipleOf" (${multipleOf}) must be greater than zero`)
98
+ const decimals = countDecimals(multipleOf)
63
99
 
64
- // Split the multiple of in integer and fraction
65
- const bigMultipleOf = multipleOf * NumberValidator.PRECISION
66
- const bigInteger = bigMultipleOf % NumberValidator.PRECISION
67
- const bigDecimal = bigMultipleOf - Math.trunc(bigMultipleOf)
68
-
69
- if (bigInteger === 0) {
100
+ if (decimals === 0) {
70
101
  // Easy case is when we only have to deal with integers...
71
102
  this.#isMultipleOf = (value): boolean => ! (value % multipleOf)
72
- } else if (bigDecimal === 0) {
103
+ } else if (decimals <= PRECISION) {
73
104
  // We have some "decimal" part (max 6 decimal digits), multiply...
74
- this.#isMultipleOf = (value): boolean => ! ((value * NumberValidator.PRECISION) % bigMultipleOf)
105
+ this.#isMultipleOf = (value): boolean => {
106
+ try {
107
+ if (countDecimals(value) > PRECISION) return false
108
+ return ! ((value * MULTIPLIER) % (multipleOf * MULTIPLIER))
109
+ } catch (error: any) {
110
+ throw new ValidationError(error.message)
111
+ }
112
+ }
75
113
  } else {
76
114
  // Required precision was too much (more than 6 decimal digits)
77
115
  assertSchema(false, `Constraint "multipleOf" (${multipleOf}) requires too much precision`)
@@ -108,20 +146,14 @@ export class NumberValidator<N extends number = number> extends Validator<N> {
108
146
 
109
147
  return value as N
110
148
  }
111
-
112
- static readonly PRECISION = 1000000
113
149
  }
114
150
 
115
- const anyNumberValidator = new class extends Validator<number> {
116
- validate(value: unknown): number {
117
- assertValidation(typeof value == 'number', 'Value is not a "number"')
118
- assertValidation(! isNaN(value), 'Number is "NaN"')
119
- return value
120
- }
121
- }
151
+ const anyNumberValidator = new AnyNumberValidator()
122
152
 
123
153
  function _number(): Validator<number>
124
- function _number<N extends number = number>(constraints?: NumberConstraints): NumberValidator<N>
154
+ function _number(constraints?: NumberConstraints): NumberValidator<number>
155
+ function _number<N extends number>(constraints?: NumberConstraints): NumberValidator<N>
156
+ function _number<B extends string>(constraints: BrandedNumberConstraints<B>): NumberValidator<number & Branding<B>>
125
157
 
126
158
  function _number(constraints?: NumberConstraints): Validator<number> {
127
159
  return constraints ? new NumberValidator(constraints) : anyNumberValidator
@@ -1,4 +1,15 @@
1
- import { additionalValidator, InferSchema, modifierValidator, never, Schema, schemaValidator, ValidationOptions, Validator } from '../types'
1
+ import {
2
+ InferSchema,
3
+ Schema,
4
+ TupleRestParameter,
5
+ ValidationOptions,
6
+ Validator,
7
+ additionalValidator,
8
+ modifierValidator,
9
+ never,
10
+ restValidator,
11
+ schemaValidator,
12
+ } from '../types'
2
13
  import { assertValidation, ValidationErrorBuilder } from '../errors'
3
14
  import { getValidator } from '../utilities'
4
15
  import { isModifier } from '../schema'
@@ -8,30 +19,47 @@ import { makeTupleRestIterable } from './tuple'
8
19
  * OBJECT VALIDATOR *
9
20
  * ========================================================================== */
10
21
 
11
- /** A `Validator` validating `object`s according to a `Schema` */
22
+ type ObjectProperty = {
23
+ validator: Validator,
24
+ readonly?: true,
25
+ optional?: true,
26
+ }
27
+
28
+ /** A `Validator` validating any `object`. */
29
+ export class AnyObjectValidator extends Validator<Record<string, any>> {
30
+ validate(value: unknown): Record<string, any> {
31
+ assertValidation(typeof value == 'object', 'Value is not an "object"')
32
+ assertValidation(value !== null, 'Value is "null"')
33
+ return value
34
+ }
35
+ }
36
+
37
+ /** A `Validator` validating `object`s according to a `Schema`. */
12
38
  export class ObjectValidator<S extends Schema> extends Validator<InferSchema<S>> {
13
39
  readonly schema: Readonly<S>
14
40
 
15
- #additionalProperties?: Validator
16
- #requiredProperties: Record<string, Validator<any>> = {}
17
- #optionalProperties: Record<string, Validator<any>> = {}
18
- #neverProperties: Set<string> = new Set<string>()
41
+ properties = new Map<string, ObjectProperty | undefined>()
42
+ additionalProperties?: Validator
19
43
 
20
44
  constructor(schema: S) {
21
45
  super()
22
46
  const { [additionalValidator]: additional, ...properties } = schema
23
47
 
24
- if (additional) this.#additionalProperties = getValidator(additional)
48
+ if (additional) this.additionalProperties = getValidator(additional)
25
49
 
26
50
  for (const key of Object.keys(properties)) {
27
51
  const definition = properties[key]
28
52
 
29
53
  if (definition === never) {
30
- this.#neverProperties.add(key)
54
+ this.properties.set(key, undefined)
31
55
  } else if (isModifier(definition)) {
32
- (definition.optional ? this.#optionalProperties : this.#requiredProperties)[key] = definition[modifierValidator]
56
+ this.properties.set(key, {
57
+ validator: definition[modifierValidator],
58
+ readonly: definition.readonly,
59
+ optional: definition.optional,
60
+ })
33
61
  } else {
34
- this.#requiredProperties[key] = getValidator(definition)
62
+ this.properties.set(key, { validator: getValidator(definition) })
35
63
  }
36
64
  }
37
65
 
@@ -46,22 +74,24 @@ export class ObjectValidator<S extends Schema> extends Validator<InferSchema<S>>
46
74
  const builder = new ValidationErrorBuilder()
47
75
  const clone: Record<string, any> = {}
48
76
 
49
- for (const [ key, validator ] of Object.entries(this.#requiredProperties)) {
50
- if (record[key] === undefined) {
51
- builder.record('Required property missing', key)
77
+ for (const [ key, property ] of this.properties.entries()) {
78
+ const { validator, optional } = property || {}
79
+
80
+ // no validator? this is "never" (forbidden)
81
+ if (! validator) {
82
+ if (record[key] === undefined) continue
83
+ if (options.stripForbiddenProperties) continue
84
+ builder.record('Forbidden property', key)
52
85
  continue
53
86
  }
54
87
 
55
- try {
56
- clone[key] = validator.validate(record[key], options)
57
- } catch (error) {
58
- builder.record(error, key)
88
+ // no value? might be optional, but definitely not validated
89
+ if (record[key] === undefined) {
90
+ if (! optional) builder.record('Required property missing', key)
91
+ continue
59
92
  }
60
- }
61
-
62
- for (const [ key, validator ] of Object.entries(this.#optionalProperties)) {
63
- if (record[key] === undefined) continue
64
93
 
94
+ // all the rest gets validated normally
65
95
  try {
66
96
  clone[key] = validator.validate(record[key], options)
67
97
  } catch (error) {
@@ -69,19 +99,8 @@ export class ObjectValidator<S extends Schema> extends Validator<InferSchema<S>>
69
99
  }
70
100
  }
71
101
 
72
- for (const key of this.#neverProperties) {
73
- if (record[key] === undefined) continue
74
- if (options.stripForbiddenProperties) continue
75
- builder.record('Forbidden property', key)
76
- }
77
-
78
- const additional = this.#additionalProperties
79
- const additionalKeys = Object.keys(record).filter((k) => {
80
- if (k in this.#requiredProperties) return false
81
- if (k in this.#optionalProperties) return false
82
- if (this.#neverProperties.has(k)) return false
83
- return true
84
- })
102
+ const additionalKeys = Object.keys(record).filter((k) => !this.properties.has(k))
103
+ const additional = this.additionalProperties
85
104
 
86
105
  if (additional) {
87
106
  additionalKeys.forEach((key) => {
@@ -102,23 +121,23 @@ export class ObjectValidator<S extends Schema> extends Validator<InferSchema<S>>
102
121
  }
103
122
  }
104
123
 
105
- /** Validate _any_ `object` */
106
- const anyObjectValidator = new class extends Validator<Record<string, any>> {
107
- validate(value: unknown): Record<string, any> {
108
- assertValidation(typeof value == 'object', 'Value is not an "object"')
109
- assertValidation(value !== null, 'Value is "null"')
110
- return value
111
- }
112
- }
124
+ const anyObjectValidator = new AnyObjectValidator()
113
125
 
114
126
  function _object(): Validator<Record<string, any>>
115
- function _object<S extends Schema>(schema: S): S
127
+ function _object<S extends Schema>(schema: S): S & {
128
+ [Symbol.iterator](): Generator<TupleRestParameter<InferSchema<S>>>
129
+ }
116
130
  function _object(schema?: Schema): Validator<Record<string, any>> | Schema {
117
131
  if (! schema) return anyObjectValidator
118
132
 
119
- return Object.defineProperty(schema, schemaValidator, {
120
- value: new ObjectValidator(schema),
121
- enumerable: false,
133
+ const validator = new ObjectValidator(schema)
134
+ function* iterator(): Generator<TupleRestParameter> {
135
+ yield { [restValidator]: validator }
136
+ }
137
+
138
+ return Object.defineProperties(schema, {
139
+ [schemaValidator]: { value: validator, enumerable: false },
140
+ [Symbol.iterator]: { value: iterator, enumerable: false },
122
141
  })
123
142
  }
124
143