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/README.md +153 -34
- package/dist/dts-generator.js +166 -0
- package/dist/dts-generator.js.map +6 -0
- package/dist/dts-generator.mjs +156 -0
- package/dist/dts-generator.mjs.map +6 -0
- package/dist/{index.cjs → index.js} +122 -120
- package/dist/index.js.map +6 -0
- package/dist/index.mjs +117 -119
- package/dist/index.mjs.map +1 -1
- package/dts-generator.d.ts +13 -0
- package/{dist/index.d.ts → index.d.ts} +72 -12
- package/package.json +19 -9
- package/src/dts-generator.ts +274 -0
- package/src/index.ts +4 -4
- package/src/types.ts +9 -0
- package/src/validators/array.ts +11 -9
- package/src/validators/number.ts +52 -20
- package/src/validators/object.ts +65 -46
- package/src/validators/string.ts +23 -9
- package/src/validators/tuple.ts +17 -13
- package/dist/index.cjs.map +0 -6
package/package.json
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "justus",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "A JavaScript validation library, with types!",
|
|
5
|
-
"main": "dist/index.
|
|
6
|
-
"
|
|
7
|
-
"
|
|
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.
|
|
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.
|
|
21
|
-
"@typescript-eslint/parser": "^5.
|
|
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.
|
|
24
|
-
"eslint": "^8.
|
|
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
|
+
}
|
package/src/validators/array.ts
CHANGED
|
@@ -20,9 +20,16 @@ export interface ArrayConstraints<V extends Validation> {
|
|
|
20
20
|
items?: V,
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
/**
|
|
24
|
-
|
|
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
|
|
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
|
|
package/src/validators/number.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
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 (
|
|
103
|
+
} else if (decimals <= PRECISION) {
|
|
73
104
|
// We have some "decimal" part (max 6 decimal digits), multiply...
|
|
74
|
-
this.#isMultipleOf = (value): boolean =>
|
|
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
|
|
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
|
|
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
|
package/src/validators/object.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
|
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
|
|
54
|
+
this.properties.set(key, undefined)
|
|
31
55
|
} else if (isModifier(definition)) {
|
|
32
|
-
(
|
|
56
|
+
this.properties.set(key, {
|
|
57
|
+
validator: definition[modifierValidator],
|
|
58
|
+
readonly: definition.readonly,
|
|
59
|
+
optional: definition.optional,
|
|
60
|
+
})
|
|
33
61
|
} else {
|
|
34
|
-
this
|
|
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,
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|