From 3a10ef017e32dc0910af5aa79950cd6a13c590bb Mon Sep 17 00:00:00 2001 From: Alex LaFroscia Date: Wed, 26 Dec 2018 14:10:56 -0500 Subject: [PATCH 1/6] fix: remove extraneous property --- tests/unit/argument-test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/argument-test.js b/tests/unit/argument-test.js index d9f878b..9076916 100644 --- a/tests/unit/argument-test.js +++ b/tests/unit/argument-test.js @@ -195,7 +195,6 @@ module('Unit | @argument', function() { test('typed value can be provided by alias', function(assert) { class Foo extends EmberObject { @argument('number') prop; - j; } class Bar extends Foo { From b5052d52a1a77a5d4cb36dfce3742dbcacb233fe Mon Sep 17 00:00:00 2001 From: Alex LaFroscia Date: Wed, 26 Dec 2018 14:11:13 -0500 Subject: [PATCH 2/6] fix: remove unnecessary object creation --- tests/unit/types/optional-test.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/unit/types/optional-test.js b/tests/unit/types/optional-test.js index 238381f..aa4c098 100644 --- a/tests/unit/types/optional-test.js +++ b/tests/unit/types/optional-test.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + import { test, module } from 'qunit'; import EmberObject from '@ember/object'; @@ -89,8 +91,6 @@ module('Unit | types | optional', function() { @argument(optional(null)) bar; } - - Foo.create({ bar: 2 }); }, /Passsing 'null' to the 'optional' helper does not make sense./); assert.throws(() => { @@ -98,8 +98,6 @@ module('Unit | types | optional', function() { @argument(optional(undefined)) bar; } - - Foo.create({ bar: 2 }); }, /Passsing 'undefined' to the 'optional' helper does not make sense./); }); }); From e5cb8234d9b84ff7ad44a1b96df85e9936cc9dde Mon Sep 17 00:00:00 2001 From: Alex LaFroscia Date: Wed, 26 Dec 2018 14:23:46 -0500 Subject: [PATCH 3/6] refactor: convert validator into Monoid This refactors the concept of what a Validator actually is, changing it from a Function into a Class. This allows for co-locating all of the information about how to check a value and format the error message into a single location. The Validator class can be treated as a Monoid, meaning that it can be combined with other instances of itself to build up more complex Validators. Many operations that we supported previously can be thought of as combinations of other Validators, so I thought that formalizing that made sense. With base combinators like `and` and `or`, we can build up some of the type modifiers that we support like `arrayOf` or `oneOf`. All of these can be expressed as Validators, in terms of other Validators. --- addon/-private/combinators/and.js | 20 ++++++++ addon/-private/combinators/index.js | 3 ++ addon/-private/combinators/not.js | 17 +++++++ addon/-private/combinators/or.js | 17 +++++++ addon/-private/resolve-validator.js | 63 ++++++++++++++++++++++++ addon/-private/types/array-of.js | 26 ++++++++-- addon/-private/types/one-of.js | 16 ++++-- addon/-private/types/optional.js | 32 +++++++----- addon/-private/types/shape-of.js | 51 ++++++++++++------- addon/-private/types/union-of.js | 14 ++++-- addon/-private/validators.js | 58 ---------------------- addon/-private/validators/-base.js | 21 ++++++++ addon/-private/validators/any.js | 7 +++ addon/-private/validators/instance-of.js | 29 +++++++++++ addon/-private/validators/type-match.js | 25 ++++++++++ addon/-private/validators/value-match.js | 21 ++++++++ addon/-private/wrap-field.js | 21 ++------ addon/index.js | 2 +- 18 files changed, 326 insertions(+), 117 deletions(-) create mode 100644 addon/-private/combinators/and.js create mode 100644 addon/-private/combinators/index.js create mode 100644 addon/-private/combinators/not.js create mode 100644 addon/-private/combinators/or.js create mode 100644 addon/-private/resolve-validator.js delete mode 100644 addon/-private/validators.js create mode 100644 addon/-private/validators/-base.js create mode 100644 addon/-private/validators/any.js create mode 100644 addon/-private/validators/instance-of.js create mode 100644 addon/-private/validators/type-match.js create mode 100644 addon/-private/validators/value-match.js diff --git a/addon/-private/combinators/and.js b/addon/-private/combinators/and.js new file mode 100644 index 0000000..b8d5486 --- /dev/null +++ b/addon/-private/combinators/and.js @@ -0,0 +1,20 @@ +import BaseValidator from '../validators/-base'; + +export class AndValidator extends BaseValidator { + constructor(...validators) { + super(); + + this.validators = validators; + } + + check(value) { + return this.validators.reduce( + (acc, validator) => acc && validator.check(value), + true + ); + } +} + +export default function and(...validators) { + return new AndValidator(...validators); +} diff --git a/addon/-private/combinators/index.js b/addon/-private/combinators/index.js new file mode 100644 index 0000000..1f5f375 --- /dev/null +++ b/addon/-private/combinators/index.js @@ -0,0 +1,3 @@ +export { default as and } from './and'; +export { default as not } from './not'; +export { default as or } from './or'; diff --git a/addon/-private/combinators/not.js b/addon/-private/combinators/not.js new file mode 100644 index 0000000..f317cd2 --- /dev/null +++ b/addon/-private/combinators/not.js @@ -0,0 +1,17 @@ +import BaseValidator from '../validators/-base'; + +export class NotValidator extends BaseValidator { + constructor(validator) { + super(); + + this.validator = validator; + } + + check(value) { + return !this.validator.check(value); + } +} + +export default function not(validator) { + return new NotValidator(validator); +} diff --git a/addon/-private/combinators/or.js b/addon/-private/combinators/or.js new file mode 100644 index 0000000..8f515d9 --- /dev/null +++ b/addon/-private/combinators/or.js @@ -0,0 +1,17 @@ +import BaseValidator from '../validators/-base'; + +export class OrValidator extends BaseValidator { + constructor(...validators) { + super(); + + this.validators = validators; + } + + check(value) { + return this.validators.some(validator => validator.check(value)); + } +} + +export default function or(...validators) { + return new OrValidator(...validators); +} diff --git a/addon/-private/resolve-validator.js b/addon/-private/resolve-validator.js new file mode 100644 index 0000000..dfb8bde --- /dev/null +++ b/addon/-private/resolve-validator.js @@ -0,0 +1,63 @@ +import { assert } from '@ember/debug'; + +import AnyValidator from './validators/any'; +import BaseValidator from './validators/-base'; +import InstanceOfValidator from './validators/instance-of'; +import { + BOOLEAN as BOOLEAN_TYPE, + NUMBER as NUMBER_TYPE, + STRING as STRING_TYPE, + SYMBOL as SYMBOL_TYPE +} from './validators/type-match'; +import { + NULL as NULL_VALUE, + UNDEFINED as UNDEFINED_VALUE +} from './validators/value-match'; +import { not, or } from './combinators/index'; + +const primitiveTypeValidators = { + any: new AnyValidator('any'), + object: not( + or( + BOOLEAN_TYPE, + NUMBER_TYPE, + STRING_TYPE, + SYMBOL_TYPE, + NULL_VALUE, + UNDEFINED_VALUE + ) + ), + + boolean: BOOLEAN_TYPE, + number: NUMBER_TYPE, + string: STRING_TYPE, + symbol: SYMBOL_TYPE, + + null: NULL_VALUE, + undefined: UNDEFINED_VALUE +}; + +export default function resolveValidator(type) { + if (type === null) { + return NULL_VALUE; + } else if (type === undefined) { + return UNDEFINED_VALUE; + } else if (type instanceof BaseValidator) { + return type; + } else if (typeof type === 'function' || typeof type === 'object') { + // We allow objects for certain classes in IE, like Element, which have typeof 'object' for some reason + return new InstanceOfValidator(type); + } else if (typeof type === 'string') { + assert( + `Unknown primitive type received: ${type}`, + primitiveTypeValidators[type] !== undefined + ); + + return primitiveTypeValidators[type]; + } else { + assert( + `Types must either be a primitive type string, class, validator, or null or undefined, received: ${type}`, + false + ); + } +} diff --git a/addon/-private/types/array-of.js b/addon/-private/types/array-of.js index fb87a81..2723b42 100644 --- a/addon/-private/types/array-of.js +++ b/addon/-private/types/array-of.js @@ -1,6 +1,24 @@ -import { assert } from '@ember/debug'; -import { resolveValidator, makeValidator } from '../validators'; import { isArray } from '@ember/array'; +import { assert } from '@ember/debug'; + +import resolveValidator from '../resolve-validator'; +import BaseValidator from '../validators/-base'; + +class ArrayOfValidator extends BaseValidator { + constructor(validator) { + super(); + + this.validator = validator; + } + + toString() { + return `arrayOf(${this.validator})`; + } + + check(value) { + return isArray(value) && value.every(value => this.validator.check(value)); + } +} export default function arrayOf(type) { assert( @@ -10,7 +28,5 @@ export default function arrayOf(type) { const validator = resolveValidator(type); - return makeValidator(`arrayOf(${validator})`, value => { - return isArray(value) && value.every(validator); - }); + return new ArrayOfValidator(validator); } diff --git a/addon/-private/types/one-of.js b/addon/-private/types/one-of.js index d015beb..cd8de89 100644 --- a/addon/-private/types/one-of.js +++ b/addon/-private/types/one-of.js @@ -1,5 +1,13 @@ import { assert } from '@ember/debug'; -import { makeValidator } from '../validators'; + +import { OrValidator } from '../combinators/or'; +import ValueMatchValidator from '../validators/value-match'; + +class OneOfValidator extends OrValidator { + toString() { + return `oneOf(${this.validators.join()})`; + } +} export default function oneOf(...list) { assert( @@ -11,7 +19,7 @@ export default function oneOf(...list) { list.every(item => typeof item === 'string') ); - return makeValidator(`oneOf(${list.join()})`, value => { - return list.includes(value); - }); + const validators = list.map(value => new ValueMatchValidator(value)); + + return new OneOfValidator(...validators); } diff --git a/addon/-private/types/optional.js b/addon/-private/types/optional.js index addabfa..3a7814d 100644 --- a/addon/-private/types/optional.js +++ b/addon/-private/types/optional.js @@ -1,8 +1,23 @@ import { assert } from '@ember/debug'; -import { resolveValidator, makeValidator } from '../validators'; -const nullValidator = resolveValidator(null); -const undefinedValidator = resolveValidator(undefined); +import resolveValidator from '../resolve-validator'; +import { + NULL as NULL_VALUE, + UNDEFINED as UNDEFINED_VALUE +} from '../validators/value-match'; +import { OrValidator } from '../combinators/or'; + +class OptionalValidator extends OrValidator { + constructor(validator) { + super(NULL_VALUE, UNDEFINED_VALUE, validator); + + this.originalValidator = validator; + } + + toString() { + return `optional(${this.originalValidator})`; + } +} export default function optional(type) { assert( @@ -11,20 +26,15 @@ export default function optional(type) { ); const validator = resolveValidator(type); - const validatorDesc = validator.toString(); assert( `Passsing 'null' to the 'optional' helper does not make sense.`, - validatorDesc !== 'null' + validator.toString() !== 'null' ); assert( `Passsing 'undefined' to the 'optional' helper does not make sense.`, - validatorDesc !== 'undefined' + validator.toString() !== 'undefined' ); - return makeValidator( - `optional(${validator})`, - value => - nullValidator(value) || undefinedValidator(value) || validator(value) - ); + return new OptionalValidator(validator); } diff --git a/addon/-private/types/shape-of.js b/addon/-private/types/shape-of.js index 71913ad..597f0ce 100644 --- a/addon/-private/types/shape-of.js +++ b/addon/-private/types/shape-of.js @@ -1,6 +1,37 @@ import { assert } from '@ember/debug'; import { get } from '@ember/object'; -import { resolveValidator, makeValidator } from '../validators'; + +import resolveValidator from '../resolve-validator'; +import BaseValidator from '../validators/-base'; + +class ShapeOfValidator extends BaseValidator { + constructor(shape) { + super(); + + this.shape = {}; + this.typeDesc = []; + + for (let key in shape) { + this.shape[key] = resolveValidator(shape[key]); + + this.typeDesc.push(`${key}:${shape[key]}`); + } + } + + toString() { + return `shapeOf({${this.typeDesc.join()}})`; + } + + check(value) { + for (let key in this.shape) { + if (this.shape[key].check(get(value, key)) !== true) { + return false; + } + } + + return true; + } +} export default function shapeOf(shape) { assert( @@ -16,21 +47,5 @@ export default function shapeOf(shape) { Object.keys(shape).length > 0 ); - let typeDesc = []; - - for (let key in shape) { - shape[key] = resolveValidator(shape[key]); - - typeDesc.push(`${key}:${shape[key]}`); - } - - return makeValidator(`shapeOf({${typeDesc.join()}})`, value => { - for (let key in shape) { - if (shape[key](get(value, key)) !== true) { - return false; - } - } - - return true; - }); + return new ShapeOfValidator(shape); } diff --git a/addon/-private/types/union-of.js b/addon/-private/types/union-of.js index 3a88de9..388b1b4 100644 --- a/addon/-private/types/union-of.js +++ b/addon/-private/types/union-of.js @@ -1,5 +1,13 @@ import { assert } from '@ember/debug'; -import { resolveValidator, makeValidator } from '../validators'; + +import resolveValidator from '../resolve-validator'; +import { OrValidator } from '../combinators/or'; + +class UnionOfValidator extends OrValidator { + toString() { + return `unionOf(${this.validators.join()})`; + } +} export default function unionOf(...types) { assert( @@ -9,7 +17,5 @@ export default function unionOf(...types) { const validators = types.map(resolveValidator); - return makeValidator(`unionOf(${validators.join()})`, value => { - return validators.some(validator => validator(value)); - }); + return new UnionOfValidator(...validators); } diff --git a/addon/-private/validators.js b/addon/-private/validators.js deleted file mode 100644 index 9c54b11..0000000 --- a/addon/-private/validators.js +++ /dev/null @@ -1,58 +0,0 @@ -import { assert } from '@ember/debug'; - -function instanceOf(type) { - return makeValidator(type.toString(), value => value instanceof type); -} - -const primitiveTypeValidators = { - any: makeValidator('any', () => true), - object: makeValidator('object', value => { - return ( - typeof value !== 'boolean' && - typeof value !== 'number' && - typeof value !== 'string' && - typeof value !== 'symbol' && - value !== null && - value !== undefined - ); - }), - - boolean: makeValidator('boolean', value => typeof value === 'boolean'), - number: makeValidator('number', value => typeof value === 'number'), - string: makeValidator('string', value => typeof value === 'string'), - symbol: makeValidator('symbol', value => typeof value === 'symbol'), - - null: makeValidator('null', value => value === null), - undefined: makeValidator('undefined', value => value === undefined) -}; - -export function makeValidator(desc, fn) { - fn.isValidator = true; - fn.toString = () => desc; - return fn; -} - -export function resolveValidator(type) { - if (type === null || type === undefined) { - return type === null - ? primitiveTypeValidators.null - : primitiveTypeValidators.undefined; - } else if (type.isValidator === true) { - return type; - } else if (typeof type === 'function' || typeof type === 'object') { - // We allow objects for certain classes in IE, like Element, which have typeof 'object' for some reason - return instanceOf(type); - } else if (typeof type === 'string') { - assert( - `Unknown primitive type received: ${type}`, - primitiveTypeValidators[type] !== undefined - ); - - return primitiveTypeValidators[type]; - } else { - assert( - `Types must either be a primitive type string, class, validator, or null or undefined, received: ${type}`, - false - ); - } -} diff --git a/addon/-private/validators/-base.js b/addon/-private/validators/-base.js new file mode 100644 index 0000000..bed39cb --- /dev/null +++ b/addon/-private/validators/-base.js @@ -0,0 +1,21 @@ +export default class BaseValidator { + check() { + throw new Error('Subclass must implement a `check` method'); + } + + formatValue(value) { + return typeof value === 'string' ? `'${value}'` : value; + } + + run(klass, key, value, phase) { + if (this.check(value) === false) { + let formattedValue = this.formatValue(value); + + throw new Error( + `${ + klass.name + }#${key} expected value of type ${this} during '${phase}', but received: ${formattedValue}` + ); + } + } +} diff --git a/addon/-private/validators/any.js b/addon/-private/validators/any.js new file mode 100644 index 0000000..ea1bb39 --- /dev/null +++ b/addon/-private/validators/any.js @@ -0,0 +1,7 @@ +import BaseValidator from './-base'; + +export default class AnyValidator extends BaseValidator { + check() { + return true; + } +} diff --git a/addon/-private/validators/instance-of.js b/addon/-private/validators/instance-of.js new file mode 100644 index 0000000..81d2f86 --- /dev/null +++ b/addon/-private/validators/instance-of.js @@ -0,0 +1,29 @@ +import BaseValidator from './-base'; + +export default class InstanceOfValidator extends BaseValidator { + constructor(klass) { + super(); + + this.klass = klass; + } + + formatValue(value) { + if (value === null) { + return `null`; + } + + if (value === undefined) { + return `undefined`; + } + + return `an instance of \`${value.constructor.name}\``; + } + + toString() { + return `\`${this.klass.name}\``; + } + + check(value) { + return value instanceof this.klass; + } +} diff --git a/addon/-private/validators/type-match.js b/addon/-private/validators/type-match.js new file mode 100644 index 0000000..09c0834 --- /dev/null +++ b/addon/-private/validators/type-match.js @@ -0,0 +1,25 @@ +import BaseValidator from './-base'; + +export default class TypeMatchValidator extends BaseValidator { + constructor(type) { + super(); + + this.type = type; + } + + toString() { + return this.type; + } + + check(value) { + return typeof value === this.type; + } +} + +export const BOOLEAN = new TypeMatchValidator('boolean'); + +export const NUMBER = new TypeMatchValidator('number'); + +export const STRING = new TypeMatchValidator('string'); + +export const SYMBOL = new TypeMatchValidator('symbol'); diff --git a/addon/-private/validators/value-match.js b/addon/-private/validators/value-match.js new file mode 100644 index 0000000..ceb72ff --- /dev/null +++ b/addon/-private/validators/value-match.js @@ -0,0 +1,21 @@ +import BaseValidator from './-base'; + +export default class ValueMatchValidator extends BaseValidator { + constructor(value) { + super(); + + this.value = value; + } + + toString() { + return `${this.value}`; + } + + check(value) { + return this.value === value; + } +} + +export const NULL = new ValueMatchValidator(null); + +export const UNDEFINED = new ValueMatchValidator(undefined); diff --git a/addon/-private/wrap-field.js b/addon/-private/wrap-field.js index 2c135b4..a3d3319 100644 --- a/addon/-private/wrap-field.js +++ b/addon/-private/wrap-field.js @@ -24,15 +24,15 @@ class ValidatedProperty { this.originalValue = originalValue; this.typeValidators = typeValidators; - runValidator(typeValidators, klass, keyName, originalValue, 'init'); + typeValidators.run(klass, keyName, originalValue, 'init'); } get(obj, keyName) { let { klass, typeValidators } = this; let newValue = this._get(obj, keyName); - if (typeValidators.length > 0) { - runValidator(typeValidators, klass, keyName, newValue, 'get'); + if (typeValidators) { + typeValidators.run(klass, keyName, newValue, 'get'); } return newValue; @@ -42,8 +42,8 @@ class ValidatedProperty { let { klass, typeValidators } = this; let newValue = this._set(obj, keyName, value); - if (typeValidators.length > 0) { - runValidator(typeValidators, klass, keyName, newValue, 'set'); + if (typeValidators) { + typeValidators.run(klass, keyName, newValue, 'set'); } return newValue; @@ -121,17 +121,6 @@ class ComputedValidatedProperty extends ValidatedProperty { } } -function runValidator(validator, klass, key, value, phase) { - if (validator(value) === false) { - let formattedValue = typeof value === 'string' ? `'${value}'` : value; - throw new Error( - `${ - klass.name - }#${key} expected value of type ${validator} during '${phase}', but received: ${formattedValue}` - ); - } -} - export function wrapField(klass, instance, validations, keyName) { const typeValidators = validations[keyName]; diff --git a/addon/index.js b/addon/index.js index d59917e..a136faf 100644 --- a/addon/index.js +++ b/addon/index.js @@ -1,6 +1,6 @@ import { assert } from '@ember/debug'; -import { resolveValidator } from './-private/validators'; +import resolveValidator from './-private/resolve-validator'; import { addValidationFor } from './-private/validations-for'; import { hasExtension as hasValidationExtension, From 7c1cce3d5bfc9a6c7373dbc60c4f2cd0ccbf8652 Mon Sep 17 00:00:00 2001 From: Alex LaFroscia Date: Wed, 26 Dec 2018 15:24:25 -0500 Subject: [PATCH 4/6] refactor: error message formatting --- addon/-private/validators/-base.js | 31 +++++++++++++++++++++++- addon/-private/validators/instance-of.js | 12 --------- tests/unit/types/array-of-test.js | 2 +- tests/unit/types/shape-of-test.js | 2 +- 4 files changed, 32 insertions(+), 15 deletions(-) diff --git a/addon/-private/validators/-base.js b/addon/-private/validators/-base.js index bed39cb..2213e0c 100644 --- a/addon/-private/validators/-base.js +++ b/addon/-private/validators/-base.js @@ -1,10 +1,39 @@ +function isPrimitive(value) { + return ( + typeof value === 'boolean' || + typeof value === 'number' || + typeof value === 'string' || + typeof value === 'symbol' + ); +} + +function formatSingleValue(value) { + if (value === null) { + return 'null'; + } + + if (value === undefined) { + return 'undefined'; + } + + if (isPrimitive(value)) { + return typeof value === 'string' ? `'${value}'` : value; + } + + return `an instance of \`${value.constructor.name}\``; +} + export default class BaseValidator { check() { throw new Error('Subclass must implement a `check` method'); } formatValue(value) { - return typeof value === 'string' ? `'${value}'` : value; + if (Array.isArray(value)) { + return `[${value.map(formatSingleValue).join(', ')}]`; + } + + return formatSingleValue(value); } run(klass, key, value, phase) { diff --git a/addon/-private/validators/instance-of.js b/addon/-private/validators/instance-of.js index 81d2f86..08df548 100644 --- a/addon/-private/validators/instance-of.js +++ b/addon/-private/validators/instance-of.js @@ -7,18 +7,6 @@ export default class InstanceOfValidator extends BaseValidator { this.klass = klass; } - formatValue(value) { - if (value === null) { - return `null`; - } - - if (value === undefined) { - return `undefined`; - } - - return `an instance of \`${value.constructor.name}\``; - } - toString() { return `\`${this.klass.name}\``; } diff --git a/tests/unit/types/array-of-test.js b/tests/unit/types/array-of-test.js index 9454eeb..6cfa1a4 100644 --- a/tests/unit/types/array-of-test.js +++ b/tests/unit/types/array-of-test.js @@ -22,7 +22,7 @@ module('Unit | types | arrayOf', function() { } Foo.create({ bar: ['baz', 2] }); - }, /Foo#bar expected value of type arrayOf\(string\) during 'init', but received: baz,2/); + }, /Foo#bar expected value of type arrayOf\(string\) during 'init', but received: \['baz', 2\]/); }); test('it throws if type does not match', function(assert) { diff --git a/tests/unit/types/shape-of-test.js b/tests/unit/types/shape-of-test.js index 1b7a7b3..2c8e9bb 100644 --- a/tests/unit/types/shape-of-test.js +++ b/tests/unit/types/shape-of-test.js @@ -35,7 +35,7 @@ module('Unit | types | shapeOf', function() { } Foo.create({ bar: { qux: 'baz' } }); - }, /Foo#bar expected value of type shapeOf\({foo:string}\) during 'init', but received: \[object Object\]/); + }, /Foo#bar expected value of type shapeOf\({foo:string}\) during 'init', but received: an instance of `Object`/); }); test('it throws if type does not match', function(assert) { From f35f4acbc925075e540786060591a7b79d83f60c Mon Sep 17 00:00:00 2001 From: Alex LaFroscia Date: Wed, 26 Dec 2018 15:30:49 -0500 Subject: [PATCH 5/6] docs: add documentation on supporting checking against a class Closes #87 --- README.md | 28 +++++++++++++++- tests/unit/types/constructor-test.js | 50 ++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 tests/unit/types/constructor-test.js diff --git a/README.md b/README.md index dc00957..f4bfc0f 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,11 @@ When rendering a component that uses `@argument`, the initial value of the prope In addition, any unexpected arguments to a component will also cause an error. -### Defining Types +## Defining Types + +The `@argument` decorator takes a definition for what kind of value the property should be set to. Usually, this will represent the type of the value. + +### Primitive Types For primitives types, the name should be provided as a string (as with `string` in the example above). The available types match those of Typescript, including: @@ -89,6 +93,28 @@ In addition, this library includes several predefined types for convenience: These types can also be imported from `@ember-decorators/argument/types` +### Class Instances + +The `@argument` decorator can also take a class constructor to validate that the property value is an instance of that class + +```js +import Component from '@ember/component'; +import { argument } from '@ember-decorators/argument'; + +class Task { + constructor() { + this.complete = false; + } +} + +export default class TaskComponent extends Component { + @argument(Task) + task; +} +``` + +Passing a class works with all of the type helpers mentioned above. + ## Installation While `ember-decorators` is not a hard requirement to use this addon, it's recommended as it adds the base class field and decorator babel transforms diff --git a/tests/unit/types/constructor-test.js b/tests/unit/types/constructor-test.js new file mode 100644 index 0000000..d2c6a7c --- /dev/null +++ b/tests/unit/types/constructor-test.js @@ -0,0 +1,50 @@ +import { test, module } from 'qunit'; +import EmberObject from '@ember/object'; + +import { argument } from '@ember-decorators/argument'; +import { arrayOf } from '@ember-decorators/argument/types'; + +class Thing {} +class OtherThing {} + +module('Unit | types | constructor', function() { + test('mathing against a class instance', function(assert) { + class Foo extends EmberObject { + @argument(Thing) bar; + } + + Foo.create({ bar: new Thing() }); + + assert.throws(function() { + Foo.create({ bar: new OtherThing() }); + }, /Foo#bar expected value of type `Thing` during 'init', but received: an instance of `OtherThing`/); + }); + + test('matching against a built-in constructor', function(assert) { + class Foo extends EmberObject { + @argument(Boolean) bar; + } + + Foo.create({ bar: new Boolean(false) }); + + assert.throws(function() { + Foo.create({ bar: 'test' }); + }, /Foo#bar expected value of type `Boolean` during 'init', but received: 'test'/); + }); + + test('working with helpers', function(assert) { + class Foo extends EmberObject { + @argument(arrayOf(Thing)) bar; + } + + Foo.create({ bar: [new Thing()] }); + + assert.throws(function() { + Foo.create({ bar: new Thing() }); + }, "Foo#bar expected value of type arrayOf(`Thing`) during 'init', but received: an instance of `Thing`"); + + assert.throws(function() { + Foo.create({ bar: [new OtherThing()] }); + }, "Foo#bar expected value of type arrayOf(`Thing`) during 'init', but received: [an instance of `OtherThing`]"); + }); +}); From 29ddb02544d610f79bf8e09dfc22c75543865910 Mon Sep 17 00:00:00 2001 From: Alex LaFroscia Date: Wed, 26 Dec 2018 16:28:59 -0500 Subject: [PATCH 6/6] feat: add exported type for `Any` Closes #87 --- addon/types.js | 2 ++ index.js | 1 + tests/dummy/app/components/kitchen-sink.js | 4 ++++ 3 files changed, 7 insertions(+) diff --git a/addon/types.js b/addon/types.js index 997b98e..b242408 100644 --- a/addon/types.js +++ b/addon/types.js @@ -1,5 +1,7 @@ import unionOf from './-private/types/union-of'; +export { default as Any } from './-private/validators/any'; + export { default as arrayOf } from './-private/types/array-of'; export { default as optional } from './-private/types/optional'; export { default as oneOf } from './-private/types/one-of'; diff --git a/index.js b/index.js index 9b2f610..43b9c79 100644 --- a/index.js +++ b/index.js @@ -63,6 +63,7 @@ module.exports = { imports: { '@ember-decorators/argument': ['argument'], '@ember-decorators/argument/types': [ + 'Any', 'arrayOf', 'optional', 'oneOf', diff --git a/tests/dummy/app/components/kitchen-sink.js b/tests/dummy/app/components/kitchen-sink.js index 38298e8..9b23b44 100644 --- a/tests/dummy/app/components/kitchen-sink.js +++ b/tests/dummy/app/components/kitchen-sink.js @@ -1,6 +1,7 @@ import Component from '@ember/component'; import { argument } from '@ember-decorators/argument'; import { + Any, arrayOf, optional, oneOf, @@ -17,6 +18,9 @@ import template from '../templates/components/kitchen-sink'; @layout(template) export default class KitchenSinkComponent extends Component { + @argument(Any) + anything; + @argument('string') someString;