├── .travis.yml ├── tests ├── test-all.js └── basic.js ├── examples ├── event-emitter.js ├── event-dom.js ├── installable.js ├── event-protocol.js └── event-object.js ├── package.json ├── History.md ├── Readme.md └── core.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.4 4 | - 0.6 5 | -------------------------------------------------------------------------------- /tests/test-all.js: -------------------------------------------------------------------------------- 1 | /* vim:set ts=2 sw=2 sts=2 expandtab */ 2 | /*jshint asi: true newcap: true undef: true es5: true node: true devel: true 3 | forin: true */ 4 | /*global define: true */ 5 | 6 | (typeof define === "undefined" ? function ($) { $(require, exports, module) } : define)(function (require, exports, module, undefined) { 7 | 8 | "use strict"; 9 | 10 | exports['test basic'] = require('./basic') 11 | 12 | if (module == require.main) 13 | require("test").run(exports); 14 | 15 | }) 16 | -------------------------------------------------------------------------------- /examples/event-emitter.js: -------------------------------------------------------------------------------- 1 | /*jshint asi:true */ 2 | // module: ./event-emitter 3 | 4 | var EventProtocol = require('./event-protocol') 5 | var EventEmitter = require('events').EventEmitter 6 | 7 | EventProtocol(EventEmitter, { 8 | on: function(target, type, listener, capture) { 9 | target.on(type, listener) 10 | }, 11 | once: function(target, type, listener, capture) { 12 | target.once(type, listener) 13 | }, 14 | off: function(target, type, listener, capture) { 15 | target.removeListener(target, type) 16 | }, 17 | emit: function(target, type, event, capture) { 18 | target.emit(type, event) 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /examples/event-dom.js: -------------------------------------------------------------------------------- 1 | /*jshint asi:true latedef: true */ 2 | // module: ./event-dom 3 | 4 | var Event = require('./event-protocol') 5 | 6 | Event(Element, { 7 | on: function(target, type, listener, capture) { 8 | target.addEventListener(type, listener, capture) 9 | }, 10 | off: function(target, type, listener, capture) { 11 | target.removeListener(type, listener, capture) 12 | }, 13 | emit: function(target, type, option, capture) { 14 | // Note: This is simplified implementation for demo purposes. 15 | var document = target.ownerDocument 16 | var event = document.createEvent('UIEvents') 17 | event.initUIEvent(type, option.bubbles, option.cancellable, 18 | document.defaultView, 1) 19 | event.data = option.data 20 | target.dispatchEvent(event) 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /examples/installable.js: -------------------------------------------------------------------------------- 1 | /*jshint asi:true latedef: true */ 2 | // module: ./installable 3 | 4 | // Protocol for working with installable application components. 5 | var Installable = protocol({ 6 | // Installs given `component` implementing this protocol. Takes optional 7 | // configuration options. 8 | install: [ protocol, [ 'options:Object' ] ], 9 | // Uninstall given `component` implementing this protocol. 10 | uninstall: [ protocol ], 11 | // Activate given `component` implementing this protocol. 12 | on: [ protocol ], 13 | // Disable given `component` implementing this protocol. 14 | off: [ protocol ] 15 | }) 16 | 17 | Installable(Object, { 18 | install: function(component, options) { 19 | // Implementation details... 20 | }, 21 | uninstall: function(component, options) { 22 | // Implementation details... 23 | }, 24 | on: function(component) { 25 | component.enabled = true 26 | }, 27 | off: function(component) { 28 | component.enabled = false 29 | } 30 | }) 31 | 32 | module.exports = Installable 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "protocol", 3 | "id": "protocol", 4 | "version": "0.4.0", 5 | "description": "Protocol based polymorphism for javascript.", 6 | "keywords": [ "polymorphism", "protocol", "cotract" ], 7 | "author": "Irakli Gozalishvili (http://jeditoolkit.com)", 8 | "homepage": "https://github.com/Gozala/protocol", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/Gozala/protocol.git", 12 | "web": "https://github.com/Gozala/protocol" 13 | }, 14 | "bugs": { 15 | "url": "http://github.com/Gozala/protocol/issues/" 16 | }, 17 | "devDependencies": { 18 | "test": ">=0.0.10", 19 | "repl-utils": ">=0.0.1", 20 | "swank-js": ">=0.0.1" 21 | }, 22 | "engines": { 23 | "node": ">=0.4.x" 24 | }, 25 | "main": "./core.js", 26 | "scripts": { 27 | "test": "node tests/test-all.js", 28 | "repl": "node ./node_modules/repl-utils", 29 | "swank": "node ./node_modules/swank-js" 30 | }, 31 | "licenses": [{ 32 | "type" : "MIT", 33 | "url" : "http://jeditoolkit.com/LICENSE" 34 | }] 35 | } 36 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## 0.4.0 / 2012-05-17 4 | 5 | - Make difference between Object and Default implementations. 6 | - Remove support for inline implementations. 7 | 8 | ## 0.3.1 / 2012-04-10 9 | 10 | - Actually stop mutating built-ins. 11 | - Typo fixes. 12 | 13 | ## 0.3.0 / 2012-04-10 14 | 15 | - Stop mutating builtins. 16 | - Remove alias names for `protocol` function. 17 | - Add API for in-line protocol implementations. 18 | - Add more tests. 19 | 20 | ## 0.2.4 / 2012-03-21 21 | 22 | - Make possible to implement protocol for a type even if ancestors already 23 | implemented it. 24 | - Use shorter names for protocol properties. 25 | - Provide some code examples. 26 | 27 | ## 0.2.3 / 2012-02-23 28 | 29 | - Add convenience syntax for extending multiple types in a same call. 30 | 31 | ## 0.2.2 / 2012-02-22 32 | 33 | - Fix incorrect behavior when extending `protocol.Object`. 34 | 35 | ## 0.2.1 / 2012-02-21 36 | 37 | - Support for multi-globals by exposing `protocol.Object`, `protocol.String`, 38 | etc that apply to all built-ins regardless of scope they're coming from. 39 | - Swap `'this'` with `protocol` in the signature definitions. 40 | 41 | ## 0.2.0 / 2012-02-05 42 | 43 | - Moving protocols to a type based polymorphism from an argument based method 44 | dispatch. 45 | 46 | ## 0.0.1 / 2011-10-10 47 | 48 | - Initial release 49 | -------------------------------------------------------------------------------- /examples/event-protocol.js: -------------------------------------------------------------------------------- 1 | /*jshint asi:true */ 2 | // module: ./event-protocol 3 | 4 | var protocol = require('protocol/core').protocol 5 | 6 | // Defining a protocol for working with an event listeners / emitters. 7 | module.exports = protocol({ 8 | // Function on takes event `target` object implementing 9 | // `Event` protocol as first argument, event `type` string 10 | // as second argument and `listener` function as a third 11 | // argument. Optionally forth boolean argument can be 12 | // specified to use a capture. Function allows registration 13 | // of event `listeners` on the event `target` for the given 14 | // event `type`. 15 | on: [ protocol, String, Function, [ Boolean ] ], 16 | 17 | // Function allows registration of single shot event `listener` 18 | // on the event `target` of the given event `type`. 19 | once: [ protocol, 'type', 'listener', [ 'capture=false' ] ], 20 | 21 | // Unregisters event `listener` of the given `type` from the given 22 | // event `target` (implementing this protocol) with a given `capture` 23 | // face. Optional `capture` argument falls back to `false`. 24 | off: [ protocol, 'type', 'listener', [ 'capture=false'] ], 25 | 26 | // Emits given `event` for the listeners of the given event `type` 27 | // of the given event `target` (implementing this protocol) with a given 28 | // `capture` face. Optional `capture` argument falls back to `false`. 29 | emit: [ protocol, 'type', 'event', [ 'capture=false' ] ] 30 | }) 31 | -------------------------------------------------------------------------------- /examples/event-object.js: -------------------------------------------------------------------------------- 1 | /*jshint asi:true */ 2 | // module: ./event-object 3 | 4 | var Event = require('./event-protocol'), on = Event.on 5 | 6 | // Weak registry of listener maps associated 7 | // to event targets. 8 | var map = WeakMap() 9 | 10 | // Returns listeners of the given event `target` 11 | // for the given `type` with a given `capture` face. 12 | function getListeners(target, type, capture) { 13 | // If there is no listeners map associated with 14 | // this target then create one. 15 | if (!map.has(target)) map.set(target, Object.create(null)) 16 | 17 | var listeners = map.get(target) 18 | // prefix event type with a capture face flag. 19 | var address = (capture ? '!' : '-') + type 20 | // If there is no listeners array for the given type & capture 21 | // face than create one and return. 22 | return listeners[address] || (listeners[address] = []) 23 | } 24 | 25 | Event(Object, { 26 | on: function(target, type, listener, capture) { 27 | var listeners = getListeners(target, type, capture) 28 | // Add listener if not registered yet. 29 | if (!~listeners.indexOf(listener)) listeners.push(listener) 30 | }, 31 | once: function(target, type, listener, capture) { 32 | on(target, type, listener, capture) 33 | on(target, type, function cleanup() { 34 | off(target, type, listener, capture) 35 | }, capture) 36 | }, 37 | off: function(target, type, listener, capture) { 38 | var listeners = getListeners(target, type, capture) 39 | var index = listeners.indexOf(listener) 40 | // Remove listener if registered. 41 | if (~index) listeners.splice(index, 1) 42 | }, 43 | emit: function(target, type, event, capture) { 44 | var listeners = getListeners(target, type, capture).slice() 45 | // TODO: Exception handling 46 | while (listeners.length) listeners.shift().call(target, event) 47 | } 48 | }) 49 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Protocol 2 | 3 | [![build status](https://secure.travis-ci.org/Gozala/protocol.png)](http://travis-ci.org/Gozala/protocol) 4 | 5 | Protocol JS library is inspired by idea of [clojure protocols]. Protocols 6 | provide a powerful way for decoupling abstraction interface definition from 7 | an actual implementation per type, without risks of interference with other 8 | libraries. 9 | 10 | There are several motivations for protocols: 11 | 12 | - Provide a high-performance, dynamic polymorphism construct as an alternative 13 | to existing object inheritance that does not provides any mechanics for 14 | guarding against name conflicts. 15 | - Provide the best parts of interfaces: 16 | - specification only, no implementation 17 | - a single type can implement multiple protocols 18 | - Protocols allow independent extension of the set of types, protocols, and 19 | implementations of protocols on types, by different parties. 20 | 21 | ## Basics 22 | 23 | A protocol is a named set of function signatures: 24 | 25 | ```js 26 | var protocol = require('protocol/core').protocol 27 | 28 | var Sequential = protocol({ 29 | first: ('Returns first item of this sequence', [ protocol ]), 30 | rest: ('Returns sequence of items after the first', [ protocol ]), 31 | join: ('Returns sequence of items where head is first, and this is rest', [ Object, protocol ]) 32 | }) 33 | ``` 34 | 35 | - No implementations are provided 36 | - Docs can be optionally specified for the protocol and the functions, via 37 | elegant JS hack. 38 | - The above yields a set of polymorphic functions and a protocol object 39 | - The resulting functions dispatch on the type of their `protocol` argument, 40 | and thus must have it in the list of arguments. 41 | 42 | `protocol` will generate an interface containing a corresponding functions. 43 | returned interface may be used to extend data types with it's implementations: 44 | 45 | ```js 46 | Sequential(Array, { 47 | first: function(array) { return array[0] || null }, 48 | rest: function(array) { return Array.prototype.slice.call(array, 1) }, 49 | join: function(item, array) { 50 | return Array.prototype.concat.call([ item ], array) 51 | } 52 | }) 53 | ``` 54 | 55 | Once protocol is implemented for a given type it can be used with a given data 56 | types: 57 | 58 | ```js 59 | Sequential.first([ 1, 2, 3 ]) // => 1 60 | Sequential.rest([ 1, 2, 3 ]) // => [ 2, 3 ] 61 | 62 | Sequential.first('hello') // TypeError: Protocol not implemented: first 63 | ``` 64 | 65 | Protocol may be implemented for any other data types by any other party: 66 | 67 | ```js 68 | Sequential(String, { 69 | first: function(string) { return string[0] || null }, 70 | rest: function(string) { return String.prototype.substr.call(string, 1) }, 71 | join: function(item, string) { return item + string } 72 | }) 73 | 74 | Sequential.first('hello') // => 'h' 75 | ``` 76 | 77 | Protocol implementation may be provided to all the data types by ommiting 78 | type argument: 79 | 80 | ```js 81 | Sequential({ 82 | first: function(_) { return _ }, 83 | rest: function(_) { return null } 84 | }) 85 | 86 | Sequential.head(5) // => 5 87 | Sequential.tail(3) // => null 88 | ``` 89 | 90 | Since protocol implementations are decoupled from the actual protocol 91 | definition there maybe multiple implementations, but user will be in charge of 92 | deciding which one to pull in. 93 | 94 | ## Argument pattern based dispatch 95 | 96 | This library previously was doing argument pattern based method dispatch. 97 | If you are looking into something more in that line, check out [dispatcher] 98 | library that was forked from protocol to explore that direction. 99 | 100 | [dispatcher]:https://github.com/Gozala/dispatcher/ "Argument patter based dispatch" 101 | [clojure protocols]:http://clojure.org/protocols "Clojure protocols" 102 | -------------------------------------------------------------------------------- /tests/basic.js: -------------------------------------------------------------------------------- 1 | /* vim:set ts=2 sw=2 sts=2 expandtab */ 2 | /*jshint asi: true undef: true es5: true node: true devel: true 3 | forin: true */ 4 | /*global define: true */ 5 | 6 | !(typeof(define) !== "function" ? function($){ $(typeof(require) !== 'function' ? (function() { throw Error('require unsupported'); }) : require, typeof(exports) === 'undefined' ? this : exports); } : define)(function(require, exports) { 7 | 8 | 'use strict'; 9 | 10 | var protocol = require('../core').protocol 11 | function Arguments() { return arguments } 12 | Arguments.prototype = Object.getPrototypeOf(Arguments()) 13 | 14 | exports['test basics'] = function(assert) { 15 | var sequence = protocol({ 16 | first: [ protocol ], 17 | rest: [ protocol ], 18 | join: [ Object, protocol ] 19 | }) 20 | 21 | sequence(String, { 22 | first: function(string) { return string[0] || null }, 23 | rest: function(string) { return String.prototype.substr.call(string, 1) }, 24 | join: function(item, string) { return item + string } 25 | }) 26 | sequence(Array, { 27 | first: function(array) { return array[0] || null }, 28 | rest: function(array) { return Array.prototype.slice.call(array, 1) }, 29 | join: function(item, array) { 30 | return Array.prototype.concat.call([ item ], array) 31 | } 32 | }) 33 | 34 | assert.equal(sequence.first([ 1, 2, 3 ]), 1, 'first works on array') 35 | assert.deepEqual(sequence.rest([ 1, 2, 3]), [ 2, 3 ], 'rest works on array') 36 | assert.deepEqual(sequence.join(1, [ 2, 3 ]), [ 1, 2, 3 ], 'join works on array') 37 | 38 | assert.equal(sequence.first('hello'), 'h', 'first works on strings') 39 | assert.equal(sequence.rest('hello'), 'ello', 'rest works on strings') 40 | assert.equal(sequence.join('h', 'ello'), 'hello', 'join works on strings') 41 | } 42 | 43 | exports['test grouped extensions'] = function(assert) { 44 | var Sequence = protocol({ 45 | head: [ protocol ], 46 | tail: [ protocol ] 47 | }) 48 | 49 | Sequence(String, Array, Arguments, { 50 | head: function head(value) { return value[0] } 51 | }) 52 | Sequence(Array, Arguments, { 53 | tail: function(value) { return Array.prototype.slice.call(value, 1) } 54 | }) 55 | Sequence(String, { 56 | tail: function(value) { return value.substr(1) } 57 | }) 58 | 59 | assert.equal(Sequence.head([ 1, 2, 3 ]), 1, 'first works on array') 60 | assert.deepEqual(Sequence.tail([ 1, 2, 3]), [ 2, 3 ], 'rest works on array') 61 | assert.equal(Sequence.head('hello'), 'h', 'first works on strings') 62 | assert.equal(Sequence.tail('hello'), 'ello', 'rest works on strings') 63 | } 64 | 65 | exports['test default & type specific implementations'] = function(assert) { 66 | var Sequence = protocol({ 67 | head: [ protocol ], 68 | tail: [ protocol ] 69 | }) 70 | 71 | Sequence({ 72 | head: function(object) { return object }, 73 | tail: function(object) { return null } 74 | }) 75 | 76 | ;[ {}, [], null, undefined, true, 3, /a/, 'b' ].forEach(function($) { 77 | assert.equal(Sequence.head($), $, 78 | 'Object protocol is implemented by:' + typeof($)) 79 | assert.equal(Sequence.tail($), null, 80 | 'Object protocol is implemented by:' + typeof($)) 81 | }) 82 | 83 | function Stream(head, tail) { 84 | var stream = Object.create(Stream.prototype) 85 | stream.head = head 86 | stream.tail = tail 87 | return stream 88 | } 89 | Sequence(Stream, { 90 | tail: function(stream) { return stream.tail }, 91 | }) 92 | 93 | var a = Stream(1, null), b = Stream(2, a) 94 | 95 | assert.equal(Sequence.head(a), a, 'Stream inherits implementation of head') 96 | assert.equal(Sequence.tail(b), a, 'Stream has own implementation of tail') 97 | 98 | } 99 | 100 | exports['test that globals are not mutated'] = function(assert) { 101 | var prototypeNames = Object.getOwnPropertyNames(Object.prototype) 102 | var functionNames = Object.getOwnPropertyNames(Object) 103 | 104 | var Foo = protocol({ 105 | isFoo: [ protocol ] 106 | }) 107 | Foo(Object, { isFoo: function(object) { return false } }) 108 | 109 | assert.deepEqual(Object.getOwnPropertyNames(Object), 110 | functionNames, 'no properties were added to function') 111 | assert.deepEqual(Object.getOwnPropertyNames(Object.prototype), 112 | prototypeNames, 'no properties were added to prototype') 113 | } 114 | 115 | if (module == require.main) 116 | require('test').run(exports); 117 | 118 | }) 119 | -------------------------------------------------------------------------------- /core.js: -------------------------------------------------------------------------------- 1 | /*jshint asi:true latedef: true */ 2 | 3 | !(typeof(define) !== "function" ? function($){ $(typeof(require) !== 'function' ? (function() { throw Error('require unsupported'); }) : require, typeof(exports) === 'undefined' ? this : exports); } : define)(function(require, exports) { 4 | 5 | 'use strict'; 6 | 7 | var unbind = Function.call.bind(Function.bind, Function.call) 8 | var slice = unbind(Array.prototype.slice) 9 | var owns = unbind(Object.prototype.hasOwnProperty) 10 | var stringify = unbind(Object.prototype.toString) 11 | 12 | var ERROR_IMPLEMENTS = 'Type already implements this protocol: ' 13 | var ERROR_DOES_NOT_IMPLEMENTS = 'Protocol is not implemented: ' 14 | 15 | function Name(name) { 16 | /** 17 | Generating unique property names. 18 | **/ 19 | return ':' + (name || '') + ':' + Math.random().toString(36).slice(2) 20 | } 21 | 22 | function typeOf(value) { 23 | /** 24 | Normalized version of `typeof`. 25 | **/ 26 | var type, prototype 27 | 28 | if (value === null) return 'Null' 29 | if (value === undefined) return 'Undefined' 30 | type = stringify(value).split(' ')[1].split(']')[0] 31 | if (type !== 'Object') return type 32 | prototype = Object.getPrototypeOf(value) 33 | type = stringify(prototype).split(' ')[1].split(']')[0] 34 | return type === 'Object' && Object.getPrototypeOf(prototype) ? 'Type' : type 35 | } 36 | 37 | var Default = {} 38 | var types = { 39 | 'Arguments': {}, 40 | 'Array': {}, 41 | 'String': {}, 42 | 'Number': {}, 43 | 'Boolean': {}, 44 | 'RegExp': {}, 45 | 'Date': {}, 46 | 'Function': {}, 47 | 'Object': {}, 48 | 'Undefined': {}, 49 | 'Null': {}, 50 | 'Default': Default 51 | } 52 | exports.types = types 53 | 54 | function protocol(signature) { 55 | /** 56 | Defines new protocol that may be implemented for different types. 57 | 58 | ## Example 59 | 60 | var sequence = protocol.define(('Logical list abstraction', { 61 | first: ('Returns the first item in the sequence', [ protocol ]), 62 | rest: ('Returns a sequence of the items after the first', [ protocol ]), 63 | stick: ('Returns a new sequence where item is first, and this is rest', [ Object, protocol ]) 64 | })) 65 | 66 | **/ 67 | function Protocol(type, methods) { 68 | /** 69 | Extends this protocol by implementing it for the given `type`. 70 | 71 | ## Example 72 | 73 | sequence(String, { 74 | first: function(string) { return string[0] || null }, 75 | rest: function(string) { return String.prototype.substr.call(string, 1) }, 76 | stick: function(item, string) { return item + string } 77 | }) 78 | sequence(Array, { 79 | first: function(array) { return array[0] || null }, 80 | rest: function(array) { return Array.prototype.slice.call(array, 1) }, 81 | stick: function(item, array) { 82 | return Array.prototype.concat.call([ item ], array) 83 | } 84 | }) 85 | **/ 86 | var types = slice(arguments) 87 | methods = types.pop() 88 | if (!types.length) extend(Protocol, Default, methods) 89 | else while (types.length) extend(Protocol, types.shift(), methods) 90 | return type 91 | } 92 | 93 | Protocol.signature = signature 94 | var descriptor = {} 95 | Object.keys(signature).forEach(function(key) { 96 | function method() { 97 | var index = method[':this-index'] 98 | var name = method[':name'] 99 | var target = arguments[index] 100 | var type = typeOf(target) 101 | var f = ( 102 | (target && target[name]) || // By instance 103 | (type in types && types[type][name]) || // By type 104 | (type === 'Type' && types.Object[name]) || // By ancestor 105 | (types.Default[name])) // Default 106 | 107 | if (!f) throw TypeError(ERROR_DOES_NOT_IMPLEMENTS + key) 108 | return f.apply(f, arguments) 109 | } 110 | method[':this-index'] = signature[key].indexOf(protocol) 111 | method[':name'] = Name(key) 112 | descriptor[key] = { value: method, enumerable: true } 113 | }) 114 | 115 | return Object.defineProperties(Protocol, descriptor) 116 | } 117 | exports.protocol = protocol 118 | 119 | function extend(protocol, type, implementation) { 120 | var descriptor = {} 121 | if (typeof(type) === 'function') type = type.prototype 122 | type = types[typeOf(type && Object.create(type))] || type 123 | 124 | Object.keys(implementation).forEach(function(key, name) { 125 | if (key in protocol) { 126 | name = protocol[key][':name'] 127 | if (owns(type, name)) throw Error(ERROR_IMPLEMENTS + key) 128 | descriptor[name] = { 129 | value: implementation[key], 130 | enumerable: false, 131 | configurable: false, 132 | writable: false 133 | } 134 | } 135 | }) 136 | 137 | Object.defineProperties(type, descriptor) 138 | return type 139 | } 140 | exports.extend = extend 141 | 142 | }); 143 | --------------------------------------------------------------------------------