1'use strict' 2 3const genfun = require('genfun') 4 5class Duck extends Function { 6 // Duck.impl(Foo, [String, Array], { frob (str, arr) { ... }}) 7 impl (target, types, impls) { 8 if (!impls && !isArray(types)) { 9 impls = types 10 types = [] 11 } 12 if (!impls && this.isDerivable) { 13 impls = this._defaultImpls 14 } 15 if (!impls) { 16 impls = {} 17 } 18 if (typeof target === 'function' && !target.isGenfun) { 19 target = target.prototype 20 } 21 checkImpls(this, target, impls) 22 checkArgTypes(this, types) 23 this._constraints.forEach(c => { 24 if (!c.verify(target, types)) { 25 throw new Error(`Implementations of ${ 26 this.name || 'this protocol' 27 } must first implement ${ 28 c.parent.name || 'its constraint protocols defined in opts.where.' 29 }`) 30 } 31 }) 32 this._methodNames.forEach(name => { 33 defineMethod(this, name, target, types, impls) 34 }) 35 } 36 37 hasImpl (arg, args) { 38 args = args || [] 39 const fns = this._methodNames 40 var gf 41 if (typeof arg === 'function' && !arg.isGenfun) { 42 arg = arg.prototype 43 } 44 args = args.map(arg => { 45 if (typeof arg === 'function' && !arg.isGenfun) { 46 return arg.prototype 47 } else { 48 return arg 49 } 50 }) 51 for (var i = 0; i < fns.length; i++) { 52 gf = arg[fns[i]] 53 if (!gf || 54 (gf.hasMethod 55 ? !gf.hasMethod.apply(gf, args) 56 : typeof gf === 'function')) { 57 return false 58 } 59 } 60 return true 61 } 62 63 // MyDuck.matches('a', ['this', 'c']) 64 matches (thisType, argTypes) { 65 if (!argTypes && isArray(thisType)) { 66 argTypes = thisType 67 thisType = 'this' 68 } 69 if (!thisType) { 70 thisType = 'this' 71 } 72 if (!argTypes) { 73 argTypes = [] 74 } 75 return new Constraint(this, thisType, argTypes) 76 } 77} 78Duck.prototype.isDuck = true 79Duck.prototype.isProtocol = true 80 81const Protoduck = module.exports = define(['duck'], { 82 createGenfun: ['duck', _metaCreateGenfun], 83 addMethod: ['duck', _metaAddMethod] 84}, { name: 'Protoduck' }) 85 86const noImplFound = module.exports.noImplFound = genfun.noApplicableMethod 87 88module.exports.define = define 89function define (types, spec, opts) { 90 if (!isArray(types)) { 91 // protocol(spec, opts?) syntax for method-based protocols 92 opts = spec 93 spec = types 94 types = [] 95 } 96 const duck = function (thisType, argTypes) { 97 return duck.matches(thisType, argTypes) 98 } 99 Object.setPrototypeOf(duck, Duck.prototype) 100 duck.isDerivable = true 101 Object.defineProperty(duck, 'name', { 102 value: (opts && opts.name) || 'Protocol' 103 }) 104 if (opts && opts.where) { 105 let where = opts.where 106 if (!isArray(opts.where)) { where = [opts.where] } 107 duck._constraints = where.map(w => w.isProtocol // `where: [Foo]` 108 ? w.matches() 109 : w 110 ) 111 } else { 112 duck._constraints = [] 113 } 114 duck.isProtocol = true 115 duck._metaobject = opts && opts.metaobject 116 duck._types = types 117 duck._defaultImpls = {} 118 duck._gfTypes = {} 119 duck._methodNames = Object.keys(spec) 120 duck._methodNames.forEach(name => { 121 checkMethodSpec(duck, name, spec) 122 }) 123 duck._constraints.forEach(c => c.attach(duck)) 124 return duck 125} 126 127function checkMethodSpec (duck, name, spec) { 128 let gfTypes = spec[name] 129 if (typeof gfTypes === 'function') { 130 duck._defaultImpls[name] = gfTypes 131 gfTypes = [gfTypes] 132 } if (typeof gfTypes[gfTypes.length - 1] === 'function') { 133 duck._defaultImpls[name] = gfTypes.pop() 134 } else { 135 duck.isDerivable = false 136 } 137 duck._gfTypes[name] = gfTypes.map(typeId => { 138 const idx = duck._types.indexOf(typeId) 139 if (idx === -1) { 140 throw new Error( 141 `type '${ 142 typeId 143 }' for function '${ 144 name 145 }' does not match any protocol types (${ 146 duck._types.join(', ') 147 }).` 148 ) 149 } else { 150 return idx 151 } 152 }) 153} 154 155function defineMethod (duck, name, target, types, impls) { 156 const methodTypes = duck._gfTypes[name].map(function (typeIdx) { 157 return types[typeIdx] 158 }) 159 for (let i = methodTypes.length - 1; i >= 0; i--) { 160 if (methodTypes[i] === undefined) { 161 methodTypes.pop() 162 } else { 163 break 164 } 165 } 166 const useMetaobject = duck._metaobject && duck._metaobject !== Protoduck 167 // `target` does not necessarily inherit from `Object` 168 if (!Object.prototype.hasOwnProperty.call(target, name)) { 169 // Make a genfun if there's nothing there 170 const gf = useMetaobject 171 ? duck._metaobject.createGenfun(duck, target, name, null) 172 : _metaCreateGenfun(duck, target, name, null) 173 target[name] = gf 174 } else if (typeof target[name] === 'function' && !target[name].isGenfun) { 175 // Turn non-gf functions into genfuns 176 const gf = useMetaobject 177 ? duck._metaobject.createGenfun(duck, target, name, target[name]) 178 : _metaCreateGenfun(duck, target, name, target[name]) 179 target[name] = gf 180 } 181 182 const fn = impls[name] || duck._defaultImpls[name] 183 if (fn) { // checkImpls made sure this is safe 184 useMetaobject 185 ? duck._metaobject.addMethod(duck, target, name, methodTypes, fn) 186 : _metaAddMethod(duck, target, name, methodTypes, fn) 187 } 188} 189 190function checkImpls (duck, target, impls) { 191 duck._methodNames.forEach(function (name) { 192 if ( 193 !impls[name] && 194 !duck._defaultImpls[name] && 195 // Existing methods on the target are acceptable defaults. 196 typeof target[name] !== 'function' 197 ) { 198 throw new Error(`Missing implementation for ${ 199 formatMethod(duck, name, duck.name) 200 }. Make sure the method is present in your ${ 201 duck.name || 'protocol' 202 } definition. Required methods: ${ 203 duck._methodNames.filter(m => { 204 return !duck._defaultImpls[m] 205 }).map(m => formatMethod(duck, m)).join(', ') 206 }.`) 207 } 208 }) 209 Object.keys(impls).forEach(function (name) { 210 if (duck._methodNames.indexOf(name) === -1) { 211 throw new Error( 212 `${name}() was included in the impl, but is not part of ${ 213 duck.name || 'the protocol' 214 }. Allowed methods: ${ 215 duck._methodNames.map(m => formatMethod(duck, m)).join(', ') 216 }.` 217 ) 218 } 219 }) 220} 221 222function formatMethod (duck, name, withDuckName) { 223 return `${ 224 withDuckName && duck.name ? `${duck.name}#` : '' 225 }${name}(${duck._gfTypes[name].map(n => duck._types[n]).join(', ')})` 226} 227 228function checkArgTypes (duck, types) { 229 var requiredTypes = duck._types 230 if (types.length > requiredTypes.length) { 231 throw new Error( 232 `${ 233 duck.name || 'Protocol' 234 } expects to be defined across ${ 235 requiredTypes.length 236 } type${requiredTypes.length > 1 ? 's' : ''}, but ${ 237 types.length 238 } ${types.length > 1 ? 'were' : 'was'} specified.` 239 ) 240 } 241} 242 243function typeName (obj) { 244 return (/\[object ([a-zA-Z0-9]+)\]/).exec(({}).toString.call(obj))[1] 245} 246 247function installMethodErrorMessage (proto, gf, target, name) { 248 noImplFound.add([gf], function (gf, thisArg, args) { 249 let parent = Object.getPrototypeOf(thisArg) 250 while (parent && parent[name] === gf) { 251 parent = Object.getPrototypeOf(parent) 252 } 253 if (parent && parent[name] && typeof parent[name] === 'function') { 254 } 255 var msg = `No ${typeName(thisArg)} impl for ${ 256 proto.name ? `${proto.name}#` : '' 257 }${name}(${[].map.call(args, typeName).join(', ')}). You must implement ${ 258 proto.name 259 ? formatMethod(proto, name, true) 260 : `the protocol ${formatMethod(proto, name)} belongs to` 261 } in order to call ${typeName(thisArg)}#${name}(${ 262 [].map.call(args, typeName).join(', ') 263 }).` 264 const err = new Error(msg) 265 err.protocol = proto 266 err.function = gf 267 err.thisArg = thisArg 268 err.args = args 269 err.code = 'ENOIMPL' 270 throw err 271 }) 272} 273 274function isArray (x) { 275 return Object.prototype.toString.call(x) === '[object Array]' 276} 277 278// Metaobject Protocol 279Protoduck.impl(Protoduck) // defaults configured by definition 280 281function _metaCreateGenfun (proto, target, name, deflt) { 282 var gf = genfun({ 283 default: deflt, 284 name: `${proto.name ? `${proto.name}#` : ''}${name}` 285 }) 286 installMethodErrorMessage(proto, gf, target, name) 287 gf.duck = proto 288 return gf 289} 290 291function _metaAddMethod (duck, target, name, methodTypes, fn) { 292 return target[name].add(methodTypes, fn) 293} 294 295// Constraints 296class Constraint { 297 constructor (parent, thisType, argTypes) { 298 this.parent = parent 299 this.target = thisType 300 this.types = argTypes 301 } 302 303 attach (obj) { 304 this.child = obj 305 if (this.target === 'this') { 306 this.thisIdx = 'this' 307 } else { 308 const idx = this.child._types.indexOf(this.target) 309 if (idx === -1) { 310 this.thisIdx = null 311 } else { 312 this.thisIdx = idx 313 } 314 } 315 this.indices = this.types.map(typeId => { 316 if (typeId === 'this') { 317 return 'this' 318 } else { 319 const idx = this.child._types.indexOf(typeId) 320 if (idx === -1) { 321 return null 322 } else { 323 return idx 324 } 325 } 326 }) 327 } 328 329 verify (target, types) { 330 const thisType = ( 331 this.thisIdx === 'this' || this.thisIdx == null 332 ) 333 ? target 334 : types[this.thisIdx] 335 const parentTypes = this.indices.map(idx => { 336 if (idx === 'this') { 337 return target 338 } else if (idx === 'this') { 339 return types[this.thisIdx] 340 } else if (idx === null) { 341 return Object 342 } else { 343 return types[idx] || Object.prototype 344 } 345 }) 346 return this.parent.hasImpl(thisType, parentTypes) 347 } 348} 349Constraint.prototype.isConstraint = true 350