• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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