generated from sapphiredev/sapphire-template
-
-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Vlad Frangu <[email protected]> Co-authored-by: Jeroen Claassens <[email protected]>
- Loading branch information
1 parent
e267b94
commit abe7ead
Showing
18 changed files
with
472 additions
and
122 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,12 @@ | ||
import { Shapes } from './lib/Shapes'; | ||
|
||
export const s = new Shapes(); | ||
|
||
export * from './lib/errors/ConstraintError'; | ||
export * from './lib/errors/ExpectedValidationError'; | ||
export * from './lib/errors/MissingPropertyError'; | ||
export * from './lib/errors/UnknownPropertyError'; | ||
export * from './lib/errors/ValidationError'; | ||
|
||
export * from './lib/Result'; | ||
export * from './type-exports'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
export class MissingPropertyError extends Error { | ||
public readonly property: PropertyKey; | ||
|
||
public constructor(property: PropertyKey) { | ||
super(`Expected property "${String(property)}" is missing`); | ||
|
||
this.property = property; | ||
} | ||
|
||
public toJSON() { | ||
return { | ||
name: this.name, | ||
property: this.property | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
export class UnknownPropertyError extends Error { | ||
public readonly property: PropertyKey; | ||
public readonly value: unknown; | ||
|
||
public constructor(property: PropertyKey, value: unknown) { | ||
super('Unknown property received'); | ||
|
||
this.property = property; | ||
this.value = value; | ||
} | ||
|
||
public toJSON() { | ||
return { | ||
name: this.name, | ||
property: this.property, | ||
value: this.value | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,10 @@ | ||
/** | ||
* @internal | ||
* This type is only used inside of the library and is not exported on the root level | ||
*/ | ||
import type { BaseValidator } from '../validators/BaseValidator'; | ||
|
||
export type Constructor<T> = (new (...args: readonly any[]) => T) | (abstract new (...args: readonly any[]) => T); | ||
|
||
export type Type<V> = V extends BaseValidator<infer T> ? T : never; | ||
|
||
// eslint-disable-next-line @typescript-eslint/ban-types | ||
export type NonNullObject = {} & object; | ||
|
||
export type MappedObjectValidator<T> = { [key in keyof T]: BaseValidator<T[key]> }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
import type { IConstraint } from '../constraints/base/IConstraint'; | ||
import { MissingPropertyError } from '../lib/errors/MissingPropertyError'; | ||
import { UnknownPropertyError } from '../lib/errors/UnknownPropertyError'; | ||
import { ValidationError } from '../lib/errors/ValidationError'; | ||
import { Result } from '../lib/Result'; | ||
import type { MappedObjectValidator, NonNullObject } from '../lib/util-types'; | ||
import { BaseValidator } from './BaseValidator'; | ||
|
||
export class ObjectValidator<T extends NonNullObject> extends BaseValidator<T> { | ||
public readonly shape: MappedObjectValidator<T>; | ||
public readonly strategy: ObjectValidatorStrategy; | ||
private readonly keys: readonly (keyof T)[]; | ||
private readonly handleStrategy: (value: NonNullObject) => Result<T, AggregateError>; | ||
|
||
public constructor( | ||
shape: MappedObjectValidator<T>, | ||
strategy: ObjectValidatorStrategy = ObjectValidatorStrategy.Ignore, | ||
constraints: readonly IConstraint<T>[] = [] | ||
) { | ||
super(constraints); | ||
this.shape = shape; | ||
this.keys = Object.keys(shape) as (keyof T)[]; | ||
this.strategy = strategy; | ||
|
||
switch (this.strategy) { | ||
case ObjectValidatorStrategy.Ignore: | ||
this.handleStrategy = (value) => this.handleIgnoreStrategy(value); | ||
break; | ||
case ObjectValidatorStrategy.Strict: { | ||
this.handleStrategy = (value) => this.handleStrictStrategy(value); | ||
break; | ||
} | ||
} | ||
} | ||
|
||
public get strict(): ObjectValidator<{ [Key in keyof T]-?: T[Key] }> { | ||
return Reflect.construct(this.constructor, [this.shape, ObjectValidatorStrategy.Strict, this.constraints]); | ||
} | ||
|
||
public get ignore(): this { | ||
return Reflect.construct(this.constructor, [this.shape, ObjectValidatorStrategy.Ignore, this.constraints]); | ||
} | ||
|
||
public get partial(): ObjectValidator<{ [Key in keyof T]?: T[Key] }> { | ||
const shape = Object.fromEntries(this.keys.map((key) => [key, this.shape[key].optional])); | ||
return Reflect.construct(this.constructor, [shape, this.strategy, this.constraints]); | ||
} | ||
|
||
public extend<ET extends NonNullObject>(schema: ObjectValidator<ET> | MappedObjectValidator<ET>): ObjectValidator<T & ET> { | ||
const shape = { ...this.shape, ...(schema instanceof ObjectValidator ? schema.shape : schema) }; | ||
return Reflect.construct(this.constructor, [shape, this.strategy, this.constraints]); | ||
} | ||
|
||
public pick<K extends keyof T>(keys: readonly K[]): ObjectValidator<{ [Key in keyof Pick<T, K>]: T[Key] }> { | ||
const shape = Object.fromEntries(keys.filter((key) => this.keys.includes(key)).map((key) => [key, this.shape[key]])); | ||
return Reflect.construct(this.constructor, [shape, this.strategy, this.constraints]); | ||
} | ||
|
||
public omit<K extends keyof T>(keys: readonly K[]): ObjectValidator<{ [Key in keyof Omit<T, K>]: T[Key] }> { | ||
const shape = Object.fromEntries(this.keys.filter((key) => !keys.includes(key as any)).map((key) => [key, this.shape[key]])); | ||
return Reflect.construct(this.constructor, [shape, this.strategy, this.constraints]); | ||
} | ||
|
||
protected override handle(value: unknown): Result<T, ValidationError | AggregateError> { | ||
const typeOfValue = typeof value; | ||
if (typeOfValue !== 'object') { | ||
return Result.err( | ||
new ValidationError('ObjectValidator', `Expected the value to be an object, but received ${typeOfValue} instead`, value) | ||
); | ||
} | ||
|
||
if (value === null) { | ||
return Result.err(new ValidationError('ObjectValidator', 'Expected the value to not be null', value)); | ||
} | ||
|
||
return this.handleStrategy(value as NonNullObject); | ||
} | ||
|
||
protected clone(): this { | ||
return Reflect.construct(this.constructor, [this.shape, this.strategy, this.constraints]); | ||
} | ||
|
||
private handleIgnoreStrategy(value: NonNullObject, errors: Error[] = []): Result<T, AggregateError> { | ||
const entries = {} as T; | ||
let i = this.keys.length; | ||
|
||
while (i--) { | ||
const key = this.keys[i]; | ||
const result = this.shape[key].run(value[key as keyof NonNullObject]); | ||
|
||
if (result.isOk()) { | ||
entries[key] = result.value; | ||
} else { | ||
const error = result.error!; | ||
if (error instanceof ValidationError && error.given === undefined) { | ||
errors.push(new MissingPropertyError(key)); | ||
} else { | ||
errors.push(error); | ||
} | ||
} | ||
} | ||
|
||
return errors.length === 0 // | ||
? Result.ok(entries) | ||
: Result.err(new AggregateError(errors, 'Failed to match at least one of the properties')); | ||
} | ||
|
||
private handleStrictStrategy(value: NonNullObject): Result<T, AggregateError> { | ||
const errors: Error[] = []; | ||
const finalResult = {} as T; | ||
const keysToIterateOver = [...new Set([...Object.keys(value), ...this.keys])].reverse(); | ||
let i = keysToIterateOver.length; | ||
|
||
while (i--) { | ||
const key = keysToIterateOver[i] as string; | ||
|
||
if (Object.prototype.hasOwnProperty.call(this.shape, key)) { | ||
const result = this.shape[key as keyof MappedObjectValidator<T>].run(value[key as keyof NonNullObject]); | ||
|
||
if (result.isOk()) { | ||
finalResult[key as keyof T] = result.value; | ||
} else { | ||
const error = result.error!; | ||
if (error instanceof ValidationError && error.given === undefined) { | ||
errors.push(new MissingPropertyError(key)); | ||
} else { | ||
errors.push(error); | ||
} | ||
} | ||
|
||
continue; | ||
} | ||
|
||
errors.push(new UnknownPropertyError(key, value[key as keyof NonNullObject])); | ||
} | ||
|
||
return errors.length === 0 // | ||
? Result.ok(finalResult) | ||
: Result.err(new AggregateError(errors, 'Failed to match at least one of the properties')); | ||
} | ||
} | ||
|
||
export const enum ObjectValidatorStrategy { | ||
Ignore, | ||
Strict | ||
} |
Oops, something went wrong.