UNPKG

@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
import { type, Type, isGenericType, typeName, createFactory } from '@rustable/type'; import { TraitError, TraitNotImplementedError, MultipleImplementationError, MethodNotImplementedError } from './error.mjs'; "use strict"; 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 };