diff options
Diffstat (limited to 'addon-sdk/source/lib/method')
-rw-r--r-- | addon-sdk/source/lib/method/.travis.yml | 5 | ||||
-rw-r--r-- | addon-sdk/source/lib/method/History.md | 55 | ||||
-rw-r--r-- | addon-sdk/source/lib/method/License.md | 18 | ||||
-rw-r--r-- | addon-sdk/source/lib/method/Readme.md | 117 | ||||
-rw-r--r-- | addon-sdk/source/lib/method/core.js | 225 | ||||
-rw-r--r-- | addon-sdk/source/lib/method/package.json | 41 | ||||
-rw-r--r-- | addon-sdk/source/lib/method/test/browser.js | 20 | ||||
-rw-r--r-- | addon-sdk/source/lib/method/test/common.js | 272 |
8 files changed, 753 insertions, 0 deletions
diff --git a/addon-sdk/source/lib/method/.travis.yml b/addon-sdk/source/lib/method/.travis.yml new file mode 100644 index 000000000..780731a47 --- /dev/null +++ b/addon-sdk/source/lib/method/.travis.yml @@ -0,0 +1,5 @@ +language: node_js +node_js: + - 0.4 + - 0.5 + - 0.6 diff --git a/addon-sdk/source/lib/method/History.md b/addon-sdk/source/lib/method/History.md new file mode 100644 index 000000000..95258c45f --- /dev/null +++ b/addon-sdk/source/lib/method/History.md @@ -0,0 +1,55 @@ +# Changes + +## 1.0.2 / 2012-12-26 + + - Delegate to polymorphic methods from `.define` and `.implement` so, they + can be overidden. + +## 1.0.1 / 2012-11-11 + + - Fix issues with different `Error` types as they all inherit from + `Error`. + +## 1.0.0 / 2012-11-09 + + - Add browser test integration. + - Fix cross-browser incompatibilities & test failures. + - Add support for host objects. + - Add optional `hint` argument for method to ease debugging. + - Remove default implementation at definition time. + +## 0.1.1 / 2012-10-15 + + - Fix regression causing custom type implementation to be stored on objects. + +## 0.1.0 / 2012-10-15 + + - Remove dependency on name module. + - Implement fallback for engines that do not support ES5. + - Add support for built-in type extensions without extending their prototypes. + - Make API for default definitions more intuitive. + Skipping type argument now defines default: + + isFoo.define(function(value) { + return false + }) + + - Make exposed `define` and `implement` polymorphic. + - Removed dev dependency on swank-js. + - Primitive types `string, number, boolean` no longer inherit method + implementations from `Object`. + +## 0.0.3 / 2012-07-17 + + - Remove module boilerplate + +## 0.0.2 / 2012-06-26 + + - Name changes to make it less conflicting with other library conventions. + - Expose function version of `define` & `implement` methods. + - Expose `Null` and `Undefined` object holding implementations for an + associated types. + +## 0.0.1 / 2012-06-25 + + - Initial release diff --git a/addon-sdk/source/lib/method/License.md b/addon-sdk/source/lib/method/License.md new file mode 100644 index 000000000..ed76489a3 --- /dev/null +++ b/addon-sdk/source/lib/method/License.md @@ -0,0 +1,18 @@ +Copyright 2012 Irakli Gozalishvili. All rights reserved. +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/addon-sdk/source/lib/method/Readme.md b/addon-sdk/source/lib/method/Readme.md new file mode 100644 index 000000000..9584c9160 --- /dev/null +++ b/addon-sdk/source/lib/method/Readme.md @@ -0,0 +1,117 @@ +# method + +[![Build Status](https://secure.travis-ci.org/Gozala/method.png)](http://travis-ci.org/Gozala/method) + +Library provides an API for defining polymorphic methods that dispatch on the +first argument type. This provides a powerful way for decouple abstraction +interface definition from an actual implementation per type, without risks +of interference with other libraries. + +### Motivation + + - Provide a high-performance, dynamic polymorphism construct as an + alternative to existing object methods that does not provides any + mechanics for guarding against name conflicts. + - Allow independent extension of types, and implementations of methods + on types, by different parties. + +## Install + + npm install method + +## Use + +```js +var method = require("method") + +// Define `isWatchable` method that can be implemented for any type. +var isWatchable = method("isWatchable") + +// If you call it on any object it will +// throw as nothing implements that method yet. +//isWatchable({}) // => Exception: method is not implemented + +// If you define private method on `Object.prototype` +// all objects will inherit it. +Object.prototype[isWatchable] = function() { + return false; +} + +isWatchable({}) // => false + + +// Although `isWatchable` property above will be enumerable and there for +// may damage some assumbtions made by other libraries. There for it"s +// recomended to use built-in helpers methods that will define extension +// without breaking assumbtions made by other libraries: + +isWatchable.define(Object, function() { return false }) + + +// There are primitive types in JS that won"t inherit methods from Object: +isWatchable(null) // => Exception: method is not implemented + +// One could either implement methods for such types: +isWatchable.define(null, function() { return false }) +isWatchable.define(undefined, function() { return false }) + +// Or simply define default implementation: +isWatchable.define(function() { return false }) + +// Alternatively default implementation may be provided at creation: +isWatchable = method(function() { return false }) + +// Method dispatches on an first argument type. That allows us to create +// new types with an alternative implementations: +function Watchable() {} +isWatchable.define(Watchable, function() { return true }) + +// This will make all `Watchable` instances watchable! +isWatchable(new Watchable()) // => true + +// Arbitrary objects can also be extended to implement given method. For example +// any object can simply made watchable: +function watchable(object) { + return isWatchable.implement(objct, function() { return true }) +} + +isWatchable(watchable({})) // => true + +// Full protocols can be defined with such methods: +var observers = "observers@" + module.filename +var watchers = method("watchers") +var watch = method("watch") +var unwatch = method("unwatch") + +watchers.define(Watchable, function(target) { + return target[observers] || (target[observers] = []) +}) + +watch.define(Watchable, function(target, watcher) { + var observers = watchers(target) + if (observers.indexOf(watcher) < 0) observers.push(watcher) + return target +}) +unwatch.define(Watchable, function(target, watcher) { + var observers = watchers(target) + var index = observers.indexOf(watcher) + if (observers.indexOf(watcher) >= 0) observers.unshift(watcher) + return target +}) + +// Define type Port that inherits form Watchable + +function Port() {} +Port.prototype = Object.create(Watchable.prototype) + +var emit = method("emit") +emit.define(Port, function(port, message) { + watchers(port).slice().forEach(function(watcher) { + watcher(message) + }) +}) + +var p = new Port() +watch(p, console.log) +emit(p, "hello world") // => info: "hello world" +``` diff --git a/addon-sdk/source/lib/method/core.js b/addon-sdk/source/lib/method/core.js new file mode 100644 index 000000000..a6a5261e6 --- /dev/null +++ b/addon-sdk/source/lib/method/core.js @@ -0,0 +1,225 @@ +"use strict"; + +var defineProperty = Object.defineProperty || function(object, name, property) { + object[name] = property.value + return object +} + +// Shortcut for `Object.prototype.toString` for faster access. +var typefy = Object.prototype.toString + +// Map to for jumping from typeof(value) to associated type prefix used +// as a hash in the map of builtin implementations. +var types = { "function": "Object", "object": "Object" } + +// Array is used to save method implementations for the host objects in order +// to avoid extending them with non-primitive values that could cause leaks. +var host = [] +// Hash map is used to save method implementations for builtin types in order +// to avoid extending their prototypes. This also allows to share method +// implementations for types across diff contexts / frames / compartments. +var builtin = {} + +function Primitive() {} +function ObjectType() {} +ObjectType.prototype = new Primitive() +function ErrorType() {} +ErrorType.prototype = new ObjectType() + +var Default = builtin.Default = Primitive.prototype +var Null = builtin.Null = new Primitive() +var Void = builtin.Void = new Primitive() +builtin.String = new Primitive() +builtin.Number = new Primitive() +builtin.Boolean = new Primitive() + +builtin.Object = ObjectType.prototype +builtin.Error = ErrorType.prototype + +builtin.EvalError = new ErrorType() +builtin.InternalError = new ErrorType() +builtin.RangeError = new ErrorType() +builtin.ReferenceError = new ErrorType() +builtin.StopIteration = new ErrorType() +builtin.SyntaxError = new ErrorType() +builtin.TypeError = new ErrorType() +builtin.URIError = new ErrorType() + + +function Method(hint) { + /** + Private Method is a callable private name that dispatches on the first + arguments same named Method: + + method(object, ...rest) => object[method](...rest) + + Optionally hint string may be provided that will be used in generated names + to ease debugging. + + ## Example + + var foo = Method() + + // Implementation for any types + foo.define(function(value, arg1, arg2) { + // ... + }) + + // Implementation for a specific type + foo.define(BarType, function(bar, arg1, arg2) { + // ... + }) + **/ + + // Create an internal unique name if `hint` is provided it is used to + // prefix name to ease debugging. + var name = (hint || "") + "#" + Math.random().toString(32).substr(2) + + function dispatch(value) { + // Method dispatches on type of the first argument. + // If first argument is `null` or `void` associated implementation is + // looked up in the `builtin` hash where implementations for built-ins + // are stored. + var type = null + var method = value === null ? Null[name] : + value === void(0) ? Void[name] : + // Otherwise attempt to use method with a generated private + // `name` that is supposedly in the prototype chain of the + // `target`. + value[name] || + // Otherwise assume it's one of the built-in type instances, + // in which case implementation is stored in a `builtin` hash. + // Attempt to find a implementation for the given built-in + // via constructor name and method name. + ((type = builtin[(value.constructor || "").name]) && + type[name]) || + // Otherwise assume it's a host object. For host objects + // actual method implementations are stored in the `host` + // array and only index for the implementation is stored + // in the host object's prototype chain. This avoids memory + // leaks that otherwise could happen when saving JS objects + // on host object. + host[value["!" + name] || void(0)] || + // Otherwise attempt to lookup implementation for builtins by + // a type of the value. This basically makes sure that all + // non primitive values will delegate to an `Object`. + ((type = builtin[types[typeof(value)]]) && type[name]) + + + // If method implementation for the type is still not found then + // just fallback for default implementation. + method = method || Default[name] + + + // If implementation is still not found (which also means there is no + // default) just throw an error with a descriptive message. + if (!method) throw TypeError("Type does not implements method: " + name) + + // If implementation was found then just delegate. + return method.apply(method, arguments) + } + + // Make `toString` of the dispatch return a private name, this enables + // method definition without sugar: + // + // var method = Method() + // object[method] = function() { /***/ } + dispatch.toString = function toString() { return name } + + // Copy utility methods for convenient API. + dispatch.implement = implementMethod + dispatch.define = defineMethod + + return dispatch +} + +// Create method shortcuts form functions. +var defineMethod = function defineMethod(Type, lambda) { + return define(this, Type, lambda) +} +var implementMethod = function implementMethod(object, lambda) { + return implement(this, object, lambda) +} + +// Define `implement` and `define` polymorphic methods to allow definitions +// and implementations through them. +var implement = Method("implement") +var define = Method("define") + + +function _implement(method, object, lambda) { + /** + Implements `Method` for the given `object` with a provided `implementation`. + Calling `Method` with `object` as a first argument will dispatch on provided + implementation. + **/ + return defineProperty(object, method.toString(), { + enumerable: false, + configurable: false, + writable: false, + value: lambda + }) +} + +function _define(method, Type, lambda) { + /** + Defines `Method` for the given `Type` with a provided `implementation`. + Calling `Method` with a first argument of this `Type` will dispatch on + provided `implementation`. If `Type` is a `Method` default implementation + is defined. If `Type` is a `null` or `undefined` `Method` is implemented + for that value type. + **/ + + // Attempt to guess a type via `Object.prototype.toString.call` hack. + var type = Type && typefy.call(Type.prototype) + + // If only two arguments are passed then `Type` is actually an implementation + // for a default type. + if (!lambda) Default[method] = Type + // If `Type` is `null` or `void` store implementation accordingly. + else if (Type === null) Null[method] = lambda + else if (Type === void(0)) Void[method] = lambda + // If `type` hack indicates built-in type and type has a name us it to + // store a implementation into associated hash. If hash for this type does + // not exists yet create one. + else if (type !== "[object Object]" && Type.name) { + var Bulitin = builtin[Type.name] || (builtin[Type.name] = new ObjectType()) + Bulitin[method] = lambda + } + // If `type` hack indicates an object, that may be either object or any + // JS defined "Class". If name of the constructor is `Object`, assume it's + // built-in `Object` and store implementation accordingly. + else if (Type.name === "Object") + builtin.Object[method] = lambda + // Host objects are pain!!! Every browser does some crazy stuff for them + // So far all browser seem to not implement `call` method for host object + // constructors. If that is a case here, assume it's a host object and + // store implementation in a `host` array and store `index` in the array + // in a `Type.prototype` itself. This avoids memory leaks that could be + // caused by storing JS objects on a host objects. + else if (Type.call === void(0)) { + var index = host.indexOf(lambda) + if (index < 0) index = host.push(lambda) - 1 + // Prefix private name with `!` so it can be dispatched from the method + // without type checks. + implement("!" + method, Type.prototype, index) + } + // If Got that far `Type` is user defined JS `Class`. Define private name + // as hidden property on it's prototype. + else + implement(method, Type.prototype, lambda) +} + +// And provided implementations for a polymorphic equivalents. +_define(define, _define) +_define(implement, _implement) + +// Define exports on `Method` as it's only thing being exported. +Method.implement = implement +Method.define = define +Method.Method = Method +Method.method = Method +Method.builtin = builtin +Method.host = host + +module.exports = Method diff --git a/addon-sdk/source/lib/method/package.json b/addon-sdk/source/lib/method/package.json new file mode 100644 index 000000000..7bb004e28 --- /dev/null +++ b/addon-sdk/source/lib/method/package.json @@ -0,0 +1,41 @@ +{ + "name": "method", + "id": "method", + "version": "1.0.2", + "description": "Functional polymorphic method dispatch", + "keywords": [ + "method", + "dispatch", + "protocol", + "polymorphism", + "type dispatch" + ], + "author": "Irakli Gozalishvili <rfobic@gmail.com> (http://jeditoolkit.com)", + "homepage": "https://github.com/Gozala/method", + "main": "./core.js", + "repository": { + "type": "git", + "url": "https://github.com/Gozala/method.git", + "web": "https://github.com/Gozala/method" + }, + "bugs": { + "url": "http://github.com/Gozala/method/issues/" + }, + "devDependencies": { + "test": "~0.x.0", + "repl-utils": "~2.0.1", + "phantomify": "~0.1.0" + }, + "scripts": { + "test": "npm run test-node && npm run test-browser", + "test-browser": "node ./node_modules/phantomify/bin/cmd.js ./test/browser.js", + "test-node": "node ./test/common.js", + "repl": "node node_modules/repl-utils" + }, + "licenses": [ + { + "type": "MIT", + "url": "https://github.com/Gozala/method/License.md" + } + ] +} diff --git a/addon-sdk/source/lib/method/test/browser.js b/addon-sdk/source/lib/method/test/browser.js new file mode 100644 index 000000000..7c8e6cd52 --- /dev/null +++ b/addon-sdk/source/lib/method/test/browser.js @@ -0,0 +1,20 @@ +"use strict"; + +exports["test common"] = require("./common") + +var Method = require("../core") + +exports["test host objects"] = function(assert) { + var isElement = Method("is-element") + isElement.define(function() { return false }) + + isElement.define(Element, function() { return true }) + + assert.notDeepEqual(typeof(Element.prototype[isElement]), "number", + "Host object's prototype is extended with a number value") + + assert.ok(!isElement({}), "object is not an Element") + assert.ok(document.createElement("div"), "Element is an element") +} + +require("test").run(exports) diff --git a/addon-sdk/source/lib/method/test/common.js b/addon-sdk/source/lib/method/test/common.js new file mode 100644 index 000000000..0418c3a23 --- /dev/null +++ b/addon-sdk/source/lib/method/test/common.js @@ -0,0 +1,272 @@ +"use strict"; + +var Method = require("../core") + +function type(value) { + return Object.prototype.toString.call(value). + split(" "). + pop(). + split("]"). + shift(). + toLowerCase() +} + +var values = [ + null, // 0 + undefined, // 1 + Infinity, // 2 + NaN, // 3 + 5, // 4 + {}, // 5 + Object.create({}), // 6 + Object.create(null), // 7 + [], // 8 + /foo/, // 9 + new Date(), // 10 + Function, // 11 + function() {}, // 12 + true, // 13 + false, // 14 + "string" // 15 +] + +function True() { return true } +function False() { return false } + +var trues = values.map(True) +var falses = values.map(False) + +exports["test throws if not implemented"] = function(assert) { + var method = Method("nope") + + assert.throws(function() { + method({}) + }, /not implement/i, "method throws if not implemented") + + assert.throws(function() { + method(null) + }, /not implement/i, "method throws on null") +} + +exports["test all types inherit from default"] = function(assert) { + var isImplemented = Method("isImplemented") + isImplemented.define(function() { return true }) + + values.forEach(function(value) { + assert.ok(isImplemented(value), + type(value) + " inherits deafult implementation") + }) +} + +exports["test default can be implemented later"] = function(assert) { + var isImplemented = Method("isImplemented") + isImplemented.define(function() { + return true + }) + + values.forEach(function(value) { + assert.ok(isImplemented(value), + type(value) + " inherits deafult implementation") + }) +} + +exports["test dispatch not-implemented"] = function(assert) { + var isDefault = Method("isDefault") + values.forEach(function(value) { + assert.throws(function() { + isDefault(value) + }, /not implement/, type(value) + " throws if not implemented") + }) +} + +exports["test dispatch default"] = function(assert) { + var isDefault = Method("isDefault") + + // Implement default + isDefault.define(True) + assert.deepEqual(values.map(isDefault), trues, + "all implementation inherit from default") + +} + +exports["test dispatch null"] = function(assert) { + var isNull = Method("isNull") + + // Implement default + isNull.define(False) + isNull.define(null, True) + assert.deepEqual(values.map(isNull), + [ true ]. + concat(falses.slice(1)), + "only null gets methods defined for null") +} + +exports["test dispatch undefined"] = function(assert) { + var isUndefined = Method("isUndefined") + + // Implement default + isUndefined.define(False) + isUndefined.define(undefined, True) + assert.deepEqual(values.map(isUndefined), + [ false, true ]. + concat(falses.slice(2)), + "only undefined gets methods defined for undefined") +} + +exports["test dispatch object"] = function(assert) { + var isObject = Method("isObject") + + // Implement default + isObject.define(False) + isObject.define(Object, True) + assert.deepEqual(values.map(isObject), + [ false, false, false, false, false ]. + concat(trues.slice(5, 13)). + concat([false, false, false]), + "all values except primitives inherit Object methods") + +} + +exports["test dispatch number"] = function(assert) { + var isNumber = Method("isNumber") + isNumber.define(False) + isNumber.define(Number, True) + + assert.deepEqual(values.map(isNumber), + falses.slice(0, 2). + concat(true, true, true). + concat(falses.slice(5)), + "all numbers inherit from Number method") +} + +exports["test dispatch string"] = function(assert) { + var isString = Method("isString") + isString.define(False) + isString.define(String, True) + + assert.deepEqual(values.map(isString), + falses.slice(0, 15). + concat(true), + "all strings inherit from String method") +} + +exports["test dispatch function"] = function(assert) { + var isFunction = Method("isFunction") + isFunction.define(False) + isFunction.define(Function, True) + + assert.deepEqual(values.map(isFunction), + falses.slice(0, 11). + concat(true, true). + concat(falses.slice(13)), + "all functions inherit from Function method") +} + +exports["test dispatch date"] = function(assert) { + var isDate = Method("isDate") + isDate.define(False) + isDate.define(Date, True) + + assert.deepEqual(values.map(isDate), + falses.slice(0, 10). + concat(true). + concat(falses.slice(11)), + "all dates inherit from Date method") +} + +exports["test dispatch RegExp"] = function(assert) { + var isRegExp = Method("isRegExp") + isRegExp.define(False) + isRegExp.define(RegExp, True) + + assert.deepEqual(values.map(isRegExp), + falses.slice(0, 9). + concat(true). + concat(falses.slice(10)), + "all regexps inherit from RegExp method") +} + +exports["test redefine for descendant"] = function(assert) { + var isFoo = Method("isFoo") + var ancestor = {} + isFoo.implement(ancestor, function() { return true }) + var descendant = Object.create(ancestor) + isFoo.implement(descendant, function() { return false }) + + assert.ok(isFoo(ancestor), "defined on ancestor") + assert.ok(!isFoo(descendant), "overrided for descendant") +} + +exports["test on custom types"] = function(assert) { + function Bar() {} + var isBar = Method("isBar") + + isBar.define(function() { return false }) + isBar.define(Bar, function() { return true }) + + assert.ok(!isBar({}), "object is get's default implementation") + assert.ok(isBar(new Bar()), "Foo type objects get own implementation") + + var isObject = Method("isObject") + isObject.define(function() { return false }) + isObject.define(Object, function() { return true }) + + assert.ok(isObject(new Bar()), "foo inherits implementation from object") + + + isObject.define(Bar, function() { return false }) + + assert.ok(!isObject(new Bar()), + "implementation inherited form object can be overrided") +} + + +exports["test error types"] = function(assert) { + var isError = Method("isError") + isError.define(function() { return false }) + isError.define(Error, function() { return true }) + + assert.ok(isError(Error("boom")), "error is error") + assert.ok(isError(TypeError("boom")), "type error is an error") + assert.ok(isError(EvalError("boom")), "eval error is an error") + assert.ok(isError(RangeError("boom")), "range error is an error") + assert.ok(isError(ReferenceError("boom")), "reference error is an error") + assert.ok(isError(SyntaxError("boom")), "syntax error is an error") + assert.ok(isError(URIError("boom")), "URI error is an error") +} + +exports["test override define polymorphic method"] = function(assert) { + var define = Method.define + var implement = Method.implement + + var fn = Method("fn") + var methods = {} + implement(define, fn, function(method, label, implementation) { + methods[label] = implementation + }) + + function foo() {} + + define(fn, "foo-case", foo) + + assert.equal(methods["foo-case"], foo, "define set property") +} + +exports["test override define via method API"] = function(assert) { + var define = Method.define + var implement = Method.implement + + var fn = Method("fn") + var methods = {} + define.implement(fn, function(method, label, implementation) { + methods[label] = implementation + }) + + function foo() {} + + define(fn, "foo-case", foo) + + assert.equal(methods["foo-case"], foo, "define set property") +} + +require("test").run(exports) |