@rustable/trait
Version:
A TypeScript library that implements Rust-like traits with compile-time type checking and runtime verification.
423 lines (420 loc) • 13.1 kB
JavaScript
import { type, Type, isGenericType, typeName, createFactory } from '@rustable/type';
import { TraitError, TraitNotImplementedError, MultipleImplementationError, MethodNotImplementedError } from './error.mjs';
;
var _a;
const traitRegistry = /* @__PURE__ */ new WeakMap();
const staticTraitRegistry = /* @__PURE__ */ new WeakMap();
const traitToTraitRegistry = /* @__PURE__ */ new WeakMap();
const traitImplementersRegistry = /* @__PURE__ */ new WeakMap();
const parentTraitsCache = /* @__PURE__ */ new WeakMap();
const traitSymbol = Symbol("TRAIT");
class Trait {
static {
_a = traitSymbol;
}
static {
this[_a] = true;
}
/**
* Checks if a value implements the trait.
* This is a type-safe alternative to the hasTrait function.
*
* @param this The trait constructor
* @param val The value to check
* @returns true if the value implements the trait
*
* @example
* ```typescript
* @trait
* class Display extends ITrait {
* display(): string {
* return 'default';
* }
* }
*
* const point = new Point(1, 2);
* if (Display.isImplFor(point)) {
* // point implements Display trait
* const display = Display.wrap(point);
* console.log(display.display());
* }
* ```
*/
static isImplFor(val) {
return hasTrait(val, this);
}
/**
* Validates that a value implements the trait.
* Throws an error if the value does not implement the trait.
*
* @param this The trait constructor
* @param val The value to validate
* @throws {Error} if the value does not implement the trait
*
* @example
* ```typescript
* @trait
* class Display extends ITrait {
* display(): string {
* return 'default';
* }
* }
*
* // Throws if point doesn't implement Display trait
* Display.validType(point);
* ```
*/
static validFor(val) {
validTrait(val, this);
}
static wrap(val, strict) {
validTrait(val, this);
return useTrait(val, this, strict);
}
/**
* Wraps a value as a static trait type.
* Specifically handles static method trait wrapping and automatically handles multiple implementations.
*
* @param this The trait constructor
* @param val The value or constructor to wrap
* @returns The wrapped static trait constructor
* @throws {Error} if the value does not implement the trait
* @throws {TraitMethodNotImplementedError} if accessing an unimplemented static trait method
*
* @example
* ```typescript
* @trait
* class FromStr extends ITrait {
* static fromStr(s: string): any {
* throw new Error('Not implemented');
* }
* }
*
* // Wrap Point's static methods
* const PointFromStr = FromStr.staticWrap(Point);
* const point = PointFromStr.fromStr('1,2');
* ```
*/
static staticWrap(val) {
validTrait(val, this);
return useTrait(type(val), this);
}
/**
* Implements the trait for a target class.
* Provides a convenient API for implementing traits with support for default implementations.
*
* @param this The trait constructor
* @param target The target class to implement the trait for
* @param implementation Optional implementation overrides
*
* @example
* ```typescript
* @trait
* class Display extends ITrait {
* display(): string {
* return 'default';
* }
* }
*
* // Use custom implementation
* Display.implFor(Point, {
* display() {
* return `(${this.x}, ${this.y})`;
* }
* });
*
* // Use default implementation
* Display.implFor(OtherClass);
* ```
*/
static implFor(target, implementation) {
implTrait(target, this, implementation);
}
/**
* Tries to implement the trait for a target class.
* Similar to implFor, but doesn't throw an error if the trait is already implemented.
*
* @param this The trait constructor
* @param target The target class to implement the trait for
* @param implementation Optional implementation overrides
*
* @example
* ```typescript
* @trait
* class Display extends ITrait {
* display(): string {
* return 'default';
* }
* }
*
* // Use custom implementation
* Display.tryImplFor(Point, {
* display() {
* return `(${this.x}, ${this.y})`;
* }
* });
*
* // Use default implementation
* Display.tryImplFor(Point);
* ```
*/
static tryImplFor(target, implementation) {
tryImplTrait(target, this, implementation);
}
}
function implTrait(target, trait, implementation) {
trait = Type(trait);
target = Type(target);
if (!trait[traitSymbol]) {
throw new TraitError(trait.name + " must be implemented using the trait function");
}
const staticImplMap = staticTraitRegistry.get(target) || /* @__PURE__ */ new WeakMap();
staticTraitRegistry.set(target, staticImplMap);
if (staticImplMap.has(trait)) {
throw new Error(`Trait ${trait.name} already implemented for ${target.name}`);
}
const parents = collectParentTraits(trait);
parents.forEach((parent) => {
const parentId = Type(parent);
if (!staticImplMap.has(parentId)) {
throw new TraitNotImplementedError(target.name, parent.name);
}
});
const staticImpl = createBound(trait, isGenericType(trait) ? 2 : 1, implementation?.static);
handleGenericType(trait, (trait2) => cacheTraitBound(target, trait2, staticImplMap, staticImpl), () => false);
const isTraitTarget = traitSymbol in target;
if (isTraitTarget) {
const traitTarget = target;
const implMap2 = traitToTraitRegistry.get(traitTarget) || /* @__PURE__ */ new Map();
traitToTraitRegistry.set(traitTarget, implMap2);
implMap2.set(trait, { implementation });
const implementers2 = traitImplementersRegistry.get(traitTarget) || [];
for (const implementer of implementers2) {
tryImplTrait(implementer, trait, implementation);
}
return;
}
addMethod(target, staticImpl, getSelfStaticBound(target));
const implMap = traitRegistry.get(target) || /* @__PURE__ */ new WeakMap();
traitRegistry.set(target, implMap);
const boundImpl = createBound(trait.prototype, isGenericType(trait) ? 2 : 1, implementation);
handleGenericType(trait, (trait2) => cacheTraitBound(target, trait2, implMap, boundImpl), () => false);
addMethod(target.prototype, boundImpl, getSelfBound(target));
let implementers = traitImplementersRegistry.get(trait);
if (!implementers) {
implementers = [];
traitImplementersRegistry.set(trait, implementers);
}
implementers.push(target);
if (isGenericType(trait)) {
traitToTraitImpl(target, Object.getPrototypeOf(trait).prototype.constructor);
}
traitToTraitImpl(target, trait);
}
function addMethod(target, boundImpl, selfBoundImpl) {
boundImpl.forEach((value, name) => {
if (!(name in target) || Object.prototype[name] === target[name]) {
Object.defineProperty(target, name, {
value: function(...args) {
return value.apply(this, args);
},
enumerable: false,
configurable: true,
writable: true
});
} else if (!selfBoundImpl.has(name)) {
Object.defineProperty(target, name, {
value: function() {
throw new MultipleImplementationError(typeName(target), name);
},
enumerable: false,
configurable: true,
writable: true
});
}
});
}
function cacheTraitBound(target, trait, implMap, boundImpl) {
const old = implMap.get(trait);
if (old !== void 0) {
const conflict = /* @__PURE__ */ new Map();
for (const [key] of boundImpl) {
conflict.set(key, function() {
throw new MultipleImplementationError(typeName(target), key);
});
}
implMap.set(trait, conflict);
} else {
implMap.set(trait, boundImpl);
}
}
function traitToTraitImpl(target, trait) {
const traitImplMap = traitToTraitRegistry.get(trait);
if (traitImplMap) {
for (const [parentTrait, implInfo] of traitImplMap) {
tryImplTrait(target, parentTrait, implInfo.implementation);
}
}
}
function getSelfStaticBound(target) {
let selfBoundImpl;
const implMap = staticTraitRegistry.get(target);
if (!implMap.has(target)) {
selfBoundImpl = createBound(target, -1);
implMap.set(target, selfBoundImpl);
} else {
selfBoundImpl = implMap.get(target);
}
return selfBoundImpl;
}
function getSelfBound(target) {
let selfBoundImpl;
const implMap = traitRegistry.get(target);
if (!implMap.has(target)) {
selfBoundImpl = createBound(target.prototype, -1);
implMap.set(target, selfBoundImpl);
} else {
selfBoundImpl = implMap.get(target);
}
return selfBoundImpl;
}
const skip = /* @__PURE__ */ new Set(["constructor"]);
function createBound(target, lc = 1, implementation) {
const bound = /* @__PURE__ */ new Map();
let current = target;
while (current !== Object.prototype && lc !== 0) {
lc--;
Object.getOwnPropertyNames(current).forEach((name) => {
try {
if (!bound.has(name) && !skip.has(name) && typeof current[name] === "function") {
bound.set(name, current[name]);
}
} catch (_) {
}
});
current = Object.getPrototypeOf(current);
}
if (implementation) {
putCustomImpl(target, implementation, bound);
}
return bound;
}
function hasTrait(target, trait) {
target = Type(type(target));
trait = Type(trait);
if (traitSymbol in target) {
return handleGenericType(target, (target2) => {
return traitToTraitRegistry.get(target2);
}, (impl) => !!impl?.has(trait));
}
return handleGenericType(target, (target2) => {
return staticTraitRegistry.get(target2);
}, (impl) => !!impl?.has(trait));
}
function useTrait(target, trait, strict = false) {
if (typeof target === "function" && traitSymbol in target)
strict = true;
if (typeof target === "function") {
return createUseProxy(target, trait, strict, (target2) => {
return staticTraitRegistry.get(target2);
});
} else {
return createUseProxy(target, trait, strict, (target2) => {
return traitRegistry.get(type(target2));
});
}
}
function createUseProxy(target, trait, strict, handleTarget) {
trait = Type(trait);
const targetType = Type(type(target));
const impl = handleGenericType(targetType, handleTarget, (impl2) => impl2?.get(trait));
if (!impl) {
throw new TraitNotImplementedError(targetType.name, trait.name);
}
const traits = collectParentTraits(trait).reverse();
const proxy = {};
for (const parent of traits) {
const parentImpl = handleGenericType(targetType, handleTarget, (impl2) => impl2?.get(parent));
addProxyMethod(proxy, target, parentImpl, strict);
}
addProxyMethod(proxy, target, impl, strict);
Object.setPrototypeOf(proxy, targetType.prototype);
return proxy;
}
function addProxyMethod(proxy, target, impl, strict) {
for (const [name, fn] of impl) {
if (!strict) {
proxy[name] = function(...args) {
try {
return target[name](...args);
} catch (error) {
if (error instanceof MultipleImplementationError) {
return fn.apply(target, args);
}
throw error;
}
};
} else {
proxy[name] = fn.bind(target);
}
}
}
function macroTrait(trait, implementation) {
const factoryFn = function(target) {
for (const parent of collectParentTraits(trait)) {
tryImplTrait(target, parent);
}
tryImplTrait(target, trait, implementation);
};
return createFactory(trait, factoryFn);
}
function tryImplTrait(target, trait, implementation) {
if (!hasTrait(target, trait)) {
implTrait(target, trait, implementation);
}
}
function validTrait(target, trait) {
if (!hasTrait(target, trait)) {
throw new TraitNotImplementedError(typeName(target), typeName(trait));
}
}
function collectParentTraits(trait) {
const traitConstructor = trait.prototype.constructor;
const cached = parentTraitsCache.get(traitConstructor);
if (cached) {
return cached;
}
const parents = [];
let proto = Object.getPrototypeOf(trait.prototype);
while (proto && proto !== Trait.prototype) {
const parentTrait = proto.constructor;
if (parentTrait && parentTrait !== Trait && parentTrait[traitSymbol]) {
parents.push(parentTrait);
}
proto = Object.getPrototypeOf(proto);
}
if (isGenericType(trait)) {
parents.shift();
}
parentTraitsCache.set(traitConstructor, parents);
return parents;
}
function handleGenericType(type2, handler, result) {
const ret = result(handler(type2));
if (!ret && isGenericType(type2)) {
return result(handler(Object.getPrototypeOf(type2.prototype).constructor));
}
return ret;
}
function putCustomImpl(trait, implementation, boundImpl) {
Object.entries(implementation).forEach(([key, method]) => {
if (key === "static") {
return;
}
if (!boundImpl.has(key)) {
throw new MethodNotImplementedError(typeName(trait), key);
}
boundImpl.set(key, method);
});
}
export { Trait, macroTrait };