diff --git a/packages/types/.npmrc b/packages/types/.npmrc new file mode 100644 index 000000000000..43c97e719a5a --- /dev/null +++ b/packages/types/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/types/LICENSE b/packages/types/LICENSE new file mode 100644 index 000000000000..004a114ac2c9 --- /dev/null +++ b/packages/types/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) IBM Corp. 2017,2018. All Rights Reserved. +Node module: @loopback/types +This project is licensed under the MIT License, full text below. + +-------- + +MIT license + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/types/README.md b/packages/types/README.md new file mode 100644 index 000000000000..4f248d1b447d --- /dev/null +++ b/packages/types/README.md @@ -0,0 +1,32 @@ +# @loopback/types + +This module contains the typing system for LoopBack 4 to handle: + +- Types for models and properties +- Types for method parameters/return + +## Installation + +``` +$ npm install --save @loopback/metadata +``` + +## Contributions + +IBM/StrongLoop is an active supporter of open source and welcomes contributions +to our projects as well as those of the Node.js community in general. For more +information on how to contribute please refer to the +[Contribution Guide](https://loopback.io/doc/en/contrib/index.html). + +## Tests + +Run `npm test` from the root folder. + +## Contributors + +See +[all contributors](https://github.com/strongloop/loopback-next/graphs/contributors). + +## License + +MIT diff --git a/packages/types/docs.json b/packages/types/docs.json new file mode 100644 index 000000000000..6da56462be5f --- /dev/null +++ b/packages/types/docs.json @@ -0,0 +1,4 @@ +{ + "content": ["index.ts", "src/index.ts"], + "codeSectionDepth": 4 +} diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts new file mode 100644 index 000000000000..8b01933d37d9 --- /dev/null +++ b/packages/types/index.d.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/types +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './dist'; diff --git a/packages/types/index.js b/packages/types/index.js new file mode 100644 index 000000000000..32536a645319 --- /dev/null +++ b/packages/types/index.js @@ -0,0 +1,7 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/types +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +const nodeMajorVersion = +process.versions.node.split('.')[0]; +module.exports = nodeMajorVersion >= 7 ? require('./dist') : require('./dist6'); diff --git a/packages/types/index.ts b/packages/types/index.ts new file mode 100644 index 000000000000..6c262162a340 --- /dev/null +++ b/packages/types/index.ts @@ -0,0 +1,8 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/types +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +// DO NOT EDIT THIS FILE +// Add any aditional (re)exports to src/index.ts instead. +export * from './src'; diff --git a/packages/types/package.json b/packages/types/package.json new file mode 100644 index 000000000000..658f049d7912 --- /dev/null +++ b/packages/types/package.json @@ -0,0 +1,53 @@ +{ + "name": "@loopback/types", + "version": "4.0.0-alpha.8", + "description": "LoopBack's types utilities for reflection and decoration", + "engines": { + "node": ">=6" + }, + "scripts": { + "acceptance": "lb-mocha \"DIST/test/acceptance/**/*.js\"", + "build": "npm run build:dist && npm run build:dist6", + "build:current": "lb-tsc", + "build:dist": "lb-tsc es2017", + "build:dist6": "lb-tsc es2015", + "build:apidocs": "lb-apidocs", + "clean": "lb-clean loopback-types*.tgz dist dist6 package api-docs", + "prepublishOnly": "npm run build && npm run build:apidocs", + "pretest": "npm run build:current", + "test": "lb-mocha \"DIST/test/unit/**/*.js\" \"DIST/test/acceptance/**/*.js\"", + "unit": "lb-mocha \"DIST/test/unit/**/*.js\"", + "verify": "npm pack && tar xf loopback-types*.tgz && tree package && npm run clean" + }, + "author": "IBM", + "copyright.owner": "IBM Corp.", + "license": "MIT", + "dependencies": { + "debug": "^3.1.0" + }, + "devDependencies": { + "@loopback/build": "^4.0.0-alpha.12", + "@loopback/testlab": "^4.0.0-alpha.22", + "@types/debug": "^0.0.30" + }, + "keywords": [ + "LoopBack", + "Types" + ], + "files": [ + "README.md", + "index.js", + "index.d.ts", + "dist/src", + "dist6/src", + "api-docs", + "src" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-next.git" + } +} diff --git a/packages/types/src/common-types.ts b/packages/types/src/common-types.ts new file mode 100644 index 000000000000..1252e89806d9 --- /dev/null +++ b/packages/types/src/common-types.ts @@ -0,0 +1,52 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/types +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +/** + * Common types/interfaces such as Class/Constructor/Options/Callback + */ +// tslint:disable:no-any + +/** + * Interface for classes with `new` operator and static properties/methods + */ +export interface Class { + // new MyClass(...args) ==> T + new (...args: any[]): T; + // Other static properties/operations + [property: string]: any; +} + +/** + * Interface for constructor functions without `new` operator, for example, + * ``` + * function Foo(x) { + * if (!(this instanceof Foo)) { return new Foo(x); } + * this.x = x; + * } + * ``` + */ +export interface ConstructorFunction { + (...args: any[]): T; +} + +/** + * Constructor type - class or function + */ +export type Constructor = Class | ConstructorFunction; + +/** + * Objects with open properties + */ +export interface AnyObject { + [property: string]: any; +} + +export type Options = AnyObject | undefined; + +/** + * Same as Partial but goes deeper and makes Partial all its properties + * and sub-properties. + */ +export type DeepPartial = {[P in keyof T]?: DeepPartial}; diff --git a/packages/types/src/handlers/any.ts b/packages/types/src/handlers/any.ts new file mode 100644 index 000000000000..11f83d8a1ad4 --- /dev/null +++ b/packages/types/src/handlers/any.ts @@ -0,0 +1,38 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/types +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Type} from './type'; + +// tslint:disable:no-any + +/** + * Any type + */ +export class AnyType implements Type { + readonly name = 'any'; + + isInstance(value: any) { + return true; + } + + isCoercible(value: any) { + return true; + } + + defaultValue(): any { + return undefined; + } + + coerce(value: any) { + return value; + } + + serialize(value: any) { + if (value && typeof value.toJSON === 'function') { + return value.toJSON(); + } + return value; + } +} diff --git a/packages/types/src/handlers/array.ts b/packages/types/src/handlers/array.ts new file mode 100644 index 000000000000..0fb75cafe35a --- /dev/null +++ b/packages/types/src/handlers/array.ts @@ -0,0 +1,53 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/types +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Type} from './type'; +import * as util from 'util'; + +// tslint:disable:no-any + +/** + * Array type, such as string[] + */ +export class ArrayType implements Type> { + constructor(public itemType: Type) {} + + readonly name = 'array'; + + isInstance(value: any) { + if (value == null) return true; + if (!Array.isArray(value)) { + return false; + } + const list = value as Array; + return list.every(i => this.itemType.isInstance(i)); + } + + isCoercible(value: any): boolean { + if (value == null) return true; + if (!Array.isArray(value)) { + return false; + } + return value.every(i => this.itemType.isCoercible(i)); + } + + defaultValue(): Array { + return []; + } + + coerce(value: any) { + if (value == null) return value; + if (!Array.isArray(value)) { + const msg = util.format('Invalid %s: %j', this.name, value); + throw new TypeError(msg); + } + return value.map(i => this.itemType.coerce(i)); + } + + serialize(value: Array | null | undefined) { + if (value == null) return value; + return value.map(i => this.itemType.serialize(i)); + } +} diff --git a/packages/types/src/handlers/boolean.ts b/packages/types/src/handlers/boolean.ts new file mode 100644 index 000000000000..424767cfe19d --- /dev/null +++ b/packages/types/src/handlers/boolean.ts @@ -0,0 +1,35 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/types +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Type} from './type'; + +// tslint:disable:no-any + +/** + * Boolean type + */ +export class BooleanType implements Type { + readonly name = 'boolean'; + + isInstance(value: any) { + return value == null || typeof value === 'boolean'; + } + + defaultValue() { + return false; + } + + isCoercible(value: any): boolean { + return true; + } + + coerce(value: any) { + return value == null ? value : Boolean(value); + } + + serialize(value: boolean | null | undefined) { + return value; + } +} diff --git a/packages/types/src/handlers/buffer.ts b/packages/types/src/handlers/buffer.ts new file mode 100644 index 000000000000..992f3cb07a9a --- /dev/null +++ b/packages/types/src/handlers/buffer.ts @@ -0,0 +1,53 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/types +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import * as util from 'util'; +import {Type} from './type'; +import {Options} from '../common-types'; + +// tslint:disable:no-any + +/** + * Buffer (binary) type + */ +export class BufferType implements Type { + readonly name = 'buffer'; + + isInstance(value: any) { + return value == null || Buffer.isBuffer(value); + } + + defaultValue() { + return Buffer.from([]); + } + + isCoercible(value: any): boolean { + if (value == null) return true; + if (typeof value === 'string') return true; + if (Buffer.isBuffer(value)) return true; + if (Array.isArray(value)) return true; + return false; + } + + coerce(value: any, options?: Options) { + if (value == null) return value; + if (Buffer.isBuffer(value)) return value as Buffer; + if (typeof value === 'string') { + options = options || {}; + const encoding = options.encoding || 'utf-8'; + return Buffer.from(value, encoding); + } else if (Array.isArray(value)) { + return Buffer.from(value); + } + const msg = util.format('Invalid %s: %j', this.name, value); + throw new TypeError(msg); + } + + serialize(value: Buffer | null | undefined, options?: Options) { + if (value == null) return value; + const encoding = (options && options.encoding) || 'base64'; + return value.toString(encoding); + } +} diff --git a/packages/types/src/handlers/date.ts b/packages/types/src/handlers/date.ts new file mode 100644 index 000000000000..773e959923ef --- /dev/null +++ b/packages/types/src/handlers/date.ts @@ -0,0 +1,61 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/types +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import * as util from 'util'; +import {Type} from './type'; + +// tslint:disable:no-any + +/** + * Date type + */ +export class DateType implements Type { + readonly name = 'date'; + + isInstance(value: any) { + return value == null || value instanceof Date; + } + + isCoercible(value: any): boolean { + // Please note new Date(...) allows the following + /* + > new Date('1') + 2001-01-01T08:00:00.000Z + > new Date('0') + 2000-01-01T08:00:00.000Z + > new Date(1) + 1970-01-01T00:00:00.001Z + > new Date(0) + 1970-01-01T00:00:00.000Z + > new Date(true) + 1970-01-01T00:00:00.001Z + > new Date(false) + 1970-01-01T00:00:00.000Z + */ + return value == null || !isNaN(new Date(value).getTime()); + } + + defaultValue() { + return new Date(); + } + + coerce(value: any) { + if (value == null) return value; + if (value instanceof Date) { + return value; + } + const d = new Date(value); + if (isNaN(d.getTime())) { + const msg = util.format('Invalid %s: %j', this.name, value); + throw new TypeError(msg); + } + return d; + } + + serialize(value: Date | null | undefined) { + if (value == null) return value; + return value.toJSON(); + } +} diff --git a/packages/types/src/handlers/index.ts b/packages/types/src/handlers/index.ts new file mode 100644 index 000000000000..f51d8c4da142 --- /dev/null +++ b/packages/types/src/handlers/index.ts @@ -0,0 +1,47 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/types +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +/** + * Built-in types for LoopBack modeling + * - Type: abstract base type + * - StringType: string + * - BooleanType: boolean + * - NumberType: number + * - DateType: Date + * - BufferType: Buffer + * - AnyType: any + * - ArrayType: Array + * - UnionType: Union of types + */ +import {Type} from './type'; +import {StringType} from './string'; +import {BooleanType} from './boolean'; +import {NumberType} from './number'; +import {DateType} from './date'; +import {BufferType} from './buffer'; +import {AnyType} from './any'; +import {ArrayType} from './array'; +import {UnionType} from './union'; +import {ObjectType} from './object'; + +export { + Type, + StringType, + BooleanType, + NumberType, + DateType, + BufferType, + AnyType, + ArrayType, + UnionType, + ObjectType, +}; + +export const STRING = new StringType(); +export const BOOLEAN = new BooleanType(); +export const NUMBER = new NumberType(); +export const DATE = new DateType(); +export const BUFFER = new BufferType(); +export const ANY = new AnyType(); diff --git a/packages/types/src/handlers/number.ts b/packages/types/src/handlers/number.ts new file mode 100644 index 000000000000..9f403c390bb8 --- /dev/null +++ b/packages/types/src/handlers/number.ts @@ -0,0 +1,42 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/types +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import * as util from 'util'; +import {Type} from './type'; + +// tslint:disable:no-any + +/** + * Number type + */ +export class NumberType implements Type { + readonly name = 'number'; + + isInstance(value: any) { + return value == null || (!isNaN(value) && typeof value === 'number'); + } + + isCoercible(value: any): boolean { + return value == null || !isNaN(Number(value)); + } + + defaultValue() { + return 0; + } + + coerce(value: any) { + if (value == null) return value; + const n = Number(value); + if (isNaN(n)) { + const msg = util.format('Invalid %s: %j', this.name, value); + throw new TypeError(msg); + } + return n; + } + + serialize(value: number | null | undefined) { + return value; + } +} diff --git a/packages/types/src/handlers/object.ts b/packages/types/src/handlers/object.ts new file mode 100644 index 000000000000..19acafd1a7a9 --- /dev/null +++ b/packages/types/src/handlers/object.ts @@ -0,0 +1,53 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/types +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import * as util from 'util'; +import {Class, AnyObject} from '../common-types'; +import {Type} from './type'; + +// tslint:disable:no-any + +/** + * Object type + */ +export class ObjectType implements Type { + name = 'object'; + + constructor(public type: Class) {} + + isInstance(value: any) { + return value == null || value instanceof this.type; + } + + isCoercible(value: any): boolean { + return ( + value == null || (typeof value === 'object' && !Array.isArray(value)) + ); + } + + defaultValue() { + return new this.type(); + } + + coerce(value: any) { + if (value == null) return value; + if (value instanceof this.type) { + return value; + } + if (typeof value !== 'object' || Array.isArray(value)) { + const msg = util.format('Invalid %s: %j', this.name, value); + throw new TypeError(msg); + } + return new this.type(value); + } + + serialize(value: T | null | undefined) { + if (value == null) return value; + if (typeof value.toJSON === 'function') { + return value.toJSON(); + } + return value; + } +} diff --git a/packages/types/src/handlers/string.ts b/packages/types/src/handlers/string.ts new file mode 100644 index 000000000000..66a3847656f4 --- /dev/null +++ b/packages/types/src/handlers/string.ts @@ -0,0 +1,42 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/types +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Type} from './type'; + +// tslint:disable:no-any + +/** + * String type + */ +export class StringType implements Type { + readonly name = 'string'; + + isInstance(value: any): boolean { + return value == null || typeof value === 'string'; + } + + isCoercible(value: any): boolean { + return true; + } + + defaultValue(): string { + return ''; + } + + coerce(value: any): string { + if (value == null) return value; + if (typeof value.toJSON === 'function') { + value = value.toJSON(); + } + if (typeof value === 'object') { + return JSON.stringify(value); + } + return String(value); + } + + serialize(value: string | null | undefined) { + return value; + } +} diff --git a/packages/types/src/handlers/type.ts b/packages/types/src/handlers/type.ts new file mode 100644 index 000000000000..589ed8c33c78 --- /dev/null +++ b/packages/types/src/handlers/type.ts @@ -0,0 +1,51 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/types +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +/** + * Typing system for LoopBack + */ +import {Options} from '../common-types'; + +// tslint:disable:no-any + +export interface Type { + /** + * Name of the type + */ + name: string; + + /** + * Test if the given value is an instance of this type + * @param value The value + */ + isInstance(value: any): boolean; + + /** + * Generate the default value for this type + */ + defaultValue(): T | null | undefined; + + /** + * Check if the given value can be coerced into this type + * @param value The value to to be coerced + * @returns {boolean} + */ + isCoercible(value: any, options?: Options): boolean; + + /** + * Coerce the value into this type + * @param value The value to be coerced + * @param options Options for coercion + * @returns Coerced value of this type + */ + coerce(value: any, options?: Options): T | null | undefined; + + /** + * Serialize a value into json + * @param value The value of this type + * @param options Options for serialization + */ + serialize(value: T | null | undefined, options?: Options): any; +} diff --git a/packages/types/src/handlers/union.ts b/packages/types/src/handlers/union.ts new file mode 100644 index 000000000000..cc7c4bb34737 --- /dev/null +++ b/packages/types/src/handlers/union.ts @@ -0,0 +1,55 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/types +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import * as util from 'util'; +import {Type} from './type'; + +// tslint:disable:no-any + +/** + * Union type, such as string | number + */ +export class UnionType implements Type { + constructor(public itemTypes: Type[]) {} + + readonly name = 'union'; + + isInstance(value: any) { + return this.itemTypes.some(t => t.isInstance(value)); + } + + isCoercible(value: any) { + return this.itemTypes.some(t => t.isCoercible(value)); + } + + defaultValue() { + return this.itemTypes[0].defaultValue(); + } + + coerce(value: any) { + // First find instances + for (const type of this.itemTypes) { + if (type.isInstance(value)) { + return type.coerce(value); + } + } + // Try coercible + for (const type of this.itemTypes) { + if (type.isCoercible(value)) { + return type.coerce(value); + } + } + const msg = util.format('Invalid %s: %j', this.name, value); + throw new TypeError(msg); + } + + serialize(value: any) { + for (const type of this.itemTypes) { + if (type.isInstance(value)) { + return type.serialize(value); + } + } + } +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts new file mode 100644 index 000000000000..dcb9f10d0420 --- /dev/null +++ b/packages/types/src/index.ts @@ -0,0 +1,8 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/types +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './common-types'; +export * from './mixin'; +export * from './handlers'; diff --git a/packages/types/src/mixin.ts b/packages/types/src/mixin.ts new file mode 100644 index 000000000000..e8cc4eeddf26 --- /dev/null +++ b/packages/types/src/mixin.ts @@ -0,0 +1,57 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/types +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Class} from './common-types'; + +// tslint:disable:no-any + +/** + * Interface for functions that can mix properties/methods into a base class + * + * For example, + * ``` + * var calculatorMixin = Base => class extends Base { + * calc() { } + * }; + * + * function timestampMixin(Base) { + * return class extends Base { + * created: Date = new Date(); + * modified: Date = new Date(); + * } + * } + * ``` + * See http://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/. + */ +export interface MixinFunc { + >(baseClass: BC): BC; +} + +/** + * A builder to compose mixins + */ +export class MixinBuilder { + /** + * Constructor for MixinBuilder + * @param baseClass The base class + */ + constructor(public baseClass: Class) {} + + /** + * Apply one or more mixin functions + * @param mixins An array of mixin functions + */ + with(...mixins: MixinFunc[]) { + return mixins.reduce((c, mixin) => mixin(c), this.baseClass); + } + + /** + * Create an instance of MixinBuilder with the base class + * @param baseClass The base class + */ + static mix(baseClass: Class) { + return new MixinBuilder(baseClass); + } +} diff --git a/packages/types/test/unit/mixin.test.ts b/packages/types/test/unit/mixin.test.ts new file mode 100644 index 000000000000..0069bdcd4a2d --- /dev/null +++ b/packages/types/test/unit/mixin.test.ts @@ -0,0 +1,78 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/types +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect} from '@loopback/testlab'; +import {Class, MixinBuilder} from '../../'; + +// tslint:disable:no-any + +class BaseClass { + baseProp: string = 'baseProp'; + + static staticMethod1(): string { + return 'static'; + } + + method1() {} +} + +function Mixin1>(superClass: T) { + return class extends superClass { + mixinProp1: string = 'mixinProp1'; + + static staticMixinMethod1(): string { + return 'mixin1.static'; + } + + mixinMethod1(): string { + return 'mixin1'; + } + }; +} + +function Mixin2>(superClass: T) { + return class extends superClass { + mixinProp2: string = 'mixinProp2'; + + static staticMixinMethod2(): string { + return 'mixin2.static'; + } + + mixinMethod2() { + return 'mixin2'; + } + }; +} + +describe('mixin builder', () => { + let newClass: any; + before(() => { + newClass = MixinBuilder.mix(BaseClass).with(Mixin1, Mixin2); + }); + + it('allows multiple classes to be mixed in', () => { + expect(newClass.staticMethod1).to.eql(BaseClass.staticMethod1); + expect(newClass.prototype.method1).to.eql(BaseClass.prototype.method1); + expect(newClass.staticMixinMethod1).to.not.null(); + expect(newClass.staticMixinMethod2).to.not.null(); + + expect(newClass.staticMethod1()).to.eql('static'); + expect(newClass.staticMixinMethod1()).to.eql('mixin1.static'); + expect(newClass.staticMixinMethod2()).to.eql('mixin2.static'); + + const x = new newClass(); + expect(typeof x.mixinMethod1).to.eql('function'); + expect(typeof x.mixinMethod2).to.eql('function'); + expect(x.mixinMethod1()).to.eql('mixin1'); + expect(x.mixinMethod2()).to.eql('mixin2'); + expect(x.mixinProp1).to.eql('mixinProp1'); + expect(x.mixinProp2).to.eql('mixinProp2'); + }); + + it('allows inheritance', () => { + const x = new newClass(); + expect(x instanceof BaseClass).to.true(); + }); +}); diff --git a/packages/types/test/unit/type-handlers.test.ts b/packages/types/test/unit/type-handlers.test.ts new file mode 100644 index 000000000000..8bf31ef7a175 --- /dev/null +++ b/packages/types/test/unit/type-handlers.test.ts @@ -0,0 +1,497 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/types +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect} from '@loopback/testlab'; +import * as types from '../../'; + +describe('types', () => { + describe('string', () => { + const stringType = new types.StringType(); + it('checks isInstance', () => { + expect(stringType.isInstance('str')).to.be.true(); + expect(stringType.isInstance(null)).to.be.true(); + expect(stringType.isInstance(undefined)).to.be.true(); + expect(stringType.isInstance(true)).to.be.false(); + expect(stringType.isInstance({x: 1})).to.be.false(); + expect(stringType.isInstance([1, 2])).to.be.false(); + expect(stringType.isInstance(1)).to.be.false(); + expect(stringType.isInstance(new Date())).to.be.false(); + }); + + it('checks isCoercible', () => { + expect(stringType.isCoercible('str')).to.be.true(); + expect(stringType.isCoercible(null)).to.be.true(); + expect(stringType.isCoercible(undefined)).to.be.true(); + expect(stringType.isCoercible(true)).to.be.true(); + expect(stringType.isCoercible({x: 1})).to.be.true(); + expect(stringType.isCoercible(1)).to.be.true(); + expect(stringType.isCoercible(new Date())).to.be.true(); + }); + + it('creates defaultValue', () => { + expect(stringType.defaultValue()).to.be.equal(''); + }); + + it('coerces values', () => { + expect(stringType.coerce('str')).to.equal('str'); + expect(stringType.coerce(null)).to.equal(null); + expect(stringType.coerce(undefined)).to.equal(undefined); + expect(stringType.coerce(true)).to.equal('true'); + expect(stringType.coerce({x: 1})).to.equal('{"x":1}'); + expect(stringType.coerce([1, '2'])).to.equal('[1,"2"]'); + expect(stringType.coerce(1)).to.equal('1'); + const date = new Date(); + expect(stringType.coerce(date)).to.equal(date.toJSON()); + }); + + it('serializes values', () => { + expect(stringType.serialize('str')).to.eql('str'); + expect(stringType.serialize(null)).null(); + expect(stringType.serialize(undefined)).undefined(); + }); + }); + + describe('boolean', () => { + const booleanType = new types.BooleanType(); + it('checks isInstance', () => { + expect(booleanType.isInstance('str')).to.be.false(); + expect(booleanType.isInstance(null)).to.be.true(); + expect(booleanType.isInstance(undefined)).to.be.true(); + expect(booleanType.isInstance(true)).to.be.true(); + expect(booleanType.isInstance(false)).to.be.true(); + expect(booleanType.isInstance({x: 1})).to.be.false(); + expect(booleanType.isInstance([1, 2])).to.be.false(); + expect(booleanType.isInstance(1)).to.be.false(); + expect(booleanType.isInstance(new Date())).to.be.false(); + }); + + it('checks isCoercible', () => { + expect(booleanType.isCoercible('str')).to.be.true(); + expect(booleanType.isCoercible(null)).to.be.true(); + expect(booleanType.isCoercible(undefined)).to.be.true(); + expect(booleanType.isCoercible(true)).to.be.true(); + expect(booleanType.isCoercible({x: 1})).to.be.true(); + expect(booleanType.isCoercible(1)).to.be.true(); + expect(booleanType.isCoercible(new Date())).to.be.true(); + }); + + it('creates defaultValue', () => { + expect(booleanType.defaultValue()).to.be.equal(false); + }); + + it('coerces values', () => { + expect(booleanType.coerce('str')).to.equal(true); + expect(booleanType.coerce(null)).to.equal(null); + expect(booleanType.coerce(undefined)).to.equal(undefined); + expect(booleanType.coerce(true)).to.equal(true); + expect(booleanType.coerce(false)).to.equal(false); + expect(booleanType.coerce({x: 1})).to.equal(true); + expect(booleanType.coerce([1, '2'])).to.equal(true); + expect(booleanType.coerce('')).to.equal(false); + expect(booleanType.coerce('true')).to.equal(true); + // string 'false' is boolean true + expect(booleanType.coerce('false')).to.equal(true); + expect(booleanType.coerce(0)).to.equal(false); + expect(booleanType.coerce(1)).to.equal(true); + const date = new Date(); + expect(booleanType.coerce(date)).to.equal(true); + }); + + it('serializes values', () => { + expect(booleanType.serialize(true)).to.eql(true); + expect(booleanType.serialize(false)).to.eql(false); + expect(booleanType.serialize(null)).null(); + expect(booleanType.serialize(undefined)).undefined(); + }); + }); + + describe('number', () => { + const numberType = new types.NumberType(); + it('checks isInstance', () => { + expect(numberType.isInstance('str')).to.be.false(); + expect(numberType.isInstance(null)).to.be.true(); + expect(numberType.isInstance(undefined)).to.be.true(); + expect(numberType.isInstance(NaN)).to.be.false(); + expect(numberType.isInstance(true)).to.be.false(); + expect(numberType.isInstance({x: 1})).to.be.false(); + expect(numberType.isInstance([1, 2])).to.be.false(); + expect(numberType.isInstance(1)).to.be.true(); + expect(numberType.isInstance(1.1)).to.be.true(); + expect(numberType.isInstance(new Date())).to.be.false(); + }); + + it('checks isCoercible', () => { + expect(numberType.isCoercible('str')).to.be.false(); + expect(numberType.isCoercible('1')).to.be.true(); + expect(numberType.isCoercible('1.1')).to.be.true(); + expect(numberType.isCoercible(null)).to.be.true(); + expect(numberType.isCoercible(undefined)).to.be.true(); + expect(numberType.isCoercible(true)).to.be.true(); + expect(numberType.isCoercible(false)).to.be.true(); + expect(numberType.isCoercible({x: 1})).to.be.false(); + expect(numberType.isCoercible(1)).to.be.true(); + expect(numberType.isCoercible(1.1)).to.be.true(); + // Date can be converted to number + expect(numberType.isCoercible(new Date())).to.be.true(); + }); + + it('creates defaultValue', () => { + expect(numberType.defaultValue()).to.be.equal(0); + }); + + it('coerces values', () => { + expect(() => numberType.coerce('str')).to.throw(/Invalid number/); + expect(numberType.coerce('1')).to.equal(1); + expect(numberType.coerce('1.1')).to.equal(1.1); + expect(numberType.coerce(null)).to.equal(null); + expect(numberType.coerce(undefined)).to.equal(undefined); + expect(numberType.coerce(true)).to.equal(1); + expect(numberType.coerce(false)).to.equal(0); + expect(() => numberType.coerce({x: 1})).to.throw(/Invalid number/); + expect(() => numberType.coerce([1, '2'])).to.throw(/Invalid number/); + expect(numberType.coerce(1)).to.equal(1); + expect(numberType.coerce(1.1)).to.equal(1.1); + const date = new Date(); + expect(numberType.coerce(date)).to.equal(date.getTime()); + }); + + it('serializes values', () => { + expect(numberType.serialize(1)).to.eql(1); + expect(numberType.serialize(1.1)).to.eql(1.1); + expect(numberType.serialize(null)).null(); + expect(numberType.serialize(undefined)).undefined(); + }); + }); + + describe('date', () => { + const dateType = new types.DateType(); + it('checks isInstance', () => { + expect(dateType.isInstance('str')).to.be.false(); + expect(dateType.isInstance(null)).to.be.true(); + expect(dateType.isInstance(undefined)).to.be.true(); + expect(dateType.isInstance(NaN)).to.be.false(); + expect(dateType.isInstance(true)).to.be.false(); + expect(dateType.isInstance({x: 1})).to.be.false(); + expect(dateType.isInstance([1, 2])).to.be.false(); + expect(dateType.isInstance(1)).to.be.false(); + expect(dateType.isInstance(1.1)).to.be.false(); + expect(dateType.isInstance(new Date())).to.be.true(); + }); + + it('checks isCoercible', () => { + expect(dateType.isCoercible('str')).to.be.false(); + expect(dateType.isCoercible('1')).to.be.true(); + expect(dateType.isCoercible('1.1')).to.be.true(); + expect(dateType.isCoercible(null)).to.be.true(); + expect(dateType.isCoercible(undefined)).to.be.true(); + expect(dateType.isCoercible(true)).to.be.true(); + expect(dateType.isCoercible(false)).to.be.true(); + expect(dateType.isCoercible({x: 1})).to.be.false(); + expect(dateType.isCoercible(1)).to.be.true(); + expect(dateType.isCoercible(1.1)).to.be.true(); + // Date can be converted to number + expect(dateType.isCoercible(new Date())).to.be.true(); + }); + + it('creates defaultValue', () => { + const d = new Date(); + const v = dateType.defaultValue(); + expect(v.getTime()).to.aboveOrEqual(d.getTime()); + expect(v.getTime()).to.approximately(d.getTime(), 50); + }); + + it('coerces values', () => { + expect(() => dateType.coerce('str')).to.throw(/Invalid date/); + // '1' will be parsed as local 2001-01-01 + expect(dateType.coerce('1')).to.eql(new Date('01/01/2001')); + // '1.1' will be parsed as local 2001-01-01 + expect(dateType.coerce('1.1')).to.eql(new Date('01/01/2001')); + expect(dateType.coerce(null)).to.equal(null); + expect(dateType.coerce(undefined)).to.equal(undefined); + expect(dateType.coerce(true)).to.eql(new Date(1)); + expect(dateType.coerce(false)).to.eql(new Date(0)); + expect(() => dateType.coerce({x: 1})).to.throw(/Invalid date/); + expect(dateType.coerce([1, '2'])).to.eql(new Date('01/02/2001')); + expect(dateType.coerce(1)).to.eql(new Date('1970-01-01T00:00:00.001Z')); + expect(dateType.coerce(1.1)).to.eql(new Date('1970-01-01T00:00:00.001Z')); + const date = new Date(); + expect(dateType.coerce(date)).to.equal(date); + }); + + it('serializes values', () => { + const date = new Date(); + expect(dateType.serialize(date)).to.eql(date.toJSON()); + expect(dateType.serialize(null)).null(); + expect(dateType.serialize(undefined)).undefined(); + }); + }); + + describe('buffer', () => { + const bufferType = new types.BufferType(); + it('checks isInstance', () => { + expect(bufferType.isInstance(new Buffer([1]))).to.be.true(); + expect(bufferType.isInstance(new Buffer('123'))).to.be.true(); + expect(bufferType.isInstance('str')).to.be.false(); + expect(bufferType.isInstance(null)).to.be.true(); + expect(bufferType.isInstance(undefined)).to.be.true(); + expect(bufferType.isInstance(true)).to.be.false(); + expect(bufferType.isInstance({x: 1})).to.be.false(); + expect(bufferType.isInstance([1, 2])).to.be.false(); + expect(bufferType.isInstance(1)).to.be.false(); + expect(bufferType.isInstance(new Date())).to.be.false(); + expect(bufferType.isInstance([1])).to.be.false(); + }); + + it('checks isCoercible', () => { + expect(bufferType.isCoercible('str')).to.be.true(); + expect(bufferType.isCoercible(null)).to.be.true(); + expect(bufferType.isCoercible(undefined)).to.be.true(); + expect(bufferType.isCoercible(new Buffer('12'))).to.be.true(); + expect(bufferType.isCoercible([1, 2])).to.be.true(); + expect(bufferType.isCoercible({x: 1})).to.be.false(); + expect(bufferType.isCoercible(1)).to.be.false(); + expect(bufferType.isCoercible(new Date())).to.be.false(); + }); + + it('creates defaultValue', () => { + expect(bufferType.defaultValue().equals(Buffer.from([]))).to.be.true(); + }); + + it('coerces values', () => { + expect(bufferType.coerce('str').equals(new Buffer('str'))).to.be.true(); + expect(bufferType.coerce([1]).equals(Buffer.from([1]))).to.be.true(); + expect(bufferType.coerce(null)).to.equal(null); + expect(bufferType.coerce(undefined)).to.equal(undefined); + const buf = new Buffer('12'); + expect(bufferType.coerce(buf)).exactly(buf); + expect(() => bufferType.coerce(1)).to.throw(/Invalid buffer/); + expect(() => bufferType.coerce(new Date())).to.throw(/Invalid buffer/); + expect(() => bufferType.coerce(true)).to.throw(/Invalid buffer/); + expect(() => bufferType.coerce(false)).to.throw(/Invalid buffer/); + }); + + it('serializes values', () => { + expect( + bufferType.serialize(new Buffer('str'), {encoding: 'utf-8'}), + ).to.eql('str'); + expect(bufferType.serialize(new Buffer('str'))).to.eql('c3Ry'); + expect(bufferType.serialize(null)).null(); + expect(bufferType.serialize(undefined)).undefined(); + }); + }); + + describe('any', () => { + const anyType = new types.AnyType(); + it('checks isInstance', () => { + expect(anyType.isInstance('str')).to.be.true(); + expect(anyType.isInstance(null)).to.be.true(); + expect(anyType.isInstance(undefined)).to.be.true(); + expect(anyType.isInstance(true)).to.be.true(); + expect(anyType.isInstance({x: 1})).to.be.true(); + expect(anyType.isInstance([1, 2])).to.be.true(); + expect(anyType.isInstance(1)).to.be.true(); + expect(anyType.isInstance(new Date())).to.be.true(); + expect(anyType.isInstance(new Buffer('123'))).to.be.true(); + }); + + it('checks isCoercible', () => { + expect(anyType.isCoercible('str')).to.be.true(); + expect(anyType.isCoercible(null)).to.be.true(); + expect(anyType.isCoercible(undefined)).to.be.true(); + expect(anyType.isCoercible(true)).to.be.true(); + expect(anyType.isCoercible({x: 1})).to.be.true(); + expect(anyType.isCoercible(1)).to.be.true(); + expect(anyType.isCoercible([1, '2'])).to.be.true(); + expect(anyType.isCoercible(new Date())).to.be.true(); + expect(anyType.isCoercible(new Buffer('123'))).to.be.true(); + }); + + it('creates defaultValue', () => { + expect(anyType.defaultValue()).to.equal(undefined); + }); + + it('coerces values', () => { + expect(anyType.coerce('str')).to.equal('str'); + expect(anyType.coerce(null)).to.equal(null); + expect(anyType.coerce(undefined)).to.equal(undefined); + expect(anyType.coerce(true)).to.equal(true); + const obj = {x: 1}; + expect(anyType.coerce(obj)).to.equal(obj); + const arr = [1, '2']; + expect(anyType.coerce(arr)).to.equal(arr); + expect(anyType.coerce(1)).to.equal(1); + const date = new Date(); + expect(anyType.coerce(date)).to.equal(date); + const buf = new Buffer('12'); + expect(anyType.coerce(buf)).to.equal(buf); + }); + + it('serializes values', () => { + expect(anyType.serialize('str')).to.eql('str'); + expect(anyType.serialize(1)).to.eql(1); + expect(anyType.serialize([1, '2'])).to.eql([1, '2']); + expect(anyType.serialize(null)).null(); + expect(anyType.serialize(undefined)).undefined(); + const date = new Date(); + expect(anyType.serialize(date)).to.eql(date.toJSON()); + const obj = {x: 1}; + expect(anyType.serialize(obj)).to.eql(obj); + const json = { + x: 1, + y: 2, + toJSON() { + return {a: json.x + json.y}; + }, + }; + expect(anyType.serialize(json)).to.eql({a: 3}); + }); + }); + + describe('array', () => { + const stringType = new types.StringType(); + const arrayType = new types.ArrayType(stringType); + + it('checks isInstance', () => { + expect(arrayType.isInstance('str')).to.be.false(); + expect(arrayType.isInstance(null)).to.be.true(); + expect(arrayType.isInstance(undefined)).to.be.true(); + expect(arrayType.isInstance(NaN)).to.be.false(); + expect(arrayType.isInstance(true)).to.be.false(); + expect(arrayType.isInstance({x: 1})).to.be.false(); + expect(arrayType.isInstance([1, 2])).to.be.false(); + expect(arrayType.isInstance([1, '2'])).to.be.false(); + expect(arrayType.isInstance(['1'])).to.be.true(); + expect(arrayType.isInstance(['1', 'a'])).to.be.true(); + expect(arrayType.isInstance(['1', 'a', null])).to.be.true(); + expect(arrayType.isInstance(['1', 'a', undefined])).to.be.true(); + expect(arrayType.isInstance([])).to.be.true(); + expect(arrayType.isInstance(1)).to.be.false(); + expect(arrayType.isInstance(1.1)).to.be.false(); + expect(arrayType.isInstance(new Date())).to.be.false(); + }); + + it('checks isCoercible', () => { + expect(arrayType.isCoercible('str')).to.be.false(); + expect(arrayType.isCoercible('1')).to.be.false(); + expect(arrayType.isCoercible('1.1')).to.be.false(); + expect(arrayType.isCoercible(null)).to.be.true(); + expect(arrayType.isCoercible(undefined)).to.be.true(); + expect(arrayType.isCoercible(true)).to.be.false(); + expect(arrayType.isCoercible(false)).to.be.false(); + expect(arrayType.isCoercible({x: 1})).to.be.false(); + expect(arrayType.isCoercible(1)).to.be.false(); + expect(arrayType.isCoercible(1.1)).to.be.false(); + expect(arrayType.isCoercible(new Date())).to.be.false(); + expect(arrayType.isCoercible([])).to.be.true(); + expect(arrayType.isCoercible(['1'])).to.be.true(); + expect(arrayType.isCoercible(['1', 2])).to.be.true(); + }); + + it('creates defaultValue', () => { + expect(arrayType.defaultValue()).to.be.eql([]); + }); + + it('coerces values', () => { + expect(() => arrayType.coerce('str')).to.throw(/Invalid array/); + expect(() => arrayType.coerce('1')).to.throw(/Invalid array/); + expect(() => arrayType.coerce('1.1')).to.throw(/Invalid array/); + expect(arrayType.coerce(null)).to.equal(null); + expect(arrayType.coerce(undefined)).to.equal(undefined); + + expect(() => arrayType.coerce(true)).to.throw(/Invalid array/); + expect(() => arrayType.coerce(false)).to.throw(/Invalid array/); + expect(() => arrayType.coerce({x: 1})).to.throw(/Invalid array/); + expect(() => arrayType.coerce(1)).to.throw(/Invalid array/); + + const date = new Date(); + expect(() => arrayType.coerce(date)).to.throw(/Invalid array/); + + expect(arrayType.coerce([1, '2'])).to.eql(['1', '2']); + expect(arrayType.coerce(['2'])).to.eql(['2']); + expect(arrayType.coerce([null, undefined, '2'])).to.eql([ + null, + undefined, + '2', + ]); + expect(arrayType.coerce([true, '2'])).to.eql(['true', '2']); + expect(arrayType.coerce([false, '2'])).to.eql(['false', '2']); + expect(arrayType.coerce([date])).to.eql([date.toJSON()]); + }); + + it('serializes values', () => { + expect(arrayType.serialize(['a'])).to.eql(['a']); + expect(arrayType.serialize(null)).null(); + expect(arrayType.serialize(undefined)).undefined(); + }); + }); + + describe('union', () => { + const numberType = new types.NumberType(); + const booleanType = new types.BooleanType(); + const unionType = new types.UnionType([numberType, booleanType]); + + it('checks isInstance', () => { + expect(unionType.isInstance('str')).to.be.false(); + expect(unionType.isInstance(null)).to.be.true(); + expect(unionType.isInstance(undefined)).to.be.true(); + expect(unionType.isInstance(NaN)).to.be.false(); + expect(unionType.isInstance(true)).to.be.true(); + expect(unionType.isInstance({x: 1})).to.be.false(); + expect(unionType.isInstance([1, 2])).to.be.false(); + expect(unionType.isInstance(1)).to.be.true(); + expect(unionType.isInstance(1.1)).to.be.true(); + expect(unionType.isInstance(new Date())).to.be.false(); + }); + + it('checks isCoercible', () => { + expect(unionType.isCoercible('str')).to.be.true(); + expect(unionType.isCoercible('1')).to.be.true(); + expect(unionType.isCoercible('1.1')).to.be.true(); + expect(unionType.isCoercible(null)).to.be.true(); + expect(unionType.isCoercible(undefined)).to.be.true(); + expect(unionType.isCoercible(true)).to.be.true(); + expect(unionType.isCoercible(false)).to.be.true(); + expect(unionType.isCoercible({x: 1})).to.be.true(); + expect(unionType.isCoercible(1)).to.be.true(); + expect(unionType.isCoercible(1.1)).to.be.true(); + // Date can be converted to number + expect(unionType.isCoercible(new Date())).to.be.true(); + }); + + it('creates defaultValue', () => { + expect(unionType.defaultValue()).to.be.equal(0); + }); + + it('coerces values', () => { + expect(unionType.coerce('str')).to.equal(true); + expect(unionType.coerce('1')).to.equal(1); + expect(unionType.coerce('1.1')).to.equal(1.1); + expect(unionType.coerce(null)).to.equal(null); + expect(unionType.coerce(undefined)).to.equal(undefined); + expect(unionType.coerce(true)).to.equal(true); + expect(unionType.coerce(false)).to.equal(false); + expect(unionType.coerce({x: 1})).to.equal(true); + expect(unionType.coerce([1, '2'])).to.equal(true); + expect(unionType.coerce(1)).to.equal(1); + expect(unionType.coerce(1.1)).to.equal(1.1); + const date = new Date(); + expect(unionType.coerce(date)).to.equal(date.getTime()); + + // Create a new union type to test invalid value + const dateType = new types.DateType(); + const numberOrDateType = new types.UnionType([numberType, dateType]); + expect(() => numberOrDateType.coerce('str')).to.throw(/Invalid union/); + }); + + it('serializes values', () => { + expect(unionType.serialize(1)).to.equal(1); + expect(unionType.serialize(1.1)).to.equal(1.1); + expect(unionType.serialize(true)).to.equal(true); + expect(unionType.serialize(false)).to.equal(false); + expect(unionType.serialize(null)).null(); + expect(unionType.serialize(undefined)).undefined(); + }); + }); +}); diff --git a/packages/types/tsconfig.build.json b/packages/types/tsconfig.build.json new file mode 100644 index 000000000000..3ffcd508d23e --- /dev/null +++ b/packages/types/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../build/config/tsconfig.common.json", + "compilerOptions": { + "rootDir": "." + }, + "include": ["index.ts", "src", "test"] +}