From 789deecfe96a32adfd98ebede749380f81520402 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 2 Mar 2017 21:54:46 +0900 Subject: wip --- src/api/it.ts | 518 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 518 insertions(+) create mode 100644 src/api/it.ts (limited to 'src/api/it.ts') diff --git a/src/api/it.ts b/src/api/it.ts new file mode 100644 index 0000000000..1aff99c9e7 --- /dev/null +++ b/src/api/it.ts @@ -0,0 +1,518 @@ +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; -- cgit v1.2.3-freya