From abe7eaddee981ef485713ff5e7b7f32ff97c645b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Rom=C3=A1n?= Date: Sun, 16 Jan 2022 21:37:11 +0100 Subject: [PATCH] feat: added ObjectValidator (#3) Co-authored-by: Vlad Frangu Co-authored-by: Jeroen Claassens --- README.md | 11 +- package.json | 3 +- src/index.ts | 4 + src/lib/Shapes.ts | 7 +- src/lib/errors/MissingPropertyError.ts | 16 +++ src/lib/errors/UnknownPropertyError.ts | 19 +++ src/lib/util-types.ts | 13 +- src/type-exports.ts | 8 ++ src/validators/ArrayValidator.ts | 20 +-- src/validators/BaseValidator.ts | 2 +- src/validators/ObjectValidator.ts | 146 +++++++++++++++++++++ src/validators/SetValidator.ts | 20 +-- src/validators/UnionValidator.ts | 26 +--- src/validators/imports.ts | 1 + tests/validators/object.test.ts | 174 +++++++++++++++++++++++++ tsconfig.base.json | 3 +- typedoc.json | 1 - yarn.lock | 120 +++++++++-------- 18 files changed, 472 insertions(+), 122 deletions(-) create mode 100644 src/lib/errors/MissingPropertyError.ts create mode 100644 src/lib/errors/UnknownPropertyError.ts create mode 100644 src/validators/ObjectValidator.ts create mode 100644 tests/validators/object.test.ts diff --git a/README.md b/README.md index ea3ffdbc..7a08027a 100644 --- a/README.md +++ b/README.md @@ -229,7 +229,6 @@ dish.parse(['Iberian ham', 10, new Date()]); ```typescript // Properties are required by default: const animal = s.object({ - // TODO name: s.string, age: s.number }); @@ -241,13 +240,11 @@ You can add additional fields using either an object or an ObjectValidator, in t ```typescript const pet = animal.extend({ - // TODO owner: s.string.nullish }); const pet = animal.extend( s.object({ - // TODO owner: s.string.nullish }) ); @@ -266,10 +263,10 @@ const pkg = s.object({ dependencies: s.string.array }); -const justTheName = pkg.pick(['name']); // TODO +const justTheName = pkg.pick(['name']); // s.object({ name: s.string }); -const noDependencies = pkg.omit(['dependencies']); // TODO +const noDependencies = pkg.omit(['dependencies']); // s.object({ name: s.string, description: s.string }); ``` @@ -311,7 +308,7 @@ person.parse({ // => { name: 'Sapphire' } ``` -##### `.strict` // TODO +##### `.strict` You can disallow unknown keys with `.strict`. If the input includes any unknown keys, an error will be thrown. @@ -327,7 +324,7 @@ person.parse({ // => throws ValidationError ``` -##### `.ignore` // TODO +##### `.ignore` You can use the `.ignore` getter to reset an object schema to the default behaviour (ignoring unrecognized keys). diff --git a/package.json b/package.json index acaa80a1..d25b57d0 100644 --- a/package.json +++ b/package.json @@ -26,13 +26,14 @@ "clean": "node scripts/clean.mjs", "typecheck": "tsc -p tsconfig.typecheck.json", "sversion": "standard-version", - "prepublishOnly": "yarn build", + "prepublishOnly": "rollup-type-bundler", "prepare": "husky install .github/husky" }, "devDependencies": { "@commitlint/cli": "^16.0.2", "@commitlint/config-conventional": "^16.0.0", "@favware/npm-deprecate": "^1.0.4", + "@favware/rollup-type-bundler": "^1.0.7", "@sapphire/eslint-config": "^4.0.9", "@sapphire/prettier-config": "^1.2.8", "@sapphire/ts-config": "^3.1.7", diff --git a/src/index.ts b/src/index.ts index 40c8cc6d..f4a00fbc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/lib/Shapes.ts b/src/lib/Shapes.ts index 3a690a04..2b5e57e5 100644 --- a/src/lib/Shapes.ts +++ b/src/lib/Shapes.ts @@ -9,12 +9,13 @@ import { NeverValidator, NullishValidator, NumberValidator, + ObjectValidator, PassthroughValidator, SetValidator, StringValidator, UnionValidator } from '../validators/imports'; -import type { Constructor } from './util-types'; +import type { Constructor, MappedObjectValidator } from './util-types'; export class Shapes { public get string() { @@ -37,6 +38,10 @@ export class Shapes { return new DateValidator(); } + public object(shape: MappedObjectValidator) { + return new ObjectValidator(shape); + } + public get undefined() { return this.literal(undefined); } diff --git a/src/lib/errors/MissingPropertyError.ts b/src/lib/errors/MissingPropertyError.ts new file mode 100644 index 00000000..c0b352a2 --- /dev/null +++ b/src/lib/errors/MissingPropertyError.ts @@ -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 + }; + } +} diff --git a/src/lib/errors/UnknownPropertyError.ts b/src/lib/errors/UnknownPropertyError.ts new file mode 100644 index 00000000..14866f11 --- /dev/null +++ b/src/lib/errors/UnknownPropertyError.ts @@ -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 + }; + } +} diff --git a/src/lib/util-types.ts b/src/lib/util-types.ts index 057e6f9e..d2fab3af 100644 --- a/src/lib/util-types.ts +++ b/src/lib/util-types.ts @@ -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 = (new (...args: readonly any[]) => T) | (abstract new (...args: readonly any[]) => T); + +export type Type = V extends BaseValidator ? T : never; + +// eslint-disable-next-line @typescript-eslint/ban-types +export type NonNullObject = {} & object; + +export type MappedObjectValidator = { [key in keyof T]: BaseValidator }; diff --git a/src/type-exports.ts b/src/type-exports.ts index 720df1c0..853217a6 100644 --- a/src/type-exports.ts +++ b/src/type-exports.ts @@ -17,10 +17,17 @@ export type { numberSafeInt } from './constraints/NumberConstraints'; export type { stringLengthEq, stringLengthGe, stringLengthGt, stringLengthLe, stringLengthLt, stringLengthNe } from './constraints/StringConstraints'; +// export type { ConstraintError, ConstraintErrorMessageBuilder } from './lib/errors/ConstraintError'; export type { ExpectedValidationError } from './lib/errors/ExpectedValidationError'; +export type { MissingPropertyError } from './lib/errors/MissingPropertyError'; +export type { UnknownPropertyError } from './lib/errors/UnknownPropertyError'; export type { ValidationError } from './lib/errors/ValidationError'; +// export type { Shapes } from './lib/Shapes'; +// +export type { Constructor, MappedObjectValidator, NonNullObject, Type } from './lib/util-types'; +// export type { ArrayValidator } from './validators/ArrayValidator'; export type { BaseValidator } from './validators/BaseValidator'; export type { BigIntValidator } from './validators/BigIntValidator'; @@ -31,6 +38,7 @@ export type { LiteralValidator } from './validators/LiteralValidator'; export type { NeverValidator } from './validators/NeverValidator'; export type { NullishValidator } from './validators/NullishValidator'; export type { NumberValidator } from './validators/NumberValidator'; +export type { ObjectValidator, ObjectValidatorStrategy } from './validators/ObjectValidator'; export type { PassthroughValidator } from './validators/PassthroughValidator'; export type { SetValidator } from './validators/SetValidator'; export type { StringValidator } from './validators/StringValidator'; diff --git a/src/validators/ArrayValidator.ts b/src/validators/ArrayValidator.ts index c298c31c..6789e47b 100644 --- a/src/validators/ArrayValidator.ts +++ b/src/validators/ArrayValidator.ts @@ -11,7 +11,11 @@ export class ArrayValidator extends BaseValidator { this.validator = validator; } - public run(values: unknown): Result { + protected override clone(): this { + return Reflect.construct(this.constructor, [this.validator, this.constraints]); + } + + protected handle(values: unknown): Result { if (!Array.isArray(values)) { return Result.err(new ValidationError('ArrayValidator', 'Expected an array', values)); } @@ -27,18 +31,6 @@ export class ArrayValidator extends BaseValidator { return errors.length === 0 // ? Result.ok(transformed) - : Result.err(new AggregateError(errors, 'Could not match any of the defined validators')); - } - - public parse(value: unknown): T[] { - return this.run(value).unwrap(); - } - - protected override clone(): this { - return Reflect.construct(this.constructor, [this.validator, this.constraints]); - } - - protected handle(): Result { - throw new Error('Unreachable'); + : Result.err(new AggregateError(errors, 'Failed to validate at least one entry')); } } diff --git a/src/validators/BaseValidator.ts b/src/validators/BaseValidator.ts index b2c8ffcd..a6f1c380 100644 --- a/src/validators/BaseValidator.ts +++ b/src/validators/BaseValidator.ts @@ -54,7 +54,7 @@ export abstract class BaseValidator { return Reflect.construct(this.constructor, [this.constraints]); } - protected abstract handle(value: unknown): Result; + protected abstract handle(value: unknown): Result; protected addConstraint(constraint: IConstraint): this { const clone = this.clone(); diff --git a/src/validators/ObjectValidator.ts b/src/validators/ObjectValidator.ts new file mode 100644 index 00000000..a3231a76 --- /dev/null +++ b/src/validators/ObjectValidator.ts @@ -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 extends BaseValidator { + public readonly shape: MappedObjectValidator; + public readonly strategy: ObjectValidatorStrategy; + private readonly keys: readonly (keyof T)[]; + private readonly handleStrategy: (value: NonNullObject) => Result; + + public constructor( + shape: MappedObjectValidator, + strategy: ObjectValidatorStrategy = ObjectValidatorStrategy.Ignore, + constraints: readonly IConstraint[] = [] + ) { + 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(schema: ObjectValidator | MappedObjectValidator): ObjectValidator { + const shape = { ...this.shape, ...(schema instanceof ObjectValidator ? schema.shape : schema) }; + return Reflect.construct(this.constructor, [shape, this.strategy, this.constraints]); + } + + public pick(keys: readonly K[]): ObjectValidator<{ [Key in keyof Pick]: 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(keys: readonly K[]): ObjectValidator<{ [Key in keyof Omit]: 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 { + 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 { + 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 { + 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].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 +} diff --git a/src/validators/SetValidator.ts b/src/validators/SetValidator.ts index 93faf480..6819c2cb 100644 --- a/src/validators/SetValidator.ts +++ b/src/validators/SetValidator.ts @@ -11,7 +11,11 @@ export class SetValidator extends BaseValidator> { this.validator = validator; } - public run(values: unknown): Result, Error> { + protected override clone(): this { + return Reflect.construct(this.constructor, [this.validator, this.constraints]); + } + + protected handle(values: unknown): Result, ValidationError | AggregateError> { if (!(values instanceof Set)) { return Result.err(new ValidationError('ArrayValidator', 'Expected an array', values)); } @@ -27,18 +31,6 @@ export class SetValidator extends BaseValidator> { return errors.length === 0 // ? Result.ok(transformed) - : Result.err(new AggregateError(errors, 'Could not match any of the defined validators')); - } - - public parse(value: unknown): Set { - return this.run(value).unwrap(); - } - - protected override clone(): this { - return Reflect.construct(this.constructor, [this.validator, this.constraints]); - } - - protected handle(): Result, ValidationError> { - throw new Error('Unreachable'); + : Result.err(new AggregateError(errors, 'Failed to validate at least one entry')); } } diff --git a/src/validators/UnionValidator.ts b/src/validators/UnionValidator.ts index ab8267cc..05dfdfa9 100644 --- a/src/validators/UnionValidator.ts +++ b/src/validators/UnionValidator.ts @@ -73,7 +73,11 @@ export class UnionValidator extends BaseValidator { return new UnionValidator([...this.validators, ...predicates]); } - public override run(value: unknown): Result { + protected override clone(): this { + return Reflect.construct(this.constructor, [this.validators, this.constraints]); + } + + protected handle(value: unknown): Result { const errors: Error[] = []; for (const validator of this.validators) { @@ -84,24 +88,4 @@ export class UnionValidator extends BaseValidator { return Result.err(new AggregateError(errors, 'Could not match any of the defined validators')); } - - public override parse(value: unknown): T { - const results: Error[] = []; - - for (const validator of this.validators) { - const result = validator.run(value); - if (result.isOk()) return result.value; - results.push(result.error!); - } - - throw new AggregateError(results, 'Could not match any of the defined validators'); - } - - protected override clone(): this { - return Reflect.construct(this.constructor, [this.validators, this.constraints]); - } - - protected handle(): Result { - throw new Error('Unreachable'); - } } diff --git a/src/validators/imports.ts b/src/validators/imports.ts index 1f2c9f92..051249f8 100644 --- a/src/validators/imports.ts +++ b/src/validators/imports.ts @@ -9,6 +9,7 @@ export * from './LiteralValidator'; export * from './NeverValidator'; export * from './NullishValidator'; export * from './NumberValidator'; +export * from './ObjectValidator'; export * from './PassthroughValidator'; export * from './SetValidator'; export * from './StringValidator'; diff --git a/tests/validators/object.test.ts b/tests/validators/object.test.ts new file mode 100644 index 00000000..1448cdf1 --- /dev/null +++ b/tests/validators/object.test.ts @@ -0,0 +1,174 @@ +import { MissingPropertyError, s, UnknownPropertyError, ValidationError } from '../../src'; + +describe('ObjectValidator', () => { + const predicate = s.object({ + username: s.string, + password: s.string + }); + + test('GIVEN a non-object value THEN throws ValidationError', () => { + expect(() => predicate.parse('hello')).toThrow( + new ValidationError('ObjectValidator', 'Expected the value to be an object, but received string instead', 'hello') + ); + }); + + test('GIVEN a null object value THEN throws ValidationError', () => { + expect(() => predicate.parse(null)).toThrow(new ValidationError('ObjectValidator', 'Expected the value to not be null', null)); + }); + + test('GIVEN a valid object THEN returns processed object', () => { + expect(predicate.parse({ username: 'Sapphire', password: 'helloworld' })).toStrictEqual({ username: 'Sapphire', password: 'helloworld' }); + }); + + test('GIVEN mismatching in one property THEN throws AggregateError with one error', () => { + expect(() => predicate.parse({ username: 42, password: 'helloworld' })).toThrow( + new AggregateError( + [new ValidationError('StringValidator', 'Expected a string primitive', 42)], + 'Failed to match at least one of the properties' + ) + ); + }); + + test('GIVEN mismatching in two properties THEN throws AggregateError with two errors', () => { + expect(() => predicate.parse({ username: 42, password: true })).toThrow( + new AggregateError( + [ + new ValidationError('StringValidator', 'Expected a string primitive', 42), + new ValidationError('StringValidator', 'Expected a string primitive', true) + ], + 'Failed to match at least one of the properties' + ) + ); + }); + + describe('Strict', () => { + const strictPredicate = predicate.strict; + + test('GIVEN matching keys and values THEN returns no errors', () => { + expect(strictPredicate.parse({ username: 'Sapphire', password: 'helloworld' })).toStrictEqual({ + username: 'Sapphire', + password: 'helloworld' + }); + }); + + test('GIVEN mismatching in one property THEN throws AggregateError with one error', () => { + expect(() => strictPredicate.parse({ username: 42, password: 'helloworld' })).toThrow( + new AggregateError( + [new ValidationError('StringValidator', 'Expected a string primitive', 42)], + 'Failed to match at least one of the properties' + ) + ); + }); + + test('GIVEN mismatching in one property and one unknown key THEN throws AggregateError with two errors', () => { + expect(() => strictPredicate.parse({ username: 42, password: 'helloworld', foo: 'bar' })).toThrow( + new AggregateError( + [new UnknownPropertyError('foo', 'bar'), new ValidationError('StringValidator', 'Expected a string primitive', 42)], + 'Failed to match at least one of the properties' + ) + ); + }); + + test('GIVEN mismatching in one property and one missing key THEN throws AggregateError with two errors', () => { + expect(() => strictPredicate.parse({ username: 42, foo: 'owo' })).toThrow( + new AggregateError( + [new UnknownPropertyError('foo', 'owo'), new MissingPropertyError('password')], + 'Failed to match at least one of the properties' + ) + ); + }); + + const optionalStrict = strictPredicate.extend({ + optionalKey: s.string.optional + }); + + test('GIVEN matching keys and values without optional keys THEN returns no errors', () => { + expect(optionalStrict.parse({ username: 'Sapphire', password: 'helloworld' })).toStrictEqual({ + username: 'Sapphire', + password: 'helloworld', + optionalKey: undefined + }); + }); + }); + + describe('Partial', () => { + const partialPredicate = predicate.partial; + + test('GIVEN empty object THEN returns an object with undefined values', () => { + expect(partialPredicate.parse({})).toStrictEqual({ username: undefined, password: undefined }); + }); + }); + + describe('Extend', () => { + test('GIVEN a plain object THEN returns a predicate validator with merged shapes', () => { + const extendPredicate = predicate.extend({ foo: s.number }); + + expect(Object.keys(extendPredicate.shape)).toStrictEqual(['username', 'password', 'foo']); + expect(extendPredicate.parse({ username: 'Sapphire', password: 'helloworld', foo: 42 })).toStrictEqual({ + username: 'Sapphire', + password: 'helloworld', + foo: 42 + }); + }); + + test('GIVEN an object predicate THEN returns a predicate validator with merged shapes', () => { + const extendPredicate = predicate.extend(s.object({ foo: s.number })); + + expect(Object.keys(extendPredicate.shape)).toStrictEqual(['username', 'password', 'foo']); + expect(extendPredicate.parse({ username: 'Sapphire', password: 'helloworld', foo: 42 })).toStrictEqual({ + username: 'Sapphire', + password: 'helloworld', + foo: 42 + }); + }); + }); + + describe('Pick', () => { + test('GIVEN no keys THEN returns an empty predicate validator', () => { + const pickPredicate = predicate.pick([]); + + expect(Object.keys(pickPredicate.shape)).toStrictEqual([]); + expect(pickPredicate.parse({ username: 'Sapphire', password: 'helloworld' })).toStrictEqual({}); + }); + + test('GIVEN one key THEN returns a subset of the object predicate', () => { + const pickPredicate = predicate.pick(['password']); + + expect(Object.keys(pickPredicate.shape)).toStrictEqual(['password']); + expect(pickPredicate.parse({ username: 'Sapphire', password: 'helloworld' })).toStrictEqual({ password: 'helloworld' }); + }); + + test('GIVEN an unknown key THEN is ignored from the new object predicate', () => { + const pickPredicate = predicate.pick(['password', 'foo' as any]); + + expect(Object.keys(pickPredicate.shape)).toStrictEqual(['password']); + expect(pickPredicate.parse({ username: 'Sapphire', password: 'helloworld' })).toStrictEqual({ password: 'helloworld' }); + }); + }); + + describe('Omit', () => { + test('GIVEN no keys THEN returns a clone of the predicate validator', () => { + const pickPredicate = predicate.omit([]); + + expect(Object.keys(pickPredicate.shape)).toStrictEqual(['username', 'password']); + expect(pickPredicate.parse({ username: 'Sapphire', password: 'helloworld' })).toStrictEqual({ + username: 'Sapphire', + password: 'helloworld' + }); + }); + + test('GIVEN one key THEN returns a subset of the object predicate', () => { + const pickPredicate = predicate.omit(['password']); + + expect(Object.keys(pickPredicate.shape)).toStrictEqual(['username']); + expect(pickPredicate.parse({ username: 'Sapphire', password: 'helloworld' })).toStrictEqual({ username: 'Sapphire' }); + }); + + test('GIVEN an unknown key THEN is ignored from the new object predicate', () => { + const pickPredicate = predicate.omit(['password', 'foo' as any]); + + expect(Object.keys(pickPredicate.shape)).toStrictEqual(['username']); + expect(pickPredicate.parse({ username: 'Sapphire', password: 'helloworld' })).toStrictEqual({ username: 'Sapphire' }); + }); + }); +}); diff --git a/tsconfig.base.json b/tsconfig.base.json index 726b41c5..d5635d1c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,6 +1,7 @@ { "extends": "@sapphire/ts-config", "compilerOptions": { - "target": "ES2021" + "target": "ES2021", + "useDefineForClassFields": false } } diff --git a/typedoc.json b/typedoc.json index 41ceac67..488ffdc3 100644 --- a/typedoc.json +++ b/typedoc.json @@ -3,6 +3,5 @@ "readme": "./README.md", "name": "@sapphire/shapeshift", "entryPoints": ["src/index.ts"], - "excludeInternal": true, "tsconfig": "./src/tsconfig.json" } diff --git a/yarn.lock b/yarn.lock index 6b4839aa..85401827 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,7 +5,7 @@ __metadata: version: 5 cacheKey: 8 -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.7": +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.0, @babel/code-frame@npm:^7.16.7": version: 7.16.7 resolution: "@babel/code-frame@npm:7.16.7" dependencies: @@ -630,6 +630,24 @@ __metadata: languageName: node linkType: hard +"@favware/rollup-type-bundler@npm:^1.0.7": + version: 1.0.7 + resolution: "@favware/rollup-type-bundler@npm:1.0.7" + dependencies: + "@sapphire/utilities": ^3.1.0 + colorette: ^2.0.16 + commander: ^8.3.0 + js-yaml: ^4.1.0 + rollup: ^2.63.0 + rollup-plugin-dts: ^4.1.0 + typescript: ^4.5.4 + bin: + rollup-type-bundler: dist/cli.js + rtb: dist/cli.js + checksum: e42a8c5b4509b78219b477acdd36baedfb97e48265796e3418baba085d0f9e65ce7ccab59c011a4eb320bcf778dfda8ff2a257910044846b02c2ca00baee73ec + languageName: node + linkType: hard + "@gar/promisify@npm:^1.0.1": version: 1.1.2 resolution: "@gar/promisify@npm:1.1.2" @@ -931,17 +949,17 @@ __metadata: linkType: hard "@sapphire/eslint-config@npm:^4.0.9": - version: 4.0.9 - resolution: "@sapphire/eslint-config@npm:4.0.9" + version: 4.0.10 + resolution: "@sapphire/eslint-config@npm:4.0.10" dependencies: - "@typescript-eslint/eslint-plugin": ^5.9.0 - "@typescript-eslint/parser": ^5.9.0 - eslint: ^8.6.0 + "@typescript-eslint/eslint-plugin": ^5.9.1 + "@typescript-eslint/parser": ^5.9.1 + eslint: ~8.6.0 eslint-config-prettier: ^8.3.0 eslint-plugin-prettier: ^4.0.0 prettier: ^2.5.1 typescript: ^4.5.4 - checksum: ffbfbb338fc97a69f5f475a229c224e7d52cfe4b6bcbca3d8e9084d664b3796816e979c177cc554e62417c4988c40d5b40a77192a1973fe997ada9b43854584e + checksum: 7cd426b2893789f9604d3532a7060ff8c3ba63fd241ac1aa04a976d7cfd11d3d5aa33d037eb7740140d70b72166b2b6df37d0611a6e590e5b4a94900702860b3 languageName: node linkType: hard @@ -970,6 +988,7 @@ __metadata: "@commitlint/cli": ^16.0.2 "@commitlint/config-conventional": ^16.0.0 "@favware/npm-deprecate": ^1.0.4 + "@favware/rollup-type-bundler": ^1.0.7 "@sapphire/eslint-config": ^4.0.9 "@sapphire/prettier-config": ^1.2.8 "@sapphire/ts-config": ^3.1.7 @@ -1007,7 +1026,7 @@ __metadata: languageName: node linkType: hard -"@sapphire/utilities@npm:^3.0.1": +"@sapphire/utilities@npm:^3.0.1, @sapphire/utilities@npm:^3.1.0": version: 3.2.0 resolution: "@sapphire/utilities@npm:3.2.0" checksum: cdecc49994a906eebbbe3bb23d61375aa6704ef7f74aee2a3bb348a68eafd54b3785fc5c3853a3951393ddf5d7a205f0a52bbb1a595546e69b82e00061be24fc @@ -1224,7 +1243,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:^5.9.0, @typescript-eslint/eslint-plugin@npm:^5.9.1": +"@typescript-eslint/eslint-plugin@npm:^5.9.1": version: 5.9.1 resolution: "@typescript-eslint/eslint-plugin@npm:5.9.1" dependencies: @@ -1263,7 +1282,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/parser@npm:^5.9.0, @typescript-eslint/parser@npm:^5.9.1": +"@typescript-eslint/parser@npm:^5.9.1": version: 5.9.1 resolution: "@typescript-eslint/parser@npm:5.9.1" dependencies: @@ -2999,58 +3018,13 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^3.0.0, eslint-visitor-keys@npm:^3.1.0, eslint-visitor-keys@npm:^3.2.0": +"eslint-visitor-keys@npm:^3.0.0, eslint-visitor-keys@npm:^3.1.0": version: 3.2.0 resolution: "eslint-visitor-keys@npm:3.2.0" checksum: fdadbb26f9e6417d3db7ad4f00bb0d573b6031c32fa72e8cdae32d038223faaeddff2ee443c90cb489bf774e75bff765c00912b8f9106d65e4f202ccd78c1b18 languageName: node linkType: hard -"eslint@npm:^8.6.0": - version: 8.7.0 - resolution: "eslint@npm:8.7.0" - dependencies: - "@eslint/eslintrc": ^1.0.5 - "@humanwhocodes/config-array": ^0.9.2 - ajv: ^6.10.0 - chalk: ^4.0.0 - cross-spawn: ^7.0.2 - debug: ^4.3.2 - doctrine: ^3.0.0 - escape-string-regexp: ^4.0.0 - eslint-scope: ^7.1.0 - eslint-utils: ^3.0.0 - eslint-visitor-keys: ^3.2.0 - espree: ^9.3.0 - esquery: ^1.4.0 - esutils: ^2.0.2 - fast-deep-equal: ^3.1.3 - file-entry-cache: ^6.0.1 - functional-red-black-tree: ^1.0.1 - glob-parent: ^6.0.1 - globals: ^13.6.0 - ignore: ^5.2.0 - import-fresh: ^3.0.0 - imurmurhash: ^0.1.4 - is-glob: ^4.0.0 - js-yaml: ^4.1.0 - json-stable-stringify-without-jsonify: ^1.0.1 - levn: ^0.4.1 - lodash.merge: ^4.6.2 - minimatch: ^3.0.4 - natural-compare: ^1.4.0 - optionator: ^0.9.1 - regexpp: ^3.2.0 - strip-ansi: ^6.0.1 - strip-json-comments: ^3.1.0 - text-table: ^0.2.0 - v8-compile-cache: ^2.0.3 - bin: - eslint: bin/eslint.js - checksum: 1c80375a48b0fe3ccae3c6354323e4f0e92e970f23abc5b9705b90b7aef514b69ebd0a63e74962d30789986c91fa41c0e25cd2f98f19e9e2a2d36aafdfc9ccc9 - languageName: node - linkType: hard - "eslint@npm:~8.6.0": version: 8.6.0 resolution: "eslint@npm:8.6.0" @@ -5029,6 +5003,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.25.7": + version: 0.25.7 + resolution: "magic-string@npm:0.25.7" + dependencies: + sourcemap-codec: ^1.4.4 + checksum: 727a1fb70f9610304fe384f1df0251eb7d1d9dd779c07ef1225690361b71b216f26f5d934bfb11c919b5b0e7ba50f6240c823a6f2e44cfd33d4a07d7747ca829 + languageName: node + linkType: hard + "make-dir@npm:^3.0.0": version: 3.1.0 resolution: "make-dir@npm:3.1.0" @@ -6170,7 +6153,23 @@ __metadata: languageName: node linkType: hard -"rollup@npm:^2.60.0": +"rollup-plugin-dts@npm:^4.1.0": + version: 4.1.0 + resolution: "rollup-plugin-dts@npm:4.1.0" + dependencies: + "@babel/code-frame": ^7.16.0 + magic-string: ^0.25.7 + peerDependencies: + rollup: ^2.55 + typescript: ~4.1 || ~4.2 || ~4.3 || ~4.4 || ~4.5 + dependenciesMeta: + "@babel/code-frame": + optional: true + checksum: add2715d4906dccb8bbad63e02668303eee553b68e2ae8c7c686abc5ea7936ef316db43845eb12bf978002f461e43b00ecfe61bd2ffedd6c570e12a289de7c18 + languageName: node + linkType: hard + +"rollup@npm:^2.60.0, rollup@npm:^2.63.0": version: 2.64.0 resolution: "rollup@npm:2.64.0" dependencies: @@ -6414,6 +6413,13 @@ __metadata: languageName: node linkType: hard +"sourcemap-codec@npm:^1.4.4": + version: 1.4.8 + resolution: "sourcemap-codec@npm:1.4.8" + checksum: b57981c05611afef31605732b598ccf65124a9fcb03b833532659ac4d29ac0f7bfacbc0d6c5a28a03e84c7510e7e556d758d0bb57786e214660016fb94279316 + languageName: node + linkType: hard + "spdx-correct@npm:^3.0.0": version: 3.1.1 resolution: "spdx-correct@npm:3.1.1"