mirror of
https://github.com/nodejs/node.git
synced 2025-05-18 02:19:02 +00:00

PR-URL: https://github.com/nodejs/node/pull/25804 Reviewed-By: Myles Borins <myles.borins@gmail.com>
350 lines
9.4 KiB
JavaScript
350 lines
9.4 KiB
JavaScript
'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
|