'use strict' const genfun = require('genfun') class Duck extends Function { // Duck.impl(Foo, [String, Array], { frob (str, arr) { ... }}) impl (target, types, impls) { if (!impls && !isArray(types)) { impls = types types = [] } if (!impls && this.isDerivable) { impls = this._defaultImpls } if (!impls) { impls = {} } if (typeof target === 'function' && !target.isGenfun) { target = target.prototype } checkImpls(this, target, impls) checkArgTypes(this, types) this._constraints.forEach(c => { if (!c.verify(target, types)) { throw new Error(`Implementations of ${ this.name || 'this protocol' } must first implement ${ c.parent.name || 'its constraint protocols defined in opts.where.' }`) } }) this._methodNames.forEach(name => { defineMethod(this, name, target, types, impls) }) } hasImpl (arg, args) { args = args || [] const fns = this._methodNames var gf if (typeof arg === 'function' && !arg.isGenfun) { arg = arg.prototype } args = args.map(arg => { if (typeof arg === 'function' && !arg.isGenfun) { return arg.prototype } else { return arg } }) for (var i = 0; i < fns.length; i++) { gf = arg[fns[i]] if (!gf || (gf.hasMethod ? !gf.hasMethod.apply(gf, args) : typeof gf === 'function')) { return false } } return true } // MyDuck.matches('a', ['this', 'c']) matches (thisType, argTypes) { if (!argTypes && isArray(thisType)) { argTypes = thisType thisType = 'this' } if (!thisType) { thisType = 'this' } if (!argTypes) { argTypes = [] } return new Constraint(this, thisType, argTypes) } } Duck.prototype.isDuck = true Duck.prototype.isProtocol = true const Protoduck = module.exports = define(['duck'], { createGenfun: ['duck', _metaCreateGenfun], addMethod: ['duck', _metaAddMethod] }, {name: 'Protoduck'}) const noImplFound = module.exports.noImplFound = genfun.noApplicableMethod module.exports.define = define function define (types, spec, opts) { if (!isArray(types)) { // protocol(spec, opts?) syntax for method-based protocols opts = spec spec = types types = [] } const duck = function (thisType, argTypes) { return duck.matches(thisType, argTypes) } Object.setPrototypeOf(duck, Duck.prototype) duck.isDerivable = true Object.defineProperty(duck, 'name', { value: (opts && opts.name) || 'Protocol' }) if (opts && opts.where) { let where = opts.where if (!isArray(opts.where)) { where = [opts.where] } duck._constraints = where.map(w => w.isProtocol // `where: [Foo]` ? w.matches() : w ) } else { duck._constraints = [] } duck.isProtocol = true duck._metaobject = opts && opts.metaobject duck._types = types duck._defaultImpls = {} duck._gfTypes = {} duck._methodNames = Object.keys(spec) duck._methodNames.forEach(name => { checkMethodSpec(duck, name, spec) }) duck._constraints.forEach(c => c.attach(duck)) return duck } function checkMethodSpec (duck, name, spec) { let gfTypes = spec[name] if (typeof gfTypes === 'function') { duck._defaultImpls[name] = gfTypes gfTypes = [gfTypes] } if (typeof gfTypes[gfTypes.length - 1] === 'function') { duck._defaultImpls[name] = gfTypes.pop() } else { duck.isDerivable = false } duck._gfTypes[name] = gfTypes.map(typeId => { const idx = duck._types.indexOf(typeId) if (idx === -1) { throw new Error( `type '${ typeId }' for function '${ name }' does not match any protocol types (${ duck._types.join(', ') }).` ) } else { return idx } }) } function defineMethod (duck, name, target, types, impls) { const methodTypes = duck._gfTypes[name].map(function (typeIdx) { return types[typeIdx] }) for (let i = methodTypes.length - 1; i >= 0; i--) { if (methodTypes[i] === undefined) { methodTypes.pop() } else { break } } const useMetaobject = duck._metaobject && duck._metaobject !== Protoduck // `target` does not necessarily inherit from `Object` if (!Object.prototype.hasOwnProperty.call(target, name)) { // Make a genfun if there's nothing there const gf = useMetaobject ? duck._metaobject.createGenfun(duck, target, name, null) : _metaCreateGenfun(duck, target, name, null) target[name] = gf } else if (typeof target[name] === 'function' && !target[name].isGenfun) { // Turn non-gf functions into genfuns const gf = useMetaobject ? duck._metaobject.createGenfun(duck, target, name, target[name]) : _metaCreateGenfun(duck, target, name, target[name]) target[name] = gf } const fn = impls[name] || duck._defaultImpls[name] if (fn) { // checkImpls made sure this is safe useMetaobject ? duck._metaobject.addMethod(duck, target, name, methodTypes, fn) : _metaAddMethod(duck, target, name, methodTypes, fn) } } function checkImpls (duck, target, impls) { duck._methodNames.forEach(function (name) { if ( !impls[name] && !duck._defaultImpls[name] && // Existing methods on the target are acceptable defaults. typeof target[name] !== 'function' ) { throw new Error(`Missing implementation for ${ formatMethod(duck, name, duck.name) }. Make sure the method is present in your ${ duck.name || 'protocol' } definition. Required methods: ${ duck._methodNames.filter(m => { return !duck._defaultImpls[m] }).map(m => formatMethod(duck, m)).join(', ') }.`) } }) Object.keys(impls).forEach(function (name) { if (duck._methodNames.indexOf(name) === -1) { throw new Error( `${name}() was included in the impl, but is not part of ${ duck.name || 'the protocol' }. Allowed methods: ${ duck._methodNames.map(m => formatMethod(duck, m)).join(', ') }.` ) } }) } function formatMethod (duck, name, withDuckName) { return `${ withDuckName && duck.name ? `${duck.name}#` : '' }${name}(${duck._gfTypes[name].map(n => duck._types[n]).join(', ')})` } function checkArgTypes (duck, types) { var requiredTypes = duck._types if (types.length > requiredTypes.length) { throw new Error( `${ duck.name || 'Protocol' } expects to be defined across ${ requiredTypes.length } type${requiredTypes.length > 1 ? 's' : ''}, but ${ types.length } ${types.length > 1 ? 'were' : 'was'} specified.` ) } } function typeName (obj) { return (/\[object ([a-zA-Z0-9]+)\]/).exec(({}).toString.call(obj))[1] } function installMethodErrorMessage (proto, gf, target, name) { noImplFound.add([gf], function (gf, thisArg, args) { let parent = Object.getPrototypeOf(thisArg) while (parent && parent[name] === gf) { parent = Object.getPrototypeOf(parent) } if (parent && parent[name] && typeof parent[name] === 'function') { } var msg = `No ${typeName(thisArg)} impl for ${ proto.name ? `${proto.name}#` : '' }${name}(${[].map.call(args, typeName).join(', ')}). You must implement ${ proto.name ? formatMethod(proto, name, true) : `the protocol ${formatMethod(proto, name)} belongs to` } in order to call ${typeName(thisArg)}#${name}(${ [].map.call(args, typeName).join(', ') }).` const err = new Error(msg) err.protocol = proto err.function = gf err.thisArg = thisArg err.args = args err.code = 'ENOIMPL' throw err }) } function isArray (x) { return Object.prototype.toString.call(x) === '[object Array]' } // Metaobject Protocol Protoduck.impl(Protoduck) // defaults configured by definition function _metaCreateGenfun (proto, target, name, deflt) { var gf = genfun({ default: deflt, name: `${proto.name ? `${proto.name}#` : ''}${name}` }) installMethodErrorMessage(proto, gf, target, name) gf.duck = proto return gf } function _metaAddMethod (duck, target, name, methodTypes, fn) { return target[name].add(methodTypes, fn) } // Constraints class Constraint { constructor (parent, thisType, argTypes) { this.parent = parent this.target = thisType this.types = argTypes } attach (obj) { this.child = obj if (this.target === 'this') { this.thisIdx = 'this' } else { const idx = this.child._types.indexOf(this.target) if (idx === -1) { this.thisIdx = null } else { this.thisIdx = idx } } this.indices = this.types.map(typeId => { if (typeId === 'this') { return 'this' } else { const idx = this.child._types.indexOf(typeId) if (idx === -1) { return null } else { return idx } } }) } verify (target, types) { const thisType = ( this.thisIdx === 'this' || this.thisIdx == null ) ? target : types[this.thisIdx] const parentTypes = this.indices.map(idx => { if (idx === 'this') { return target } else if (idx === 'this') { return types[this.thisIdx] } else if (idx === null) { return Object } else { return types[idx] || Object.prototype } }) return this.parent.hasImpl(thisType, parentTypes) } } Constraint.prototype.isConstraint = true