schemaglobin
Advanced tools
Comparing version 3.3.7 to 4.0.0
@@ -1,2 +0,3 @@ | ||
export * from "./types"; | ||
export { NullIfOptional, FalseIfOptional, EmptyIfOptional, DeepPartial, UnknownObject } from "./types"; | ||
export { SchemaOptions, ValidateFlags, Schema, SchemaType, Schemas, SchemasType } from "./Schema"; | ||
export * from "./schemas/ArraySchema"; | ||
@@ -14,1 +15,2 @@ export * from "./schemas/BooleanSchema"; | ||
export * from "./Invalid"; | ||
export * from "./helpers"; |
@@ -20,1 +20,3 @@ "use strict"; | ||
__export(require("./Invalid")); | ||
// Export helpers. | ||
__export(require("./helpers")); |
@@ -1,2 +0,2 @@ | ||
import { Schema, SchemaWithSchemas, SchemaOptions, SchemaType } from "../types"; | ||
import { Schema, SchemaOptions, SchemaType, ValidateFlags } from "../Schema"; | ||
import { Invalid } from "../Invalid"; | ||
@@ -37,3 +37,3 @@ interface ArrayOptions<S extends Schema = Schema, R extends boolean = false> extends SchemaOptions { | ||
*/ | ||
export declare class ArraySchema<S extends Schema = Schema, R extends boolean = false> implements SchemaWithSchemas<ReadonlyArray<SchemaType<S>>> { | ||
export declare class ArraySchema<S extends Schema = Schema, R extends boolean = false> implements Schema<ReadonlyArray<SchemaType<S>>> { | ||
readonly title: string; | ||
@@ -44,15 +44,10 @@ readonly description: string; | ||
readonly required: R; | ||
/** | ||
* Describe the minimum and maximum numbers of items. | ||
*/ | ||
/** Describe the minimum and maximum numbers of items. */ | ||
readonly min: number | null; | ||
readonly max: number | null; | ||
/** | ||
* Describe the format for _all_ items in the array. | ||
*/ | ||
/** Describe the format for _all_ items in the array. */ | ||
readonly items: S; | ||
constructor({ title, description, placeholder, value, required, min, max, items, }: ArrayOptions<S, R>); | ||
schema(): S; | ||
private coerce; | ||
validate(unsafeValue?: unknown): ReadonlyArray<SchemaType<S>> | Invalid; | ||
validate(unsafeValue?: unknown, flags?: ValidateFlags): ReadonlyArray<SchemaType<S>> | Invalid; | ||
} | ||
@@ -59,0 +54,0 @@ /** Shortcuts for ArraySchema. */ |
@@ -33,5 +33,3 @@ "use strict"; | ||
constructor({ title = "", description = "", placeholder = "", value = [], required = false, min = 0, max = null, items, }) { | ||
/** | ||
* Describe the minimum and maximum numbers of items. | ||
*/ | ||
/** Describe the minimum and maximum numbers of items. */ | ||
this.min = null; | ||
@@ -48,6 +46,2 @@ this.max = null; | ||
} | ||
// Implement SchemasSchema | ||
schema() { | ||
return this.items; | ||
} | ||
coerce(value) { | ||
@@ -60,3 +54,3 @@ if (!value) | ||
} | ||
validate(unsafeValue = this.value) { | ||
validate(unsafeValue = this.value, flags = {}) { | ||
// Coorce. | ||
@@ -89,3 +83,3 @@ const unsafeArray = this.coerce(unsafeValue); | ||
const current = unsafeArray[i]; | ||
const value = items.validate(current); | ||
const value = items.validate(current, flags); | ||
if (value instanceof Invalid_1.Invalid) { | ||
@@ -92,0 +86,0 @@ invalid = true; |
@@ -1,2 +0,3 @@ | ||
import { Schema, SchemaOptions, FalseIfOptional } from "../types"; | ||
import type { Schema, SchemaOptions } from "../Schema"; | ||
import type { FalseIfOptional } from "../types"; | ||
import { Invalid } from "../Invalid"; | ||
@@ -3,0 +4,0 @@ interface BooleanOptions<R extends boolean> extends SchemaOptions { |
@@ -1,2 +0,3 @@ | ||
import { Schema, SchemaOptions, NullIfOptional } from "../types"; | ||
import type { Schema, SchemaOptions } from "../Schema"; | ||
import type { NullIfOptional } from "../types"; | ||
import { Invalid } from "../Invalid"; | ||
@@ -3,0 +4,0 @@ /** Convert an unknown value to a date string (or return Invalid) */ |
@@ -1,2 +0,3 @@ | ||
import { Schema, SchemaOptions, NullIfOptional } from "../types"; | ||
import type { Schema, SchemaOptions } from "../Schema"; | ||
import type { NullIfOptional } from "../types"; | ||
import { Invalid } from "../Invalid"; | ||
@@ -3,0 +4,0 @@ interface EmailOptions<R extends boolean = false> extends SchemaOptions { |
@@ -1,2 +0,3 @@ | ||
import { Schema, SchemaOptions, NullIfOptional } from "../types"; | ||
import type { Schema, SchemaOptions } from "../Schema"; | ||
import type { NullIfOptional } from "../types"; | ||
import { Invalid } from "../Invalid"; | ||
@@ -3,0 +4,0 @@ interface KeyOptions<R extends boolean = false> extends SchemaOptions { |
@@ -1,2 +0,2 @@ | ||
import { Schema, SchemaWithSchemas, SchemaOptions, SchemaType } from "../types"; | ||
import type { Schema, SchemaOptions, SchemaType, ValidateFlags } from "../Schema"; | ||
import { Invalid } from "../Invalid"; | ||
@@ -14,3 +14,3 @@ interface MapOptions<S extends Schema = Schema, R extends boolean = false> extends SchemaOptions { | ||
*/ | ||
export declare class MapSchema<S extends Schema = Schema, R extends boolean = false> implements SchemaWithSchemas<Readonly<Record<string, SchemaType<S>>>> { | ||
export declare class MapSchema<S extends Schema = Schema, R extends boolean = false> implements Schema<Readonly<Record<string, SchemaType<S>>>> { | ||
readonly title: string; | ||
@@ -33,5 +33,4 @@ readonly description: string; | ||
constructor({ title, description, placeholder, value, required, items, min, max, }: MapOptions<S, R>); | ||
schema(): S; | ||
private coerce; | ||
validate(unsafeValue?: unknown): Readonly<Record<string, SchemaType<S>>> | Invalid; | ||
validate(unsafeValue?: unknown, flags?: ValidateFlags): Readonly<Record<string, SchemaType<S>>> | Invalid; | ||
} | ||
@@ -38,0 +37,0 @@ /** Shortcuts for MapSchema. */ |
@@ -25,6 +25,2 @@ "use strict"; | ||
} | ||
// Implement SchemasSchema | ||
schema() { | ||
return this.items; | ||
} | ||
coerce(value) { | ||
@@ -37,3 +33,3 @@ if (!value) | ||
} | ||
validate(unsafeValue = this.value) { | ||
validate(unsafeValue = this.value, flags = {}) { | ||
// Coorce. | ||
@@ -66,3 +62,3 @@ const unsafeObject = this.coerce(unsafeValue); | ||
// Validate the value. | ||
const value = this.items.validate(current); | ||
const value = this.items.validate(current, flags); | ||
if (value instanceof Invalid_1.Invalid) { | ||
@@ -69,0 +65,0 @@ invalid = true; |
@@ -1,2 +0,3 @@ | ||
import { Schema, SchemaOptions, NullIfOptional } from "../types"; | ||
import type { Schema, SchemaOptions } from "../Schema"; | ||
import type { NullIfOptional } from "../types"; | ||
import { Invalid } from "../Invalid"; | ||
@@ -3,0 +4,0 @@ interface NumberOptions<T extends number = number, R extends boolean = false> extends SchemaOptions { |
@@ -1,6 +0,5 @@ | ||
import { Schema, SchemaWithSchemas, SchemaOptions, SchemaType, NullIfOptional } from "../types"; | ||
import type { Schema, SchemaType, Schemas, SchemasType, SchemaOptions, ValidateFlags } from "../Schema"; | ||
import type { NullIfOptional, DeepPartial } from "../types"; | ||
import { Invalid } from "../Invalid"; | ||
interface ObjectOptions<S extends { | ||
[prop: string]: Schema; | ||
} = {}, R extends boolean = false> extends SchemaOptions { | ||
interface ObjectOptions<S extends Schemas = Schemas, R extends boolean = false> extends SchemaOptions { | ||
readonly props: S; | ||
@@ -12,2 +11,5 @@ readonly value?: Partial<{ | ||
} | ||
interface ObjectFlags extends ValidateFlags { | ||
partial?: boolean; | ||
} | ||
/** | ||
@@ -19,7 +21,3 @@ * Schema that defines a valid object. | ||
*/ | ||
export declare class ObjectSchema<S extends { | ||
[prop: string]: Schema; | ||
} = {}, R extends boolean = false> implements SchemaWithSchemas<Readonly<{ | ||
[K in keyof S]: SchemaType<S[K]>; | ||
}> | NullIfOptional<R>> { | ||
export declare class ObjectSchema<S extends Schemas = Schemas, R extends boolean = false> implements Schema<SchemasType<S> | NullIfOptional<R>> { | ||
readonly title: string; | ||
@@ -39,18 +37,6 @@ readonly description: string; | ||
private coerce; | ||
validate(unsafeValue?: unknown): Readonly<{ | ||
[K in keyof S]: SchemaType<S[K]>; | ||
}> | NullIfOptional<R> | Invalid; | ||
/** | ||
* Like `validate()` method but works on a Partial value. | ||
* e.g. Missing properties in the object don't cause Invalid to be thrown. | ||
* | ||
* - Recursive! Nested ObjectSchema instances are also partially validated. | ||
* - Props that don't appear in `options.props` are silently removed. | ||
* | ||
* @returns The valid partial value, or an Invalid summarising the issues. | ||
*/ | ||
partialValidate(unsafeValue: unknown): Readonly<Partial<{ | ||
[K in keyof S]: Partial<SchemaType<S[K]>>; | ||
}>> | NullIfOptional<R> | Invalid; | ||
schema<K extends keyof S & string>(key: K): S[K]; | ||
validate(unsafeValue: unknown, flags: { | ||
partial: true; | ||
} & ObjectFlags): DeepPartial<SchemasType<S>> | NullIfOptional<R> | Invalid; | ||
validate(unsafeValue?: unknown, flags?: ObjectFlags): SchemasType<S> | NullIfOptional<R> | Invalid; | ||
} | ||
@@ -57,0 +43,0 @@ /** Shortcuts for ObjectSchema. */ |
"use strict"; | ||
/* eslint-disable no-dupe-class-members */ | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const Invalid_1 = require("../Invalid"); | ||
// Whether we're in 'partial' mode or not. | ||
let partial = false; | ||
/** | ||
@@ -28,3 +27,3 @@ * Schema that defines a valid object. | ||
} | ||
validate(unsafeValue = this.value) { | ||
validate(unsafeValue = this.value, flags = {}) { | ||
// Coorce. | ||
@@ -48,3 +47,3 @@ const unsafeObj = this.coerce(unsafeValue); | ||
const invalids = {}; | ||
if (partial) { | ||
if (flags.partial) { | ||
Object.entries(unsafeObj).forEach(([key, unsafeProp]) => { | ||
@@ -54,3 +53,3 @@ if (key in this.props) { | ||
const schema = this.props[key]; | ||
const safeProp = schema.validate(unsafeProp); | ||
const safeProp = schema.validate(unsafeProp, flags); | ||
if (safeProp instanceof Invalid_1.Invalid) { | ||
@@ -76,3 +75,3 @@ invalid = true; | ||
const unsafeProp = unsafeObj[key]; | ||
const safeProp = schema.validate(unsafeProp); | ||
const safeProp = schema.validate(unsafeProp, flags); | ||
if (safeProp instanceof Invalid_1.Invalid) { | ||
@@ -98,27 +97,2 @@ invalid = true; | ||
} | ||
/** | ||
* Like `validate()` method but works on a Partial value. | ||
* e.g. Missing properties in the object don't cause Invalid to be thrown. | ||
* | ||
* - Recursive! Nested ObjectSchema instances are also partially validated. | ||
* - Props that don't appear in `options.props` are silently removed. | ||
* | ||
* @returns The valid partial value, or an Invalid summarising the issues. | ||
*/ | ||
partialValidate(unsafeValue) { | ||
try { | ||
partial = true; | ||
const value = this.validate(unsafeValue); | ||
partial = false; | ||
return value; | ||
} | ||
catch (err) { | ||
partial = false; | ||
throw err; | ||
} | ||
} | ||
// Implement SchemasSchema | ||
schema(key) { | ||
return this.props[key]; | ||
} | ||
} | ||
@@ -125,0 +99,0 @@ exports.ObjectSchema = ObjectSchema; |
@@ -1,2 +0,3 @@ | ||
import { Schema, SchemaOptions, NullIfOptional } from "../types"; | ||
import type { Schema, SchemaOptions } from "../Schema"; | ||
import type { NullIfOptional } from "../types"; | ||
import { Invalid } from "../Invalid"; | ||
@@ -3,0 +4,0 @@ interface PhoneOptions<R extends boolean = false> extends SchemaOptions { |
@@ -1,2 +0,3 @@ | ||
import { Schema, SchemaOptions, EmptyIfOptional } from "../types"; | ||
import type { Schema, SchemaOptions } from "../Schema"; | ||
import type { EmptyIfOptional } from "../types"; | ||
import { Invalid } from "../Invalid"; | ||
@@ -3,0 +4,0 @@ interface StringOptions<T extends string = string, R extends boolean = false> extends SchemaOptions { |
@@ -1,2 +0,3 @@ | ||
import { Schema, SchemaOptions, NullIfOptional } from "../types"; | ||
import type { NullIfOptional } from "../types"; | ||
import type { Schema, SchemaOptions } from "../Schema"; | ||
import { Invalid } from "../Invalid"; | ||
@@ -3,0 +4,0 @@ interface UrlOptions<R extends boolean = false> extends SchemaOptions { |
{ | ||
"name": "schemaglobin", | ||
"description": "Validate user-entered data.", | ||
"version": "3.3.7", | ||
"version": "4.0.0", | ||
"repository": "https://github.com/dhoulb/schemaglobin", | ||
@@ -17,7 +17,7 @@ "author": "Dave Houlbrooke <dave@shax.com>", | ||
"fix": "npm run fix:prettier && npm run fix:eslint", | ||
"fix:prettier": "prettier --write \"./**/*\"", | ||
"fix:eslint": "eslint --fix \"./**/*\"", | ||
"fix:prettier": "prettier --write './**/*.{md,json}'", | ||
"fix:eslint": "eslint --fix './**/*.{ts,tsx}'", | ||
"test": "npm run test:prettier && npm run test:eslint && npm run test:typescript && npm run test:jest", | ||
"test:prettier": "prettier --check \"./**/*\"", | ||
"test:eslint": "eslint \"./**/*\"", | ||
"test:prettier": "prettier --check './**/*.{md,json}'", | ||
"test:eslint": "eslint './**/*.{ts,tsx}'", | ||
"test:typescript": "tsc --noEmit", | ||
@@ -30,13 +30,13 @@ "test:jest": "jest", | ||
"@types/jest": "^25.1.1", | ||
"@typescript-eslint/eslint-plugin": "^2.22.0", | ||
"@typescript-eslint/parser": "^2.22.0", | ||
"eslint": "^6.7.1", | ||
"eslint-config-prettier": "^6.10.0", | ||
"@typescript-eslint/eslint-plugin": "^2.32.0", | ||
"@typescript-eslint/parser": "^2.32.0", | ||
"eslint": "^6.8.0", | ||
"eslint-config-prettier": "^6.11.0", | ||
"eslint-plugin-import": "^2.20.1", | ||
"eslint-plugin-prettier": "^3.1.1", | ||
"jest": "^25.1.0", | ||
"prettier": "^1.19.1", | ||
"ts-jest": "^25.0.0", | ||
"jest": "^25.5.4", | ||
"prettier": "^2.0.5", | ||
"ts-jest": "^25.5.1", | ||
"typescript": "^3.8.3" | ||
} | ||
} |
@@ -233,4 +233,3 @@ # Schemaglobin: Validate unknown user input against schemas | ||
- `ObjectSchema.validate()` | ||
- Normally returns `{}` | ||
- If `options.props` is set it can return a more specific type, e.g.`{ a: string, b: number }` | ||
- If `flags.partial` is set it returns a deep partial of the object (i.e. all props become optional). | ||
@@ -289,2 +288,25 @@ ```ts | ||
### Validation flags | ||
The second argument passed to `validate()` is an options bag of flags which can modify the validation behaviour. For example the `flags.partial` flag makes `ObjectSchema` validation _partial_ (i.e. missing properties are not invalid). | ||
```ts | ||
import { object, string, number } from "schemaglobin"; | ||
const schema = object.required({{ | ||
name: string.required, | ||
age: number.required, | ||
job: string.required, | ||
}); | ||
// Normally a partial value is invalid... | ||
const invalid = schema.validate({ name: "Dave", age: "35" }); | ||
console.log(invalid.message); // "Invalid format" | ||
console.log(invalid.messages); // { job: "Must be string" } | ||
// But with the partial flag the value is valid. | ||
const valid = schema.validate({ name: "Dave", age: "35" }); | ||
console.log(valid); // { name: "Dave", age: 35 } | ||
``` | ||
## Reference | ||
@@ -456,6 +478,5 @@ | ||
`ObjectSchema` instances also provide the following methods: | ||
`validate()` on `ObjectSchema` instances supports the following flags: | ||
- `partialValidate(value: Partial<T>)` - Validate a _partial_ object where some properties are missing (normal `validate()` would return `Invalid` if required properties were missing). | ||
- `schema(key: string)` - Get the subschema from `options.props` that corresponds to `key` | ||
- `flags.partial` - Validate a partial object, i.e. everything in `options.props` becomes optional, and missing props are not added to the returned value (works deeply/recursively too because `flags` is passed down to subschemas). | ||
@@ -462,0 +483,0 @@ ### `array()` |
// Export all types. | ||
export * from "./types"; | ||
export { NullIfOptional, FalseIfOptional, EmptyIfOptional, DeepPartial, UnknownObject } from "./types"; | ||
export { SchemaOptions, ValidateFlags, Schema, SchemaType, Schemas, SchemasType } from "./Schema"; | ||
@@ -19,1 +20,4 @@ // Export all schemas. | ||
export * from "./Invalid"; | ||
// Export helpers. | ||
export * from "./helpers"; |
@@ -1,2 +0,2 @@ | ||
import { Schema, SchemaWithSchemas, SchemaOptions, SchemaType } from "../types"; | ||
import { Schema, SchemaOptions, SchemaType, ValidateFlags } from "../Schema"; | ||
import { Invalid } from "../Invalid"; | ||
@@ -41,3 +41,3 @@ | ||
export class ArraySchema<S extends Schema = Schema, R extends boolean = false> | ||
implements SchemaWithSchemas<ReadonlyArray<SchemaType<S>>> { | ||
implements Schema<ReadonlyArray<SchemaType<S>>> { | ||
readonly title: string; | ||
@@ -49,11 +49,7 @@ readonly description: string; | ||
/** | ||
* Describe the minimum and maximum numbers of items. | ||
*/ | ||
/** Describe the minimum and maximum numbers of items. */ | ||
readonly min: number | null = null; | ||
readonly max: number | null = null; | ||
/** | ||
* Describe the format for _all_ items in the array. | ||
*/ | ||
/** Describe the format for _all_ items in the array. */ | ||
readonly items: S; | ||
@@ -81,7 +77,2 @@ | ||
// Implement SchemasSchema | ||
schema(): S { | ||
return this.items; | ||
} | ||
private coerce(value: unknown): unknown[] | Invalid { | ||
@@ -93,3 +84,3 @@ if (!value) return []; // Convert falsy to empty array. | ||
validate(unsafeValue: unknown = this.value): ReadonlyArray<SchemaType<S>> | Invalid { | ||
validate(unsafeValue: unknown = this.value, flags: ValidateFlags = {}): ReadonlyArray<SchemaType<S>> | Invalid { | ||
// Coorce. | ||
@@ -125,3 +116,3 @@ const unsafeArray = this.coerce(unsafeValue); | ||
const current = unsafeArray[i]; | ||
const value = items.validate(current); | ||
const value = items.validate(current, flags); | ||
if (value instanceof Invalid) { | ||
@@ -128,0 +119,0 @@ invalid = true; |
@@ -1,2 +0,3 @@ | ||
import { Schema, SchemaOptions, FalseIfOptional } from "../types"; | ||
import type { Schema, SchemaOptions } from "../Schema"; | ||
import type { FalseIfOptional } from "../types"; | ||
import { Invalid } from "../Invalid"; | ||
@@ -3,0 +4,0 @@ |
@@ -1,2 +0,3 @@ | ||
import { Schema, SchemaOptions, NullIfOptional } from "../types"; | ||
import type { Schema, SchemaOptions } from "../Schema"; | ||
import type { NullIfOptional } from "../types"; | ||
import { Invalid } from "../Invalid"; | ||
@@ -3,0 +4,0 @@ |
@@ -1,2 +0,3 @@ | ||
import { Schema, SchemaOptions, NullIfOptional } from "../types"; | ||
import type { Schema, SchemaOptions } from "../Schema"; | ||
import type { NullIfOptional } from "../types"; | ||
import { Invalid } from "../Invalid"; | ||
@@ -3,0 +4,0 @@ |
@@ -1,2 +0,3 @@ | ||
import { Schema, SchemaOptions, NullIfOptional } from "../types"; | ||
import type { Schema, SchemaOptions } from "../Schema"; | ||
import type { NullIfOptional } from "../types"; | ||
import { Invalid } from "../Invalid"; | ||
@@ -3,0 +4,0 @@ |
@@ -1,2 +0,2 @@ | ||
import { Schema, SchemaWithSchemas, SchemaOptions, SchemaType } from "../types"; | ||
import type { Schema, SchemaOptions, SchemaType, ValidateFlags } from "../Schema"; | ||
import { Invalid } from "../Invalid"; | ||
@@ -18,3 +18,3 @@ | ||
export class MapSchema<S extends Schema = Schema, R extends boolean = false> | ||
implements SchemaWithSchemas<Readonly<Record<string, SchemaType<S>>>> { | ||
implements Schema<Readonly<Record<string, SchemaType<S>>>> { | ||
readonly title: string; | ||
@@ -59,7 +59,2 @@ readonly description: string; | ||
// Implement SchemasSchema | ||
schema(): S { | ||
return this.items; | ||
} | ||
private coerce(value: unknown): Record<string, unknown> | Invalid { | ||
@@ -71,3 +66,6 @@ if (!value) return {}; // Convert falsy to empty object. | ||
validate(unsafeValue: unknown = this.value): Readonly<Record<string, SchemaType<S>>> | Invalid { | ||
validate( | ||
unsafeValue: unknown = this.value, | ||
flags: ValidateFlags = {}, | ||
): Readonly<Record<string, SchemaType<S>>> | Invalid { | ||
// Coorce. | ||
@@ -101,3 +99,3 @@ const unsafeObject = this.coerce(unsafeValue); | ||
// Validate the value. | ||
const value = this.items.validate(current); | ||
const value = this.items.validate(current, flags); | ||
if (value instanceof Invalid) { | ||
@@ -104,0 +102,0 @@ invalid = true; |
@@ -1,2 +0,3 @@ | ||
import { Schema, SchemaOptions, NullIfOptional } from "../types"; | ||
import type { Schema, SchemaOptions } from "../Schema"; | ||
import type { NullIfOptional } from "../types"; | ||
import { Invalid } from "../Invalid"; | ||
@@ -3,0 +4,0 @@ |
@@ -1,9 +0,9 @@ | ||
import { Schema, SchemaWithSchemas, SchemaOptions, SchemaType, NullIfOptional } from "../types"; | ||
/* eslint-disable no-dupe-class-members */ | ||
import type { Schema, SchemaType, Schemas, SchemasType, SchemaOptions, ValidateFlags } from "../Schema"; | ||
import type { NullIfOptional, DeepPartial, UnknownObject } from "../types"; | ||
import { Invalid } from "../Invalid"; | ||
// Object with only string keys and unknown values. | ||
type UnknownObject = { [key: string]: unknown }; | ||
// Options. | ||
interface ObjectOptions<S extends { [prop: string]: Schema } = {}, R extends boolean = false> extends SchemaOptions { | ||
interface ObjectOptions<S extends Schemas = Schemas, R extends boolean = false> extends SchemaOptions { | ||
readonly props: S; | ||
@@ -14,4 +14,6 @@ readonly value?: Partial<{ [K in keyof S]: SchemaType<S[K]> }> | NullIfOptional<R>; | ||
// Whether we're in 'partial' mode or not. | ||
let partial = false; | ||
// Flags. | ||
interface ObjectFlags extends ValidateFlags { | ||
partial?: boolean; | ||
} | ||
@@ -24,4 +26,4 @@ /** | ||
*/ | ||
export class ObjectSchema<S extends { [prop: string]: Schema } = {}, R extends boolean = false> | ||
implements SchemaWithSchemas<Readonly<{ [K in keyof S]: SchemaType<S[K]> }> | NullIfOptional<R>> { | ||
export class ObjectSchema<S extends Schemas = Schemas, R extends boolean = false> | ||
implements Schema<SchemasType<S> | NullIfOptional<R>> { | ||
readonly title: string; | ||
@@ -62,4 +64,10 @@ readonly description: string; | ||
validate( | ||
unsafeValue: unknown, | ||
flags: { partial: true } & ObjectFlags, | ||
): DeepPartial<SchemasType<S>> | NullIfOptional<R> | Invalid; | ||
validate(unsafeValue?: unknown, flags?: ObjectFlags): SchemasType<S> | NullIfOptional<R> | Invalid; | ||
validate( | ||
unsafeValue: unknown = this.value, | ||
): Readonly<{ [K in keyof S]: SchemaType<S[K]> }> | NullIfOptional<R> | Invalid { | ||
flags: ObjectFlags = {}, | ||
): DeepPartial<SchemasType<S>> | SchemasType<S> | NullIfOptional<R> | Invalid { | ||
// Coorce. | ||
@@ -84,3 +92,3 @@ const unsafeObj = this.coerce(unsafeValue); | ||
const invalids: { [key: string]: string } = {}; | ||
if (partial) { | ||
if (flags.partial) { | ||
Object.entries(unsafeObj).forEach(([key, unsafeProp]) => { | ||
@@ -90,3 +98,3 @@ if (key in this.props) { | ||
const schema = this.props[key]; | ||
const safeProp = schema.validate(unsafeProp); | ||
const safeProp = schema.validate(unsafeProp, flags); | ||
if (safeProp instanceof Invalid) { | ||
@@ -108,3 +116,3 @@ invalid = true; | ||
const unsafeProp = unsafeObj[key]; | ||
const safeProp = schema.validate(unsafeProp); | ||
const safeProp = schema.validate(unsafeProp, flags); | ||
if (safeProp instanceof Invalid) { | ||
@@ -127,32 +135,4 @@ invalid = true; | ||
// Return immuatably (return output if changes were made, or exact input otherwise). | ||
return (changed ? safeObj : unsafeObj) as { [K in keyof S]: SchemaType<S[K]> }; | ||
return (changed ? safeObj : unsafeObj) as DeepPartial<SchemasType<S>> | SchemasType<S>; | ||
} | ||
/** | ||
* Like `validate()` method but works on a Partial value. | ||
* e.g. Missing properties in the object don't cause Invalid to be thrown. | ||
* | ||
* - Recursive! Nested ObjectSchema instances are also partially validated. | ||
* - Props that don't appear in `options.props` are silently removed. | ||
* | ||
* @returns The valid partial value, or an Invalid summarising the issues. | ||
*/ | ||
partialValidate( | ||
unsafeValue: unknown, | ||
): Readonly<Partial<{ [K in keyof S]: Partial<SchemaType<S[K]>> }>> | NullIfOptional<R> | Invalid { | ||
try { | ||
partial = true; | ||
const value = this.validate(unsafeValue); | ||
partial = false; | ||
return value; | ||
} catch (err) { | ||
partial = false; | ||
throw err; | ||
} | ||
} | ||
// Implement SchemasSchema | ||
schema<K extends keyof S & string>(key: K): S[K] { | ||
return this.props[key]; | ||
} | ||
} | ||
@@ -159,0 +139,0 @@ |
@@ -1,2 +0,3 @@ | ||
import { Schema, SchemaOptions, NullIfOptional } from "../types"; | ||
import type { Schema, SchemaOptions } from "../Schema"; | ||
import type { NullIfOptional } from "../types"; | ||
import { Invalid } from "../Invalid"; | ||
@@ -3,0 +4,0 @@ |
/* eslint-disable no-control-regex */ | ||
import { Schema, SchemaOptions, EmptyIfOptional } from "../types"; | ||
import type { Schema, SchemaOptions } from "../Schema"; | ||
import type { EmptyIfOptional } from "../types"; | ||
import { Invalid } from "../Invalid"; | ||
@@ -5,0 +6,0 @@ |
@@ -1,2 +0,3 @@ | ||
import { Schema, SchemaOptions, NullIfOptional } from "../types"; | ||
import type { NullIfOptional } from "../types"; | ||
import type { Schema, SchemaOptions } from "../Schema"; | ||
import { Invalid } from "../Invalid"; | ||
@@ -3,0 +4,0 @@ |
@@ -8,5 +8,4 @@ import { | ||
ObjectSchema, | ||
NullIfOptional, | ||
Invalid, | ||
SchemaType, | ||
DeepPartial, | ||
} from "../../src"; | ||
@@ -16,3 +15,5 @@ | ||
describe("ObjectSchema", () => { | ||
test("TypeScript", () => { | ||
const PARTIAL = { partial: true } as const; | ||
test("TypeScript (no partial flag)", () => { | ||
const s1: ObjectSchema<{ num: NumberSchema<number, false> }, false> = object.optional({ | ||
@@ -23,6 +24,4 @@ num: new NumberSchema({ required: false }), | ||
const r1: { num: number | null } | null | Invalid = s1.validate(); | ||
const ss1a: NumberSchema<number, false> = s1.schema("num"); | ||
const sr1a: number | null | Invalid = ss1a.validate(); | ||
const ss1b: NumberSchema<number, false> = s1.props.num; | ||
const sr1b: number | null | Invalid = ss1b.validate(); | ||
const ss1: NumberSchema<number, false> = s1.props.num; | ||
const sr1: number | null | Invalid = ss1.validate(); | ||
const s2: ObjectSchema<{ num: NumberSchema<number, true> }, true> = object.required({ | ||
@@ -33,6 +32,4 @@ num: new NumberSchema({ required: true }), | ||
const r2: { num: number } | Invalid = s2.validate(); | ||
const ss2a: NumberSchema<number, true> = s2.schema("num"); | ||
const sr2a: number | Invalid = ss2a.validate(); | ||
const ss2b: NumberSchema<number, true> = s2.props.num; | ||
const sr2b: number | Invalid = ss2b.validate(); | ||
const ss2: NumberSchema<number, true> = s2.props.num; | ||
const sr2: number | Invalid = ss2.validate(); | ||
const s3: ObjectSchema<{ num: NumberSchema<number, true> }, true> = object({ | ||
@@ -42,3 +39,3 @@ props: { num: number.required }, | ||
}); | ||
const v3: Record<string, number> | Invalid = s3.validate(); | ||
const v3: { num: number } | Invalid = s3.validate(); | ||
const s4: ObjectSchema<{ num: NumberSchema<number, true> }, false> = object({ | ||
@@ -48,11 +45,11 @@ props: { num: number.required }, | ||
}); | ||
const v4: Record<string, number> | null | Invalid = s4.validate(); | ||
const v4: { num: number } | null | Invalid = s4.validate(); | ||
const s5: ObjectSchema<{ num: NumberSchema<number, true> }, false> = object({ | ||
props: { num: number.required }, | ||
}); | ||
const v5: Record<string, number> | null | Invalid = s5.validate(); | ||
const v5: { num: number } | null | Invalid = s5.validate(); | ||
const s6: ObjectSchema<{ num: NumberSchema<number, true> }, false> = new ObjectSchema({ | ||
props: { num: number.required }, | ||
}); | ||
const v6: Record<string, number> | null | Invalid = s6.validate(); | ||
const v6: { num: number } | null | Invalid = s6.validate(); | ||
const s7: ObjectSchema<{ num: NumberSchema<number, true> }, false> = new ObjectSchema({ | ||
@@ -62,3 +59,3 @@ props: { num: number.required }, | ||
}); | ||
const v7: Record<string, number> | null | Invalid = s7.validate(); | ||
const v7: { num: number } | null | Invalid = s7.validate(); | ||
const s8: ObjectSchema<{ num: NumberSchema<number, true> }, true> = new ObjectSchema({ | ||
@@ -68,4 +65,42 @@ props: { num: number.required }, | ||
}); | ||
const v8: Record<string, number> | null | Invalid = s8.validate(); | ||
const v8: { num: number } | null | Invalid = s8.validate(); | ||
}); | ||
test("TypeScript (partial flag)", () => { | ||
const s1: ObjectSchema<{ num: NumberSchema<number, false> }, false> = object.optional({ | ||
num: new NumberSchema({ required: false }), | ||
}); | ||
const r1: { num?: number | null } | null | Invalid = s1.validate({}, PARTIAL); | ||
const s2: ObjectSchema<{ num: NumberSchema<number, true> }, true> = object.required({ | ||
num: new NumberSchema({ required: true }), | ||
}); | ||
const r2: { num?: number } | Invalid = s2.validate({}, PARTIAL); | ||
const s3: ObjectSchema<{ num: NumberSchema<number, true> }, true> = object({ | ||
props: { num: number.required }, | ||
required: true, | ||
}); | ||
const v3: { num?: number } | Invalid = s3.validate({}, PARTIAL); | ||
const s4: ObjectSchema<{ num: NumberSchema<number, true> }, false> = object({ | ||
props: { num: number.required }, | ||
required: false, | ||
}); | ||
const v4: { num?: number } | null | Invalid = s4.validate({}, PARTIAL); | ||
const s5: ObjectSchema<{ num: NumberSchema<number, true> }, false> = object({ | ||
props: { num: number.required }, | ||
}); | ||
const v5: { num?: number } | null | Invalid = s5.validate({}, PARTIAL); | ||
const s6: ObjectSchema<{ num: NumberSchema<number, true> }, false> = new ObjectSchema({ | ||
props: { num: number.required }, | ||
}); | ||
const v6: { num?: number } | null | Invalid = s6.validate({}, PARTIAL); | ||
const s7: ObjectSchema<{ num: NumberSchema<number, true> }, false> = new ObjectSchema({ | ||
props: { num: number.required }, | ||
required: false, | ||
}); | ||
const v7: DeepPartial<{ num: number }> | null | Invalid = s7.validate({}, PARTIAL); | ||
const s8: ObjectSchema<{ num: NumberSchema<number, true> }, true> = new ObjectSchema({ | ||
props: { num: number.required }, | ||
required: true, | ||
}); | ||
const v8: { num?: number } | null | Invalid = s8.validate({}, PARTIAL); | ||
}); | ||
test("Constructs correctly", () => { | ||
@@ -86,3 +121,3 @@ const props = {}; | ||
}); | ||
describe("validate()", () => { | ||
describe("validate() (no partial flag)", () => { | ||
test("Non-objects throw error", () => { | ||
@@ -215,34 +250,33 @@ const schema = object({ props: {} }); | ||
}); | ||
describe("partialValidate()", () => { | ||
test("Non-objects throw error", () => { | ||
describe("validate() (partial flag)", () => { | ||
test("Partial non-objects throw error", () => { | ||
const schema = object({ props: {} }); | ||
expect(schema.partialValidate("abc")).toEqual(new Invalid("Must be object")); | ||
expect(schema.partialValidate(123)).toEqual(new Invalid("Must be object")); | ||
expect(schema.partialValidate(true)).toEqual(new Invalid("Must be object")); | ||
expect(schema.validate("abc", PARTIAL)).toEqual(new Invalid("Must be object")); | ||
expect(schema.validate(123, PARTIAL)).toEqual(new Invalid("Must be object")); | ||
expect(schema.validate(true, PARTIAL)).toEqual(new Invalid("Must be object")); | ||
}); | ||
test("Falsy values return null", () => { | ||
test("Partial falsy values return null", () => { | ||
const schema = object({ props: {} }); | ||
expect(schema.partialValidate(0)).toBe(null); | ||
expect(schema.partialValidate(null)).toBe(null); | ||
expect(schema.partialValidate(false)).toBe(null); | ||
expect(schema.validate(0, PARTIAL)).toBe(null); | ||
expect(schema.validate(null, PARTIAL)).toBe(null); | ||
expect(schema.validate(false, PARTIAL)).toBe(null); | ||
}); | ||
describe("options.required", () => { | ||
test("Required null objects return Required", () => { | ||
test("Partial required null objects return Required", () => { | ||
const schema = object({ props: {}, required: true }); | ||
expect(schema.partialValidate(null)).toEqual(new Invalid("Required")); | ||
expect(schema.validate(null, PARTIAL)).toEqual(new Invalid("Required")); | ||
}); | ||
test("Required non-null objects are not invalid", () => { | ||
test("Partial required non-null objects are not invalid", () => { | ||
const schema = object({ props: {}, required: true }); | ||
const obj = {}; | ||
expect(schema.partialValidate(obj)).toBe(obj); | ||
expect(schema.validate(obj, PARTIAL)).toBe(obj); | ||
}); | ||
test("Non-required empty objects do not return Required", () => { | ||
test("Partial non-required empty objects do not return Required", () => { | ||
const schema = object({ props: {}, required: false }); | ||
const obj = {}; | ||
expect(schema.partialValidate(obj)).toBe(obj); | ||
expect(schema.validate(obj, PARTIAL)).toBe(obj); | ||
}); | ||
}); | ||
describe("options.props", () => { | ||
test("Object with no missing props that validates is returned unchanged", () => { | ||
test("Partial object with no missing props that validates is returned unchanged", () => { | ||
const a = { num: 123, str: "abc", bool: true }; | ||
@@ -256,5 +290,5 @@ const schema = object({ | ||
}); | ||
expect(schema.partialValidate(a)).toBe(a); | ||
expect(schema.validate(a, PARTIAL)).toBe(a); | ||
}); | ||
test("Object with missing props that validates is returned unchanged", () => { | ||
test("Partial object with missing props that validates is returned unchanged", () => { | ||
const a = { num: 123 }; | ||
@@ -268,5 +302,5 @@ const schema = object({ | ||
}); | ||
expect(schema.partialValidate(a)).toBe(a); | ||
expect(schema.validate(a, PARTIAL)).toBe(a); | ||
}); | ||
test("Object with props and fixable schema is fixed", () => { | ||
test("Partial object with props and fixable schema is fixed", () => { | ||
const schema = object({ | ||
@@ -279,5 +313,5 @@ props: { | ||
}); | ||
expect(schema.partialValidate({ num: "123", str: 123 })).toEqual({ num: 123, str: "123" }); | ||
expect(schema.validate({ num: "123", str: 123 }, PARTIAL)).toEqual({ num: 123, str: "123" }); | ||
}); | ||
test("Object with props has unknown fields stripped", () => { | ||
test("Partial object with props has unknown fields stripped", () => { | ||
const schema = object({ | ||
@@ -291,7 +325,10 @@ props: { | ||
expect( | ||
schema.partialValidate({ | ||
num: 123, | ||
str: "abcdef", | ||
excess: "should be removed", | ||
}), | ||
schema.validate( | ||
{ | ||
num: 123, | ||
str: "abcdef", | ||
excess: "should be removed", | ||
}, | ||
PARTIAL, | ||
), | ||
).toEqual({ | ||
@@ -303,3 +340,3 @@ num: 123, | ||
}); | ||
test("Deeply nested objects are also partially validated", () => { | ||
test("Partial deeply nested objects are also partially validated", () => { | ||
const schema = object({ | ||
@@ -320,10 +357,13 @@ props: { | ||
expect( | ||
schema.partialValidate({ | ||
num: 123, | ||
str: "abcdef", | ||
obj: { | ||
schema.validate( | ||
{ | ||
num: 123, | ||
str: "abcdef", | ||
obj: { | ||
num: 123, | ||
str: "abcdef", | ||
}, | ||
}, | ||
}), | ||
PARTIAL, | ||
), | ||
).toEqual({ | ||
@@ -340,3 +380,3 @@ num: 123, | ||
}); | ||
test("Objects with unfixable errors in subschemas returns Invalids", () => { | ||
test("Partial objects with unfixable errors in subschemas returns Invalids", () => { | ||
const schema = object({ | ||
@@ -349,3 +389,3 @@ props: { | ||
}); | ||
const invalid = schema.partialValidate({ dogs: "abc", cats: false }); | ||
const invalid = schema.validate({ dogs: "abc", cats: false }, PARTIAL); | ||
expect(invalid).toBeInstanceOf(Object); | ||
@@ -352,0 +392,0 @@ if (invalid instanceof Invalid && invalid.messages) { |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
224953
65
562
4767