From c8cf03f200277d395ee75aada2d65a9cad716ff8 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Fri, 26 Sep 2014 17:36:57 -0700 Subject: [PATCH] refactor: move rtts-assert into `modules` directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rtts assertion lib is only needed for js, but it should be treated like any other module (e.g. facade, …) --- gulpfile.js | 7 +- karma-js.conf.js | 5 +- modules/core/src/view/view.js | 2 +- modules/rtts_assert/API.md | 44 ++ modules/rtts_assert/README.md | 1 + modules/rtts_assert/package.json | 31 ++ modules/rtts_assert/src/rtts_assert.es6 | 327 +++++++++++++++ modules/rtts_assert/test/rtts_assert_spec.es6 | 377 ++++++++++++++++++ test-main.js | 5 +- 9 files changed, 789 insertions(+), 10 deletions(-) create mode 100644 modules/rtts_assert/API.md create mode 100644 modules/rtts_assert/README.md create mode 100644 modules/rtts_assert/package.json create mode 100644 modules/rtts_assert/src/rtts_assert.es6 create mode 100644 modules/rtts_assert/test/rtts_assert_spec.es6 diff --git a/gulpfile.js b/gulpfile.js index e40833556e..0442370bee 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -20,7 +20,7 @@ var js2es5Options = { types: true, // parse types script: false, // parse as a module modules: 'register', - typeAssertionModule: 'assert', + typeAssertionModule: 'rtts_assert/rtts_assert', typeAssertions: true }; @@ -52,12 +52,9 @@ gulp.task('jsRuntime/build', function() { function createJsRuntimeTask(isWatch) { var srcFn = isWatch ? watch : gulp.src.bind(gulp); - var rttsAssert = srcFn('tools/rtts-assert/src/assert.js') - .pipe(gulpTraceur(js2es5Options, resolveModuleName)) - .pipe(gulp.dest('build/js')); var traceurRuntime = srcFn(gulpTraceur.RUNTIME_PATH) .pipe(gulp.dest('build/js')); - return mergeStreams(rttsAssert, traceurRuntime); + return traceurRuntime; } // ----------------------- diff --git a/karma-js.conf.js b/karma-js.conf.js index 8f91d870eb..5b1ec0c654 100644 --- a/karma-js.conf.js +++ b/karma-js.conf.js @@ -26,9 +26,8 @@ module.exports = function(config) { script: false, modules: 'register', types: true, - // TODO: turn this on! - // typeAssertions: true, - // typeAssertionModule: 'assert', + typeAssertions: true, + typeAssertionModule: 'rtts_assert/rtts_assert', annotations: true }, resolveModuleName: function(fileName) { diff --git a/modules/core/src/view/view.js b/modules/core/src/view/view.js index 51213c73a3..ddf7c9949f 100644 --- a/modules/core/src/view/view.js +++ b/modules/core/src/view/view.js @@ -21,7 +21,7 @@ export class View { onRecordChange(record:Record, target) { // dispatch to element injector or text nodes based on context - if (target is ElementInjectorTarge) { + if (target instanceof ElementInjectorTarge) { // we know that it is ElementInjectorTarge var eTarget:ElementInjectorTarget = target; onChangeDispatcher.notify(this, eTarget); diff --git a/modules/rtts_assert/API.md b/modules/rtts_assert/API.md new file mode 100644 index 0000000000..dbe350fdc7 --- /dev/null +++ b/modules/rtts_assert/API.md @@ -0,0 +1,44 @@ +// Asserting APIs: +// - generated by Traceur (based on type annotations) +// - can be also used in tests for instance +assert.type(something, Type); +assert.returnType(returnValue, Type); +assert.argumentTypes(firstArg, Type, secondArg, Type); + +// this can be used anywhere in the code +// (useful inside test, when we don't wanna define an interface) +assert(value).is(...) + + +// Custom type assert: +// - i have a custom type +// - adding an assert methos +assert.define(MyUser, function(value) { + assert(value).is(Type, Type2); // or + assert(value, 'name').is(assert.string); + assert(value, 'contact').is(assert.structure({ + email: assert.string, + cell: assert.string + })); + assert(value, 'contacts').is(assert.arrayOf(assert.structure({email: assert.string}))); +}); + + + +// Define interface (an empty type with assert method) +// - returns an empty class with assert method +var Email = assert.define('IEmail', function(value) { + assert(value).is(String); + + if (value.indexOf('@') !== -1) { + assert.fail('has to contain "@"'); + } +}); + + +// Predefined types +assert.string +assert.number +assert.boolean +assert.arrayOf(...types) +assert.structure(object) diff --git a/modules/rtts_assert/README.md b/modules/rtts_assert/README.md new file mode 100644 index 0000000000..784709602d --- /dev/null +++ b/modules/rtts_assert/README.md @@ -0,0 +1 @@ +http://angular.github.io/assert/ diff --git a/modules/rtts_assert/package.json b/modules/rtts_assert/package.json new file mode 100644 index 0000000000..3a56cf734b --- /dev/null +++ b/modules/rtts_assert/package.json @@ -0,0 +1,31 @@ +{ + "name": "rtts-assert", + "version": "0.0.1", + "description": "A type assertion library for Traceur.", + "main": "./dist/cjs/assert.js", + "homepage": "https://github.com/angular/assert", + "repository": { + "type": "git", + "url": "git://github.com/angular/assert.git" + }, + "bugs": { + "url": "https://github.com/angular/assert/issues" + }, + "dependencies": {}, + "devDependencies": { + "gulp": "^3.5.6", + "gulp-connect": "~1.0.5", + "gulp-traceur": "~0.4.0", + "karma": "^0.12.1", + "karma-chrome-launcher": "^0.1.2", + "karma-jasmine": "^0.2.2", + "karma-requirejs": "^0.2.1", + "karma-traceur-preprocessor": "^0.2.2", + "pipe": "git://github.com/angular/pipe#remove-transitive-deps" + }, + "scripts": { + "test": "karma start --single-run" + }, + "author": "Vojta Jína ", + "license": "Apache-2.0" +} diff --git a/modules/rtts_assert/src/rtts_assert.es6 b/modules/rtts_assert/src/rtts_assert.es6 new file mode 100644 index 0000000000..da6ea51a64 --- /dev/null +++ b/modules/rtts_assert/src/rtts_assert.es6 @@ -0,0 +1,327 @@ +// TODO(vojta): +// - extract into multiple files +// - different error types +// - simplify/humanize error messages +// - throw when invalid input (such as odd number of args into assert.argumentTypes) + +var POSITION_NAME = ['', '1st', '2nd', '3rd']; +function argPositionName(i) { + var position = (i / 2) + 1; + + return POSITION_NAME[position] || (position + 'th'); +} + +var primitives = $traceurRuntime.type; + +function assertArgumentTypes(...params) { + var actual, type; + var currentArgErrors; + var errors = []; + var msg; + + for (var i = 0, l = params.length; i < l; i = i + 2) { + actual = params[i]; + type = params[i + 1]; + + currentArgErrors = []; + + // currentStack = []; + // + + if (!isType(actual, type, currentArgErrors)) { + + // console.log(JSON.stringify(errors, null, ' ')); + // TODO(vojta): print "an instance of" only if T starts with uppercase. + errors.push(argPositionName(i) + ' argument has to be an instance of ' + prettyPrint(type) + ', got ' + prettyPrint(actual)); + if (currentArgErrors.length) { + errors.push(currentArgErrors); + } + } + } + + if (errors.length) { + throw new Error('Invalid arguments given!\n' + formatErrors(errors)); + } +} + +function prettyPrint(value) { + if (typeof value === 'undefined') { + return 'undefined'; + } + + if (typeof value === 'string') { + return '"' + value + '"'; + } + + if (typeof value === 'boolean') { + return value.toString(); + } + + if (value === null) { + return 'null'; + } + + if (typeof value === 'object') { + if (value.map) { + return '[' + value.map(prettyPrint).join(', ') + ']'; + } + + var properties = Object.keys(value); + return '{' + properties.map((p) => p + ': ' + prettyPrint(value[p])).join(', ') + '}'; + } + + return value.__assertName || value.name || value.toString(); +} + +function isType(value, T, errors) { + + if (T === primitives.void) { + return typeof value === 'undefined'; + } + + if (T === primitives.any || value === null) { + return true; + } + + if (T === primitives.string) { + return typeof value === 'string'; + } + + if (T === primitives.number) { + return typeof value === 'number'; + } + + if (T === primitives.boolean) { + return typeof value === 'boolean'; + } + + // var parentStack = currentStack; + // currentStack = []; + + // shouldnt this create new stack? + if (typeof T.assert === 'function') { + var parentStack = currentStack; + var isValid; + currentStack = errors; + try { + isValid = T.assert(value) ; + } catch (e) { + fail(e.message); + isValid = false; + } + + currentStack = parentStack; + + if (typeof isValid === 'undefined') { + isValid = errors.length === 0; + } + + return isValid; + + // if (!currentStack.length) { + // currentStack = parentStack; + // return []; + // } + // var res = currentStack; + // currentStack = parentStack; + // return ['not instance of ' + prettyPrint(T), res]; + } + + return value instanceof T; + + // if (!(value instanceof T)) { + // fail('not instance of ' + prettyPrint(T)); + // } + + // var res = currentStack; + // currentStack = parentStack; + + // return res; +} + +function formatErrors(errors, indent = ' ') { + return errors.map((e) => { + if (typeof e === 'string') return indent + '- ' + e; + return formatErrors(e, indent + ' '); + }).join('\n'); +} + + +// assert a type of given value and throw if does not pass +function type(actual, T) { + var errors = []; + // currentStack = []; + + if (!isType(actual, T, errors)) { + // console.log(JSON.stringify(errors, null, ' ')); + // TODO(vojta): print "an instance of" only if T starts with uppercase. + var msg = 'Expected an instance of ' + prettyPrint(T) + ', got ' + prettyPrint(actual) + '!'; + if (errors.length) { + msg += '\n' + formatErrors(errors); + } + + throw new Error(msg); + } +} + +function returnType(actual, T) { + var errors = []; + // currentStack = []; + + if (!isType(actual, T, errors)) { + // console.log(JSON.stringify(errors, null, ' ')); + // TODO(vojta): print "an instance of" only if T starts with uppercase. + var msg = 'Expected to return an instance of ' + prettyPrint(T) + ', got ' + prettyPrint(actual) + '!'; + if (errors.length) { + msg += '\n' + formatErrors(errors); + } + + throw new Error(msg); + } + + return actual; +} + + +// TODO(vojta): define these with DSL? +var string = define('string', function(value) { + return typeof value === 'string'; +}); + +// function string() {} +// string.assert = function(value) { +// return typeof value === 'string'; +// }; + +var boolean = define('boolean', function(value) { + return typeof value === 'boolean'; +}); +// function boolean() {} +// boolean.assert = function(value) { +// return typeof value === 'boolean'; +// }; + +var number = define('number', function(value) { + return typeof value === 'number'; +}); +// function number() {} +// number.assert = function(value) { +// return typeof value === 'number'; +// }; + + +function arrayOf(...types) { + return assert.define('array of ' + types.map(prettyPrint).join('/'), function(value) { + if (assert(value).is(Array)) { + for (var item of value) { + assert(item).is(...types); + } + } + }); +} + +function structure(definition) { + var properties = Object.keys(definition); + return assert.define('object with properties ' + properties.join(', '), function(value) { + if (assert(value).is(Object)) { + for (var property of properties) { + assert(value[property]).is(definition[property]); + } + } + }) +} + + + +// I'm sorry, bad global state... to make the API nice ;-) +var currentStack = []; + +function fail(message) { + currentStack.push(message); +} + +function define(classOrName, check) { + var cls = classOrName; + + if (typeof classOrName === 'string') { + cls = function() {}; + cls.__assertName = classOrName; + } + + cls.assert = function(value) { + // var parentStack = currentStack; + + // currentStack = []; + + return check(value); + + // if (currentStack.length) { + // parentStack.push(currentStack) + // } + // currentStack = parentStack; + }; + + return cls; +} + + + +function assert(value) { + return { + is: function is(...types) { + // var errors = [] + var allErrors = []; + var errors; + + for (var type of types) { + errors = []; + + if (isType(value, type, errors)) { + return true; + } + + // if no errors, merge multiple "is not instance of " into x/y/z ? + allErrors.push(prettyPrint(value) + ' is not instance of ' + prettyPrint(type)) + if (errors.length) { + allErrors.push(errors); + } + } + + // if (types.length > 1) { + // currentStack.push(['has to be ' + types.map(prettyPrint).join(' or '), ...allErrors]); + // } else { + currentStack.push(...allErrors); + // } + return false; + } + }; +} + + +// PUBLIC API + +// asserting API + +// throw if no type provided +assert.type = type; + +// throw if odd number of args +assert.argumentTypes = assertArgumentTypes; +assert.returnType = returnType; + + +// define AP; +assert.define = define; +assert.fail = fail; + +// primitive value type; +assert.string = string; +assert.number = number; +assert.boolean = boolean; + +// custom types +assert.arrayOf = arrayOf; +assert.structure = structure; + + +export {assert} diff --git a/modules/rtts_assert/test/rtts_assert_spec.es6 b/modules/rtts_assert/test/rtts_assert_spec.es6 new file mode 100644 index 0000000000..12be7cd645 --- /dev/null +++ b/modules/rtts_assert/test/rtts_assert_spec.es6 @@ -0,0 +1,377 @@ +// # Assert.js +// A run-time type assertion library for JavaScript. Designed to be used with [Traceur](https://github.com/google/traceur-compiler). + + +// - [Basic Type Check](#basic-type-check) +// - [Custom Check](#custom-check) +// - [Primitive Values](#primitive-values) +// - [Describing more complex types](#describing-more-complex-types) +// - [assert.arrayOf](#assert-arrayof) +// - [assert.structure](#assert-structure) +// - [Integrating with Traceur](#integrating-with-traceur) + +import {assert} from 'rtts_assert/rtts_assert'; + + + +// ## Basic Type Check +// By default, `instanceof` is used to check the type. +// +// Note that you can use `assert.type()` in unit tests or anywhere in your code. +// Most of the time, you will use it with Traceur. +// Jump to the [Traceur section](#integrating-with-traceur) to see an example of that. +describe('basic type check', function() { + + class Type {} + + it('should pass', function() { + assert.type(new Type(), Type); + }); + + + it('should fail', function() { + expect(() => assert.type(123, Type)) + .toThrowError('Expected an instance of Type, got 123!'); + }); + + + it('should allow null', function() { + assert.type(null, Type); + }); +}); + + + +// ## Custom Check +// Often, `instanceof` is not flexible enough. +// In that case, your type can define its own `assert` method which will be used instead. +// +// See [Describing More Complex Types](#describing-more-complex-types) for examples how to +// define custom checks using `assert.define()`. +describe('custom check', function() { + + class Type {} + + // the basic check can just return true/false, without specifying any reason + it('should pass when returns true', function() { + Type.assert = function(value) { + return true; + }; + + assert.type({}, Type); + }); + + + it('should fail when returns false', function() { + Type.assert = function(value) { + return false; + }; + + expect(() => assert.type({}, Type)) + .toThrowError('Expected an instance of Type, got {}!'); + }); + + + // Using `assert.fail()` allows to report even multiple errors. + it('should fail when calls assert.fail()', function() { + Type.assert = function(value) { + assert.fail('not smart enough'); + assert.fail('not blue enough'); + }; + + expect(() => assert.type({}, Type)) + .toThrowError('Expected an instance of Type, got {}!\n' + + ' - not smart enough\n' + + ' - not blue enough'); + }); + + + it('should fail when throws an exception', function() { + Type.assert = function(value) { + throw new Error('not long enough'); + }; + + expect(function() { + assert.type(12345, Type); + }).toThrowError('Expected an instance of Type, got 12345!\n' + + ' - not long enough'); + }); +}); + + + +// ## Primitive Values +// You don't want to check primitive values (such as strings, numbers, or booleans) using `typeof` rather than +// `instanceof`. +// +// Again, you probably won't write this code and rather use Traceur to do it for you, simply based on type annotations. +describe('primitive value check', function() { + var primitive = $traceurRuntime.type; + + describe('string', function() { + + it('should pass', function() { + assert.type('xxx', primitive.string); + }); + + + it('should fail', function() { + expect(() => assert.type(12345, primitive.string)) + .toThrowError('Expected an instance of string, got 12345!'); + }); + + it('should allow null', function() { + assert.type(null, primitive.string); + }); + }); + + + describe('number', function() { + + it('should pass', function() { + assert.type(123, primitive.number); + }); + + + it('should fail', function() { + expect(() => assert.type(false, primitive.number)) + .toThrowError('Expected an instance of number, got false!'); + }); + + it('should allow null', function() { + assert.type(null, primitive.number); + }); + }); + + + describe('boolean', function() { + + it('should pass', function() { + assert.type(true, primitive.boolean); + assert.type(false, primitive.boolean); + }); + + + it('should fail', function() { + expect(() => assert.type(123, primitive.boolean)) + .toThrowError('Expected an instance of boolean, got 123!'); + }); + + it('should allow null', function() { + assert.type(null, primitive.boolean); + }); + }); +}); + + + +// ## Describing more complex types +// +// Often, a simple type check using `instanceof` or `typeof` is not enough. +// That's why you can define custom checks using this DSL. +// The goal was to make them easy to compose and as descriptive as possible. +// Of course you can write your own DSL on the top of this. +describe('define', function() { + + // If the first argument to `assert.define()` is a type (function), it will define `assert` method on that function. + // + // In this example, being a type of Type means being a either a function or object. + it('should define assert for an existing type', function() { + class Type {} + + assert.define(Type, function(value) { + assert(value).is(Function, Object); + }); + + assert.type({}, Type); + assert.type(function() {}, Type); + expect(() => assert.type('str', Type)) + .toThrowError('Expected an instance of Type, got "str"!\n' + + ' - "str" is not instance of Function\n' + + ' - "str" is not instance of Object'); + }); + + + // If the first argument to `assert.define()` is a string, + // it will create an interface - basically an empty class with `assert` method. + it('should define an interface', function() { + var User = assert.define('MyUser', function(user) { + assert(user).is(Object); + }); + + assert.type({}, User); + expect(() => assert.type(12345, User)) + .toThrowError('Expected an instance of MyUser, got 12345!\n' + + ' - 12345 is not instance of Object'); + }); + + + // Here are a couple of more APIs to describe your custom types... + // + // ### assert.arrayOf + // Checks if the value is an array and if so, it checks whether all the items are one the given types. + // These types can be composed types, not just simple ones. + describe('arrayOf', function() { + + var Titles = assert.define('ListOfTitles', function(value) { + assert(value).is(assert.arrayOf(assert.string, assert.number)); + }); + + it('should pass', function () { + assert.type(['one', 55, 'two'], Titles); + }); + + + it('should fail when non-array given', function () { + expect(() => assert.type('foo', Titles)) + .toThrowError('Expected an instance of ListOfTitles, got "foo"!\n' + + ' - "foo" is not instance of array of string/number\n' + + ' - "foo" is not instance of Array'); + }); + + + it('should fail when an invalid item in the array', function () { + expect(() => assert.type(['aaa', true], Titles)) + .toThrowError('Expected an instance of ListOfTitles, got ["aaa", true]!\n' + + ' - ["aaa", true] is not instance of array of string/number\n' + + ' - true is not instance of string\n' + + ' - true is not instance of number'); + }); + }); + + + // ### assert.structure + // Similar to `assert.arrayOf` which checks a content of an array, + // `assert.structure` checks if the value is an object with specific properties. + describe('structure', function() { + + var User = assert.define('MyUser', function(value) { + assert(value).is(assert.structure({ + name: assert.string, + age: assert.number + })); + }); + + it('should pass', function () { + assert.type({name: 'Vojta', age: 28}, User); + }); + + + it('should fail when non-object given', function () { + expect(() => assert.type(123, User)) + .toThrowError('Expected an instance of MyUser, got 123!\n' + + ' - 123 is not instance of object with properties name, age\n' + + ' - 123 is not instance of Object'); + }); + + + it('should fail when an invalid property', function () { + expect(() => assert.type({name: 'Vojta', age: true}, User)) + .toThrowError('Expected an instance of MyUser, got {name: "Vojta", age: true}!\n' + + ' - {name: "Vojta", age: true} is not instance of object with properties name, age\n' + + ' - true is not instance of number'); + }); + }); +}); + + + +// ## Integrating with Traceur +// +// Manually calling `assert.type()` in your code is cumbersome. Most of the time, you'll want to +// have Traceur add the calls to `assert.type()` to your code based on type annotations. +// +// This has several advantages: +// - it's shorter and nicer, +// - you can easily ignore it when generating production code. +// +// You'll need to run Traceur with `--types=true --type-assertions=true --type-assertion-module="path/to/assert"`. +describe('Traceur', function() { + + describe('arguments', function() { + + function reverse(str: string) { + return str ? reverse(str.substring(1)) + str[0] : '' + } + + it('should pass', function() { + expect(reverse('angular')).toBe('ralugna'); + }); + + + it('should fail', function() { + expect(() => reverse(123)) + .toThrowError('Invalid arguments given!\n' + + ' - 1st argument has to be an instance of string, got 123'); + }); + }); + + + describe('return value', function() { + + function foo(bar): number { + return bar; + } + + it('should pass', function() { + expect(foo(123)).toBe(123); + }); + + + it('should fail', function() { + expect(() => foo('bar')) + .toThrowError('Expected to return an instance of number, got "bar"!'); + }); + }); + + + describe('variables', function() { + + it('should pass', function() { + var count:number = 1; + }); + + + it('should fail', function() { + expect(() => { + var count: number = true; + }).toThrowError('Expected an instance of number, got true!'); + }); + }); + + + describe('void', function() { + function foo(bar): void { + return bar; + } + + it('should pass when not defined', function() { + function nonReturn(): void {} + function returnNothing(): void { return; } + function returnUndefined(): void { return undefined; } + + foo(); + foo(undefined); + nonReturn(); + returnNothing(); + returnUndefined(); + }); + + + it('should fail when a value returned', function() { + expect(() => foo('bar')) + .toThrowError('Expected to return an instance of voidType, got "bar"!'); + }); + + + it('should fail when null returned', function() { + expect(() => foo(null)) + .toThrowError('Expected to return an instance of voidType, got null!'); + }); + }); +}); + + +//
+// This documentation was generated from [assert.spec.js](https://github.com/vojtajina/assert/blob/master/test/assert.spec.js) using [Docco](http://jashkenas.github.io/docco/). +//
diff --git a/test-main.js b/test-main.js index 536f040d2f..0ba9fe3b8e 100644 --- a/test-main.js +++ b/test-main.js @@ -7,6 +7,9 @@ Object.keys(window.__karma__.files).forEach(function(path) { .replace(/\/src\//, '/') .replace(/\/test\//, '/') .replace(/\.\w*$/, ''); - System.get(moduleName).main(); + var mod = System.get(moduleName); + if (mod.main) { + mod.main(); + } } });