justus 0.0.1 → 0.0.5
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 +158 -0
- package/dist/dts-generator.mjs.map +6 -0
- package/dist/{index.cjs → index.js} +246 -124
- package/dist/index.js.map +6 -0
- package/dist/index.mjs +218 -121
- package/dist/index.mjs.map +2 -2
- package/dts-generator.d.ts +13 -0
- package/{dist/index.d.ts → index.d.ts} +171 -40
- package/package.json +27 -16
- package/src/dts-generator.ts +276 -0
- package/src/errors.ts +3 -3
- package/src/index.ts +32 -8
- package/src/schema.ts +6 -6
- package/src/types.ts +15 -6
- package/src/validators/array.ts +14 -12
- package/src/validators/date.ts +3 -3
- package/src/validators/number.ts +54 -22
- package/src/validators/object.ts +67 -48
- package/src/validators/string.ts +25 -11
- package/src/validators/tuple.ts +17 -13
- package/src/validators/union.ts +3 -3
- package/src/validators/url.ts +140 -0
- package/dist/index.cjs.map +0 -6
|
@@ -0,0 +1,276 @@
|
|
|
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
|
+
URLValidator,
|
|
21
|
+
Validation,
|
|
22
|
+
Validator,
|
|
23
|
+
} from './index'
|
|
24
|
+
|
|
25
|
+
/* ========================================================================== *
|
|
26
|
+
* LOCAL TYPES *
|
|
27
|
+
* ========================================================================== */
|
|
28
|
+
|
|
29
|
+
/** A function taking a `Validator` and producing its `TypeNode`. */
|
|
30
|
+
type TypeGenerator<V extends Validator = Validator> = (
|
|
31
|
+
validator: V,
|
|
32
|
+
references: ReadonlyMap<Validator, string>
|
|
33
|
+
) => ts.TypeNode
|
|
34
|
+
|
|
35
|
+
/** The generic constructor of a `Validator` instance. */
|
|
36
|
+
type ValidatorConstructor<V extends Validator = Validator> = { // <T = any> = {
|
|
37
|
+
new (...args: any[]): V
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* ========================================================================== *
|
|
41
|
+
* GENERATE TYPES FOR VALIDATORS *
|
|
42
|
+
* ========================================================================== */
|
|
43
|
+
|
|
44
|
+
/** Registry of all `Validator` constructors and related `TypeGenerator`s. */
|
|
45
|
+
const generators = new Map<Function, TypeGenerator<any>>()
|
|
46
|
+
|
|
47
|
+
/** Register a `TypeGenerator` function for a `Validator`. */
|
|
48
|
+
export function registerTypeGenerator<V extends Validator>(
|
|
49
|
+
validator: ValidatorConstructor<V>,
|
|
50
|
+
generator: TypeGenerator<V>,
|
|
51
|
+
): void {
|
|
52
|
+
assertSchema(validator.prototype instanceof Validator, 'Not a `Validator` class')
|
|
53
|
+
generators.set(validator, generator)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Generate typings for the given `Validation`s. */
|
|
57
|
+
export function generateTypes(validations: Record<string, Validation>): string {
|
|
58
|
+
// Create two maps (one mapping "string -> validator" and another mapping
|
|
59
|
+
// "validator -> string"). The first map will serve as our "exports" map,
|
|
60
|
+
// while the second will make sure that any exported validator gets referenced
|
|
61
|
+
// in the generated DTS, rather than being re-generated
|
|
62
|
+
const validators = new Map<string, Validator>()
|
|
63
|
+
const references = new Map<Validator, string>()
|
|
64
|
+
|
|
65
|
+
Object.entries(validations).forEach(([ name, validation ]) => {
|
|
66
|
+
const validator = getValidator(validation)
|
|
67
|
+
// References will be added only once, first one takes precedence!
|
|
68
|
+
if (! references.has(validator)) references.set(validator, name)
|
|
69
|
+
validators.set(name, validator)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// Create the array of type alias declarations to be printed and exported
|
|
73
|
+
const types: ts.TypeAliasDeclaration[] = []
|
|
74
|
+
for (const [ name, validator ] of validators.entries()) {
|
|
75
|
+
// Clone our references map, and remove the validator being exported. This
|
|
76
|
+
// will make sure that we don't have any loops in our types
|
|
77
|
+
const referenceable = new Map(references)
|
|
78
|
+
if (referenceable.get(validator) === name) {
|
|
79
|
+
referenceable.delete(validator)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Generate the type of the validator, with our stripped reference table
|
|
83
|
+
const type = generateTypeNode(validator, referenceable)
|
|
84
|
+
|
|
85
|
+
// Create a type alias declaration with the name of the export
|
|
86
|
+
const modifiers = [ ts.factory.createModifier(ts.SyntaxKind.ExportKeyword) ]
|
|
87
|
+
const decl = ts.factory.createTypeAliasDeclaration(undefined, modifiers, name, [], type)
|
|
88
|
+
types.push(decl)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Print out all our type aliases
|
|
92
|
+
return ts.createPrinter().printList(
|
|
93
|
+
ts.ListFormat.SourceFileStatements,
|
|
94
|
+
ts.factory.createNodeArray(types),
|
|
95
|
+
ts.createSourceFile('types.d.ts', '', ts.ScriptTarget.Latest))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* ========================================================================== *
|
|
99
|
+
* TYPE GENERATORS *
|
|
100
|
+
* ========================================================================== */
|
|
101
|
+
|
|
102
|
+
/** Generate a TypeScript `TypeNode` for the given validator instance. */
|
|
103
|
+
function generateTypeNode(
|
|
104
|
+
validator: Validator,
|
|
105
|
+
references: ReadonlyMap<Validator, string>,
|
|
106
|
+
): ts.TypeNode {
|
|
107
|
+
const reference = references.get(validator)
|
|
108
|
+
if (reference) return ts.factory.createTypeReferenceNode(reference)
|
|
109
|
+
|
|
110
|
+
const generator = generators.get(validator.constructor)
|
|
111
|
+
assertSchema(!! generator, `Type generator for "${validator.constructor.name}" not found`)
|
|
112
|
+
return generator(validator, references)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* ========================================================================== */
|
|
116
|
+
|
|
117
|
+
// Simple nodes
|
|
118
|
+
|
|
119
|
+
const anyType = ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)
|
|
120
|
+
const anyArrayType = ts.factory.createArrayTypeNode(anyType)
|
|
121
|
+
const booleanType = ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword)
|
|
122
|
+
const numberType = ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)
|
|
123
|
+
const neverType = ts.factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword)
|
|
124
|
+
const stringType = ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
|
|
125
|
+
const recordType = ts.factory.createMappedTypeNode(
|
|
126
|
+
undefined, // readonly
|
|
127
|
+
ts.factory.createTypeParameterDeclaration('key', stringType),
|
|
128
|
+
undefined, // name type
|
|
129
|
+
undefined, // question token
|
|
130
|
+
anyType, // type of the mapped key
|
|
131
|
+
undefined) // members
|
|
132
|
+
|
|
133
|
+
// Modifiers and tokens
|
|
134
|
+
|
|
135
|
+
const readonlyKeyword = [ ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword) ]
|
|
136
|
+
const optionalKeyword = ts.factory.createToken(ts.SyntaxKind.QuestionToken)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
/* ========================================================================== */
|
|
140
|
+
|
|
141
|
+
// Simple generator returning nodes
|
|
142
|
+
|
|
143
|
+
registerTypeGenerator(AnyValidator, () => anyType)
|
|
144
|
+
registerTypeGenerator(AnyArrayValidator, () => anyArrayType)
|
|
145
|
+
registerTypeGenerator(AnyNumberValidator, () => numberType)
|
|
146
|
+
registerTypeGenerator(AnyObjectValidator, () => recordType)
|
|
147
|
+
registerTypeGenerator(AnyStringValidator, () => stringType)
|
|
148
|
+
registerTypeGenerator(BooleanValidator, () => booleanType)
|
|
149
|
+
registerTypeGenerator(DateValidator, () => ts.factory.createTypeReferenceNode('Date'))
|
|
150
|
+
registerTypeGenerator(URLValidator, () => ts.factory.createTypeReferenceNode('URL'))
|
|
151
|
+
|
|
152
|
+
/* ========================================================================== */
|
|
153
|
+
|
|
154
|
+
// Complex generator functions...
|
|
155
|
+
|
|
156
|
+
registerTypeGenerator(ArrayValidator, (validator, references) => {
|
|
157
|
+
const itemType = generateTypeNode(validator.items, references)
|
|
158
|
+
return ts.factory.createArrayTypeNode(itemType)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
registerTypeGenerator(ConstantValidator, (validator) => {
|
|
162
|
+
const literal =
|
|
163
|
+
typeof validator.constant === 'number' ? ts.factory.createNumericLiteral(validator.constant) :
|
|
164
|
+
typeof validator.constant === 'string' ? ts.factory.createStringLiteral(validator.constant) :
|
|
165
|
+
validator.constant === false ? ts.factory.createFalse() :
|
|
166
|
+
validator.constant === true ? ts.factory.createTrue() :
|
|
167
|
+
validator.constant === null ? ts.factory.createNull() :
|
|
168
|
+
undefined
|
|
169
|
+
|
|
170
|
+
assertSchema(!! literal, `Invalid constant "${validator.constant}"`)
|
|
171
|
+
return ts.factory.createLiteralTypeNode(literal)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
registerTypeGenerator(NumberValidator, (validator: NumberValidator) => {
|
|
175
|
+
if (! validator.brand) return numberType
|
|
176
|
+
|
|
177
|
+
const signature = ts.factory.createPropertySignature(undefined, `__brand_${validator.brand}`, undefined, neverType)
|
|
178
|
+
const literal = ts.factory.createTypeLiteralNode([ signature ])
|
|
179
|
+
return ts.factory.createIntersectionTypeNode([ numberType, literal ])
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
registerTypeGenerator(StringValidator, (validator: StringValidator) => {
|
|
183
|
+
if (! validator.brand) return stringType
|
|
184
|
+
|
|
185
|
+
const signature = ts.factory.createPropertySignature(undefined, `__brand_${validator.brand}`, undefined, neverType)
|
|
186
|
+
const literal = ts.factory.createTypeLiteralNode([ signature ])
|
|
187
|
+
return ts.factory.createIntersectionTypeNode([ stringType, literal ])
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
registerTypeGenerator(TupleValidator, (validator: TupleValidator<any>, references) => {
|
|
191
|
+
const members = validator.members
|
|
192
|
+
|
|
193
|
+
// count how many rest parameters do we have..
|
|
194
|
+
const { count, first, next } =
|
|
195
|
+
members.reduce(({ count, first, next }, { single }, i) => {
|
|
196
|
+
if (! single) {
|
|
197
|
+
if (i < first) first = i
|
|
198
|
+
next = i + 1
|
|
199
|
+
count += 1
|
|
200
|
+
}
|
|
201
|
+
return { count, first, next }
|
|
202
|
+
}, { count: 0, first: members.length, next: -1 })
|
|
203
|
+
|
|
204
|
+
// if we have zero or one rest parameter, things are easy...
|
|
205
|
+
if (count < 2) {
|
|
206
|
+
const types = members.map(({ single, validator }) => {
|
|
207
|
+
const memberType = generateTypeNode(validator, references)
|
|
208
|
+
|
|
209
|
+
if (single) return generateTypeNode(validator, references)
|
|
210
|
+
|
|
211
|
+
const arrayType = ts.factory.createArrayTypeNode(memberType)
|
|
212
|
+
return ts.factory.createRestTypeNode(arrayType)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
return ts.factory.createTupleTypeNode(types)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// We have two or more rest parameters... we need combine everything between
|
|
219
|
+
// the first and the last one in a giant union!
|
|
220
|
+
const before = members.slice(0, first)
|
|
221
|
+
.map(({ validator }) => generateTypeNode(validator, references))
|
|
222
|
+
const types = members.slice(first, next)
|
|
223
|
+
.map(({ validator }) => generateTypeNode(validator, references))
|
|
224
|
+
const after = members.slice(next)
|
|
225
|
+
.map(({ validator }) => generateTypeNode(validator, references))
|
|
226
|
+
|
|
227
|
+
const union = ts.factory.createUnionTypeNode(types)
|
|
228
|
+
const array = ts.factory.createArrayTypeNode(union)
|
|
229
|
+
const rest = ts.factory.createRestTypeNode(array)
|
|
230
|
+
|
|
231
|
+
return ts.factory.createTupleTypeNode([ ...before, rest, ...after ])
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
registerTypeGenerator(AllOfValidator, (validator, references) => {
|
|
235
|
+
const members = validator.validators.map((validator) => generateTypeNode(validator, references))
|
|
236
|
+
return ts.factory.createIntersectionTypeNode(members)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
registerTypeGenerator(OneOfValidator, (validator, references) => {
|
|
240
|
+
const members = validator.validators.map((validator) => generateTypeNode(validator, references))
|
|
241
|
+
return ts.factory.createUnionTypeNode(members)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
registerTypeGenerator(ObjectValidator, (validator, references) => {
|
|
245
|
+
const properties: ts.PropertySignature[] = []
|
|
246
|
+
|
|
247
|
+
for (const [ key, property ] of validator.properties.entries()) {
|
|
248
|
+
const { validator, readonly, optional } = property || { optional: true }
|
|
249
|
+
const type = validator ? generateTypeNode(validator, references) : neverType
|
|
250
|
+
|
|
251
|
+
const signature = ts.factory.createPropertySignature(
|
|
252
|
+
readonly ? readonlyKeyword : undefined,
|
|
253
|
+
key,
|
|
254
|
+
optional ? optionalKeyword : undefined,
|
|
255
|
+
type)
|
|
256
|
+
|
|
257
|
+
properties.push(signature)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (validator.additionalProperties) {
|
|
261
|
+
const extra = ts.factory.createMappedTypeNode(
|
|
262
|
+
undefined, // readonly
|
|
263
|
+
ts.factory.createTypeParameterDeclaration('key', stringType),
|
|
264
|
+
undefined, // name type
|
|
265
|
+
undefined, // question token
|
|
266
|
+
generateTypeNode(validator.additionalProperties, references),
|
|
267
|
+
undefined) // members
|
|
268
|
+
|
|
269
|
+
if (properties.length == 0) return extra
|
|
270
|
+
|
|
271
|
+
const type = ts.factory.createTypeLiteralNode(properties)
|
|
272
|
+
return ts.factory.createIntersectionTypeNode([ type, extra ])
|
|
273
|
+
} else {
|
|
274
|
+
return ts.factory.createTypeLiteralNode(properties)
|
|
275
|
+
}
|
|
276
|
+
})
|
package/src/errors.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
type ValidationErrors = { path: (string | number)[], message: string }[]
|
|
1
|
+
export type ValidationErrors = { path: (string | number)[], message: string }[]
|
|
2
2
|
|
|
3
3
|
/** Combine the components of a _path_ into a human readable string */
|
|
4
4
|
function pathToString(path: (string | number)[]): string {
|
|
@@ -86,8 +86,8 @@ export class ValidationErrorBuilder {
|
|
|
86
86
|
* @param key - The key in an object, or index in an array where the
|
|
87
87
|
* vaildation error was encountered
|
|
88
88
|
*/
|
|
89
|
-
record(error: any, key
|
|
90
|
-
const path =
|
|
89
|
+
record(error: any, ...key: (string | number)[]): this {
|
|
90
|
+
const path = [ ...key ]
|
|
91
91
|
if (error instanceof ValidationError) {
|
|
92
92
|
error.errors.forEach(({ path: subpath, message }) => {
|
|
93
93
|
this.errors.push({ path: [ ...path, ...subpath ], message })
|
package/src/index.ts
CHANGED
|
@@ -9,16 +9,17 @@ export * from './types'
|
|
|
9
9
|
export * from './utilities'
|
|
10
10
|
|
|
11
11
|
// Validators
|
|
12
|
-
export { allOf, oneOf, AllOfValidator, OneOfValidator } from './validators/union'
|
|
12
|
+
export { allOf, oneOf, AllOfValidator, InferAllOfValidationType, InferOneOfValidationType, OneOfValidator, UnionArguments } from './validators/union'
|
|
13
13
|
export { any, AnyValidator } from './validators/any'
|
|
14
|
-
export { array, arrayOf, ArrayValidator } from './validators/array'
|
|
14
|
+
export { _array, array, arrayOf, AnyArrayValidator, ArrayConstraints, ArrayValidator } from './validators/array'
|
|
15
15
|
export { boolean, BooleanValidator } from './validators/boolean'
|
|
16
16
|
export { constant, ConstantValidator } from './validators/constant'
|
|
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'
|
|
21
|
-
export { tuple, TupleValidator } from './validators/tuple'
|
|
17
|
+
export { _date, date, DateConstraints, DateValidator } from './validators/date'
|
|
18
|
+
export { _number, number, AnyNumberValidator, BrandedNumberConstraints, NumberConstraints, NumberValidator } from './validators/number'
|
|
19
|
+
export { _object, object, AnyObjectValidator, ObjectProperty, ObjectValidator } from './validators/object'
|
|
20
|
+
export { _string, string, AnyStringValidator, BrandedStringConstraints, StringConstraints, StringValidator } from './validators/string'
|
|
21
|
+
export { tuple, TupleMember, TupleValidator } from './validators/tuple'
|
|
22
|
+
export { _url, url, URLConstraints, URLValidator } from './validators/url'
|
|
22
23
|
|
|
23
24
|
/* ========================================================================== *
|
|
24
25
|
* VALIDATE FUNCTION (our main entry point) *
|
|
@@ -32,7 +33,12 @@ export type ValidateOptions = {
|
|
|
32
33
|
-readonly [ key in keyof ValidationOptions ]?: ValidationOptions[key] | undefined
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Validate a _value_ using the specified `Validation`.
|
|
38
|
+
*
|
|
39
|
+
* By default additional and forbidden properties will _not_ be stripped and
|
|
40
|
+
* reported as an error.
|
|
41
|
+
*/
|
|
36
42
|
export function validate<V extends Validation>(
|
|
37
43
|
validation: V,
|
|
38
44
|
value: any,
|
|
@@ -46,3 +52,21 @@ export function validate<V extends Validation>(
|
|
|
46
52
|
|
|
47
53
|
return getValidator(validation).validate(value, opts)
|
|
48
54
|
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Validate a _value_ using the specified `Validation`, automatically stripping
|
|
58
|
+
* additional properties (but not forbidden ones).
|
|
59
|
+
*/
|
|
60
|
+
export function strip<V extends Validation>(
|
|
61
|
+
validation: V,
|
|
62
|
+
value: any,
|
|
63
|
+
options: ValidateOptions = {},
|
|
64
|
+
): InferValidation<V> {
|
|
65
|
+
const opts: ValidationOptions = {
|
|
66
|
+
stripAdditionalProperties: true,
|
|
67
|
+
stripForbiddenProperties: false,
|
|
68
|
+
...options,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return getValidator(validation).validate(value, opts)
|
|
72
|
+
}
|
package/src/schema.ts
CHANGED
|
@@ -19,12 +19,12 @@ import {
|
|
|
19
19
|
* ========================================================================== */
|
|
20
20
|
|
|
21
21
|
/** Internal definition of `allowAdditionalProperties(...)` */
|
|
22
|
-
function _allowAdditionalProperties(): AdditionalProperties<Validator<any>>
|
|
23
|
-
function _allowAdditionalProperties(allow: true): AdditionalProperties<Validator<any>>
|
|
24
|
-
function _allowAdditionalProperties(allow: false): AdditionalProperties<false>
|
|
25
|
-
function _allowAdditionalProperties<V extends Validation>(validation: V): AdditionalProperties<Validator<InferValidation<V>>>
|
|
22
|
+
export function _allowAdditionalProperties(): AdditionalProperties<Validator<any>>
|
|
23
|
+
export function _allowAdditionalProperties(allow: true): AdditionalProperties<Validator<any>>
|
|
24
|
+
export function _allowAdditionalProperties(allow: false): AdditionalProperties<false>
|
|
25
|
+
export function _allowAdditionalProperties<V extends Validation>(validation: V): AdditionalProperties<Validator<InferValidation<V>>>
|
|
26
26
|
|
|
27
|
-
function _allowAdditionalProperties(options?: Validation | boolean): AdditionalProperties<Validator | false> {
|
|
27
|
+
export function _allowAdditionalProperties(options?: Validation | boolean): AdditionalProperties<Validator | false> {
|
|
28
28
|
if (options === false) return { [additionalValidator]: false }
|
|
29
29
|
if (options === true) return { [additionalValidator]: any }
|
|
30
30
|
|
|
@@ -49,7 +49,7 @@ allowAdditionalProperties[additionalValidator] = any
|
|
|
49
49
|
* SCHEMA KEYS MODIFIERS *
|
|
50
50
|
* ========================================================================== */
|
|
51
51
|
|
|
52
|
-
type CombineModifiers<M1 extends Modifier, M2 extends Modifier> =
|
|
52
|
+
export type CombineModifiers<M1 extends Modifier, M2 extends Modifier> =
|
|
53
53
|
M1 extends ReadonlyModifier ?
|
|
54
54
|
M2 extends ReadonlyModifier<infer V> ? ReadonlyModifier<V> :
|
|
55
55
|
M2 extends OptionalModifier<infer V> ? CombinedModifier<V> :
|
package/src/types.ts
CHANGED
|
@@ -115,7 +115,7 @@ export type InferValidation<V> =
|
|
|
115
115
|
* ========================================================================== */
|
|
116
116
|
|
|
117
117
|
/** Infer the type validated by a `Validation` or `TupleRestParameter` */
|
|
118
|
-
type InferValidationOrTupleRest<T> =
|
|
118
|
+
export type InferValidationOrTupleRest<T> =
|
|
119
119
|
T extends TupleRestParameter<infer X> ? X :
|
|
120
120
|
T extends Validation ? InferValidation<T> :
|
|
121
121
|
never
|
|
@@ -237,7 +237,7 @@ export type InferSchema<S extends Schema> =
|
|
|
237
237
|
/* -------------------------------------------------------------------------- */
|
|
238
238
|
|
|
239
239
|
/** Infer the type of keys associated with `Validation`s */
|
|
240
|
-
type InferRequired<S extends Schema> = {
|
|
240
|
+
export type InferRequired<S extends Schema> = {
|
|
241
241
|
[ key in keyof S as
|
|
242
242
|
key extends string ?
|
|
243
243
|
S[key] extends Validation ? key :
|
|
@@ -251,7 +251,7 @@ type InferRequired<S extends Schema> = {
|
|
|
251
251
|
/* -------------------------------------------------------------------------- */
|
|
252
252
|
|
|
253
253
|
/** Infer the type of _read only_ `Schema` properties */
|
|
254
|
-
type InferReadonlyModifiers<S extends Schema> = {
|
|
254
|
+
export type InferReadonlyModifiers<S extends Schema> = {
|
|
255
255
|
readonly [ key in keyof S as
|
|
256
256
|
key extends string ?
|
|
257
257
|
S[key] extends OptionalModifier<Validator> ? never :
|
|
@@ -263,7 +263,7 @@ type InferReadonlyModifiers<S extends Schema> = {
|
|
|
263
263
|
}
|
|
264
264
|
|
|
265
265
|
/** Infer the type of _optional_ `Schema` properties */
|
|
266
|
-
type InferOptionalModifiers<S extends Schema> = {
|
|
266
|
+
export type InferOptionalModifiers<S extends Schema> = {
|
|
267
267
|
[ key in keyof S as
|
|
268
268
|
key extends string ?
|
|
269
269
|
S[key] extends ReadonlyModifier<Validator> ? never :
|
|
@@ -275,7 +275,7 @@ type InferOptionalModifiers<S extends Schema> = {
|
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
/** Infer the type of _read only_ **and** _optional_ `Schema` properties */
|
|
278
|
-
type InferCombinedModifiers<S extends Schema> = {
|
|
278
|
+
export type InferCombinedModifiers<S extends Schema> = {
|
|
279
279
|
readonly [ key in keyof S as
|
|
280
280
|
key extends string ?
|
|
281
281
|
S[key] extends CombinedModifier ? key :
|
|
@@ -288,7 +288,7 @@ type InferCombinedModifiers<S extends Schema> = {
|
|
|
288
288
|
/* -------------------------------------------------------------------------- */
|
|
289
289
|
|
|
290
290
|
/** Ensure that we properly type `never` properties */
|
|
291
|
-
type InferNever<S extends Schema> =
|
|
291
|
+
export type InferNever<S extends Schema> =
|
|
292
292
|
{ [ key in keyof S as
|
|
293
293
|
key extends string ?
|
|
294
294
|
S[key] extends typeof never ? key :
|
|
@@ -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,19 +87,14 @@ 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
|
|
|
92
|
-
function _array(): Validator<any[]>
|
|
93
|
-
function _array<V extends Validation>(constraints: ArrayConstraints<V>): ArrayValidator<InferValidation<V>>
|
|
94
|
+
export function _array(): Validator<any[]>
|
|
95
|
+
export function _array<V extends Validation>(constraints: ArrayConstraints<V>): ArrayValidator<InferValidation<V>>
|
|
94
96
|
|
|
95
|
-
function _array(options?: ArrayConstraints<Validation>): Validator<any[]> {
|
|
97
|
+
export function _array(options?: ArrayConstraints<Validation>): Validator<any[]> {
|
|
96
98
|
if (! options) return anyArrayValidator
|
|
97
99
|
|
|
98
100
|
const items = getValidator(options.items)
|
package/src/validators/date.ts
CHANGED
|
@@ -67,10 +67,10 @@ export class DateValidator extends Validator<Date> {
|
|
|
67
67
|
|
|
68
68
|
const anyDateValidator = new DateValidator()
|
|
69
69
|
|
|
70
|
-
function _date(): DateValidator
|
|
71
|
-
function _date(constraints: DateConstraints): DateValidator
|
|
70
|
+
export function _date(): DateValidator
|
|
71
|
+
export function _date(constraints: DateConstraints): DateValidator
|
|
72
72
|
|
|
73
|
-
function _date(constraints?: DateConstraints): DateValidator {
|
|
73
|
+
export function _date(constraints?: DateConstraints): DateValidator {
|
|
74
74
|
return constraints ? new DateValidator(constraints) : anyDateValidator
|
|
75
75
|
}
|
|
76
76
|
|
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,22 +146,16 @@ 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
|
-
function _number(): Validator<number>
|
|
124
|
-
function _number
|
|
153
|
+
export function _number(): Validator<number>
|
|
154
|
+
export function _number(constraints?: NumberConstraints): NumberValidator<number>
|
|
155
|
+
export function _number<N extends number>(constraints?: NumberConstraints): NumberValidator<N>
|
|
156
|
+
export function _number<B extends string>(constraints: BrandedNumberConstraints<B>): NumberValidator<number & Branding<B>>
|
|
125
157
|
|
|
126
|
-
function _number(constraints?: NumberConstraints): Validator<number> {
|
|
158
|
+
export function _number(constraints?: NumberConstraints): Validator<number> {
|
|
127
159
|
return constraints ? new NumberValidator(constraints) : anyNumberValidator
|
|
128
160
|
}
|
|
129
161
|
|