Created
July 3, 2021 14:36
-
-
Save jhunterkohler/a6b42a42166709d9c822651437750ff7 to your computer and use it in GitHub Desktop.
Callable classes in javascript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export function log(value, options) { | |
return console.dir(value, { | |
showHidden: true, | |
depth: null, | |
colors: true, | |
...options | |
}); | |
} | |
export function isKey(key) { | |
return ( | |
typeof key == "string" || | |
typeof key == "symbol" || | |
typeof key == "number" | |
); | |
} | |
export function wrapNew(ctor) { | |
return function wrapper() { | |
return new ctor(arguments); | |
}; | |
} | |
export function rename(func, name) { | |
return Reflect.defineProperty(func, "name", { | |
value: name, | |
configurable: true | |
}); | |
} | |
export function isTrivial(object) { | |
return ( | |
!(typeof object == "object" || typeof object == "function") || | |
object == null || | |
object == Object.prototype || | |
object == Function.prototype | |
); | |
} | |
export function bindMethod(object, key) { | |
const { name } = object[key]; | |
const defined = Reflect.defineProperty(object, key, { | |
...Reflect.getOwnPropertyDescriptor(object, key), | |
value: object[key].bind(object) | |
}); | |
if (defined) { | |
rename(object[key], name); | |
} | |
return defined; | |
} | |
export function bindAll(object) { | |
const properties = new Set(); | |
const failed = []; // Properties that could not be assigned. | |
let temp = object; | |
while (!isTrivial(temp)) { | |
for (const key of Reflect.ownKeys(temp)) { | |
if ( | |
typeof object[key] == "function" && | |
key != "constructor" && | |
!properties.has(key) | |
) { | |
if (!bindMethod(object, key)) { | |
failed.push(key); | |
} | |
} | |
// Non-function keys should not be bound if overriden by getter, | |
// property, etc.. | |
properties.add(key); | |
} | |
temp = Reflect.getPrototypeOf(temp); | |
} | |
if (failed.length) { | |
return failed; | |
} | |
} | |
export function bindEach(object, keys) { | |
const failed = []; | |
for (const key of keys) { | |
if (!bindMethod(object, key)) { | |
failed.push(key); | |
} | |
} | |
if (failed.length) { | |
return failed; | |
} | |
} | |
export class AbstractError extends Error { | |
constructor(constructorOpt, message) { | |
super(message); | |
if (constructorOpt) { | |
Error.captureStackTrace(this, constructorOpt); | |
} | |
rename(this, new.target.name); | |
} | |
} | |
export class AbstractClassError extends AbstractError { | |
constructor(constructorOpt, message) { | |
message ||= | |
`Cannot instantiate abstract class` + | |
(constructorOpt ? ` ${String(constructorOpt.name)}` : ""); | |
super(constructorOpt, message); | |
} | |
} | |
export class AbstractMethodError extends AbstractError { | |
constructor(constructorOpt, message) { | |
message ||= "Unimplemented abstract method"; | |
super(constructorOpt, message); | |
} | |
} | |
export function defineInstanceProperty(cls, property, value) { | |
return Reflect.defineProperty(cls.prototype, property, { | |
value, | |
configurable: true, | |
enumerable: false, | |
writable: typeof value == "function" | |
}); | |
} | |
export const Callable = new Proxy( | |
// Must be (anonymous), or any other name, to stop naming collisions with | |
// local function closures reference to the Callable proxy. Renamed | |
// in the static block. To reference the proxy target, use | |
// `this.constructor`. | |
class extends Function { | |
static #_ = (() => { | |
rename(this, "Callable"); | |
defineInstanceProperty(this, Symbol.toStringTag, "Callable"); | |
defineInstanceProperty(this, "toString", Object.prototype.toString); | |
})(); | |
constructor(name = "__call__") { | |
super(); | |
if (!isKey(name)) { | |
name = String(name); | |
} | |
rename(this, name); | |
if (new.target == Callable) { | |
throw new AbstractClassError(this.constructor); | |
} | |
if (typeof this[name] != "function") { | |
throw new AbstractMethodError( | |
this.constructor, | |
`Abstract method '${name}' was not implemented` + | |
` on '${new.target}'` | |
); | |
} | |
return new Proxy(this, { | |
apply(target, thisArg, argumentList) { | |
return target[target.name].apply(thisArg, argumentList); | |
} | |
}); | |
} | |
}, | |
{ | |
apply(target, thisArg, [defaultName]) { | |
class RenamedCallable extends Callable { | |
constructor(name = defaultName) { | |
super(name); | |
if (new.target == RenamedCallable) { | |
throw new AbstractClassError(RenamedCallable); | |
} | |
} | |
} | |
rename(RenamedCallable, `Callable(${String(defaultName)})`); | |
return RenamedCallable; | |
} | |
} | |
); | |
export const Main = Callable("main"); | |
class Main extends Callable("main") { | |
static #_ = () => { | |
defineInstanceProperty(Main, Symbol.toStringTag, "Main"); | |
}; | |
constructor() { | |
bindAll(this); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment