import * as mongo from 'mongodb'; import hasDuplicates from '../common/has-duplicates'; type Validator = (value: T) => boolean | Error; type Modifier = (value: T) => T; interface Factory { get: () => [any, Error]; required: () => Factory; validate: (validator: Validator) => Factory; modify: (modifier: Modifier) => Factory; } class FactoryCore implements Factory { value: any; error: Error; constructor() { this.value = null; this.error = null; } /** * このインスタンスの値が undefined または null の場合エラーにします */ required() { if (this.error === null && this.value === null) { this.error = new Error('required'); } return this; } /** * このインスタンスの値およびエラーを取得します */ get(): [any, Error] { return [this.value, this.error]; } /** * このインスタンスの値に対して妥当性を検証します * バリデータが false またはエラーを返した場合エラーにします * @param validator バリデータ */ validate(validator: Validator) { if (this.error || this.value === null) return this; const result = validator(this.value); if (result === false) { this.error = new Error('invalid-format'); } else if (result instanceof Error) { this.error = result; } return this; } modify(modifier: Modifier) { if (this.error || this.value === null) return this; try { this.value = modifier(this.value); } catch (e) { this.error = e; } return this; } } class BooleanFactory extends FactoryCore { value: boolean; error: Error; constructor(value) { super(); if (value === undefined || value === null) { this.value = null; } else if (typeof value != 'boolean') { this.error = new Error('must-be-a-boolean'); } else { this.value = value; } } /** * このインスタンスの値が undefined または null の場合エラーにします */ required() { return super.required(); } /** * このインスタンスの値およびエラーを取得します */ get(): [boolean, Error] { return super.get(); } /** * このインスタンスの値に対して妥当性を検証します * バリデータが false またはエラーを返した場合エラーにします * @param validator バリデータ */ validate(validator: Validator) { return super.validate(validator); } modify(modifier: Modifier) { return super.modify(modifier); } } class NumberFactory extends FactoryCore { value: number; error: Error; constructor(value) { super(); if (value === undefined || value === null) { this.value = null; } else if (!Number.isFinite(value)) { this.error = new Error('must-be-a-number'); } else { this.value = value; } } /** * 値が指定された範囲内にない場合エラーにします * @param min 下限 * @param max 上限 */ range(min: number, max: number) { if (this.error || this.value === null) return this; if (this.value < min || this.value > max) { this.error = new Error('invalid-range'); } return this; } /** * このインスタンスの値が undefined または null の場合エラーにします */ required() { return super.required(); } /** * このインスタンスの値およびエラーを取得します */ get(): [number, Error] { return super.get(); } /** * このインスタンスの値に対して妥当性を検証します * バリデータが false またはエラーを返した場合エラーにします * @param validator バリデータ */ validate(validator: Validator) { return super.validate(validator); } modify(modifier: Modifier) { return super.modify(modifier); } } class StringFactory extends FactoryCore { value: string; error: Error; constructor(value) { super(); if (value === undefined || value === null) { this.value = null; } else if (typeof value != 'string') { this.error = new Error('must-be-a-string'); } else { this.value = value; } } /** * 文字数が指定された範囲内にない場合エラーにします * @param min 下限 * @param max 上限 */ range(min: number, max: number) { if (this.error || this.value === null) return this; if (this.value.length < min || this.value.length > max) { this.error = new Error('invalid-range'); } return this; } trim() { if (this.error || this.value === null) return this; this.value = this.value.trim(); return this; } /** * このインスタンスの値が undefined または null の場合エラーにします */ required() { return super.required(); } /** * このインスタンスの値およびエラーを取得します */ get(): [string, Error] { return super.get(); } /** * このインスタンスの値に対して妥当性を検証します * バリデータが false またはエラーを返した場合エラーにします * @param validator バリデータ */ validate(validator: Validator) { return super.validate(validator); } modify(modifier: Modifier) { return super.modify(modifier); } } class ArrayFactory extends FactoryCore { value: any[]; error: Error; constructor(value) { super(); if (value === undefined || value === null) { this.value = null; } else if (!Array.isArray(value)) { this.error = new Error('must-be-an-array'); } else { this.value = value; } } /** * 配列の値がユニークでない場合(=重複した項目がある場合)エラーにします */ unique() { if (this.error || this.value === null) return this; if (hasDuplicates(this.value)) { this.error = new Error('must-be-unique'); } return this; } /** * 配列の長さが指定された範囲内にない場合エラーにします * @param min 下限 * @param max 上限 */ range(min: number, max: number) { if (this.error || this.value === null) return this; if (this.value.length < min || this.value.length > max) { this.error = new Error('invalid-range'); } return this; } /** * このインスタンスの値が undefined または null の場合エラーにします */ required() { return super.required(); } /** * このインスタンスの値およびエラーを取得します */ get(): [any[], Error] { return super.get(); } /** * このインスタンスの値に対して妥当性を検証します * バリデータが false またはエラーを返した場合エラーにします * @param validator バリデータ */ validate(validator: Validator) { return super.validate(validator); } modify(modifier: Modifier) { return super.modify(modifier); } } class IdFactory extends FactoryCore { value: mongo.ObjectID; error: Error; constructor(value) { super(); if (value === undefined || value === null) { this.value = null; } else if (typeof value != 'string' || !mongo.ObjectID.isValid(value)) { this.error = new Error('must-be-an-id'); } else { this.value = new mongo.ObjectID(value); } } /** * このインスタンスの値が undefined または null の場合エラーにします */ required() { return super.required(); } /** * このインスタンスの値およびエラーを取得します */ get(): [any[], Error] { return super.get(); } /** * このインスタンスの値に対して妥当性を検証します * バリデータが false またはエラーを返した場合エラーにします * @param validator バリデータ */ validate(validator: Validator) { return super.validate(validator); } modify(modifier: Modifier) { return super.modify(modifier); } } class ObjectFactory extends FactoryCore { value: any; error: Error; constructor(value) { super(); if (value === undefined || value === null) { this.value = null; } else if (typeof value != 'object') { this.error = new Error('must-be-an-object'); } else { this.value = value; } } /** * このインスタンスの値が undefined または null の場合エラーにします */ required() { return super.required(); } /** * このインスタンスの値およびエラーを取得します */ get(): [any, Error] { return super.get(); } /** * このインスタンスの値に対して妥当性を検証します * バリデータが false またはエラーを返した場合エラーにします * @param validator バリデータ */ validate(validator: Validator) { return super.validate(validator); } modify(modifier: Modifier) { return super.modify(modifier); } } type MustBe = { must: { be: { a: { string: () => StringFactory; number: () => NumberFactory; boolean: () => BooleanFactory; }; an: { id: () => IdFactory; array: () => ArrayFactory; object: () => ObjectFactory; }; }; }; }; const mustBe = (value: any) => ({ must: { be: { a: { string: () => new StringFactory(value), number: () => new NumberFactory(value), boolean: () => new BooleanFactory(value) }, an: { id: () => new IdFactory(value), array: () => new ArrayFactory(value), object: () => new ObjectFactory(value) } } } }); type Type = 'id' | 'string' | 'number' | 'boolean' | 'array' | 'set' | 'object'; type Pipe = (x: T) => T | boolean | Error; function validate(value: any, type: 'id', isRequired?: boolean, pipe?: Pipe | Pipe[]): [mongo.ObjectID, Error]; function validate(value: any, type: 'string', isRequired?: boolean, pipe?: Pipe | Pipe[]): [string, Error]; function validate(value: any, type: 'number', isRequired?: boolean, pipe?: Pipe | Pipe[]): [number, Error]; function validate(value: any, type: 'boolean', isRequired?: boolean): [boolean, Error]; function validate(value: any, type: 'array', isRequired?: boolean, pipe?: Pipe | Pipe[]): [any[], Error]; function validate(value: any, type: 'set', isRequired?: boolean, pipe?: Pipe | Pipe[]): [any[], Error]; function validate(value: any, type: 'object', isRequired?: boolean, pipe?: Pipe | Pipe[]): [any, Error]; function validate(value: any, type: Type, isRequired?: boolean, pipe?: Pipe | Pipe[]): [any, Error] { if (value === undefined || value === null) { if (isRequired) { return [null, new Error('is-required')] } else { return [null, null] } } switch (type) { case 'id': if (typeof value != 'string' || !mongo.ObjectID.isValid(value)) { return [null, new Error('incorrect-id')]; } break; case 'string': if (typeof value != 'string') { return [null, new Error('must-be-a-string')]; } break; case 'number': if (!Number.isFinite(value)) { return [null, new Error('must-be-a-number')]; } break; case 'boolean': if (typeof value != 'boolean') { return [null, new Error('must-be-a-boolean')]; } break; case 'array': if (!Array.isArray(value)) { return [null, new Error('must-be-an-array')]; } break; case 'set': if (!Array.isArray(value)) { return [null, new Error('must-be-an-array')]; } else if (hasDuplicates(value)) { return [null, new Error('duplicated-contents')]; } break; case 'object': if (typeof value != 'object') { return [null, new Error('must-be-an-object')]; } break; } if (type == 'id') value = new mongo.ObjectID(value); if (pipe) { const pipes = Array.isArray(pipe) ? pipe : [pipe]; for (let i = 0; i < pipes.length; i++) { const result = pipes[i](value); if (result === false) { return [null, new Error('invalid-format')]; } else if (result instanceof Error) { return [null, result]; } else if (result !== true) { value = result; } } } return [value, null]; } function it(value: any): MustBe; function it(value: any, type: 'id', isRequired?: boolean, pipe?: Pipe | Pipe[]): [mongo.ObjectID, Error]; function it(value: any, type: 'string', isRequired?: boolean, pipe?: Pipe | Pipe[]): [string, Error]; function it(value: any, type: 'number', isRequired?: boolean, pipe?: Pipe | Pipe[]): [number, Error]; function it(value: any, type: 'boolean', isRequired?: boolean): [boolean, Error]; function it(value: any, type: 'array', isRequired?: boolean, pipe?: Pipe | Pipe[]): [any[], Error]; function it(value: any, type: 'set', isRequired?: boolean, pipe?: Pipe | Pipe[]): [any[], Error]; function it(value: any, type: 'object', isRequired?: boolean, pipe?: Pipe | Pipe[]): [any, Error]; function it(value: any, type?: any, isRequired?: boolean, pipe?: Pipe | Pipe[]): any { if (typeof type === 'undefined') { return mustBe(value); } else { return validate(value, type, isRequired, pipe); } } export default it;