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.
@@ -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?: string | number): this {
90
- const path = key === undefined ? [] : [ key ]
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
- /** Validate a _value_ using the specified `Validation` */
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
+ }
@@ -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,19 +87,14 @@ 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
 
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)
@@ -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
 
@@ -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,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 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
- function _number(): Validator<number>
124
- function _number<N extends number = number>(constraints?: NumberConstraints): NumberValidator<N>
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