diff --git a/eslint.config.cjs b/eslint.config.cjs index 2c72cbf..88d71a9 100644 --- a/eslint.config.cjs +++ b/eslint.config.cjs @@ -22,8 +22,8 @@ module.exports = [ 'lit-a11y/click-events-have-key-events': 'off', 'no-debugger': 'error', 'no-console': 'error', - semi: ['error', 'never'], - indent: ['error', 'tab'] + indent: 'off', + semi: ['error', 'never'] } } ] diff --git a/src/diamond.ts b/src/diamond.ts index df83747..18fce0a 100644 --- a/src/diamond.ts +++ b/src/diamond.ts @@ -1,10 +1,37 @@ -import { allFLegs, bottomLeg, diamondHandler, fLegs, temporaryBuiltObjects } from './utils' +import { allFLegs, bottomLeg, fLegs, nextInFLeg, temporaryBuiltObjects } from './utils' let buildingDiamond: { built: object strategy: BuildingStrategy } | null = null +export const diamondHandler: { + getPrototypeOf(target: Ctor): Ctor + get(target: Ctor, p: PropertyKey, receiver: Ctor): any + set(target: Ctor, p: PropertyKey, v: any, receiver: Ctor): boolean +} & ProxyHandler = { + get(target, p, receiver) { + const pd = nextInFLeg(receiver.constructor, p, target) + return pd && ('value' in pd ? pd.value : 'get' in pd ? pd.get!.call(receiver) : undefined) + }, + set(target, p, v, receiver) { + const pd = nextInFLeg(receiver.constructor, p, target) + if (!pd || pd.writable) + Object.defineProperty(receiver, p, { + value: v, + writable: true, + enumerable: true, + configurable: true + }) + else if (pd && pd.set) pd.set.call(receiver, v) + else return false + return true + }, + getPrototypeOf(target) { + return Object + } +} + export default function Diamond( ...baseClasses: TBases ): Ctor> { diff --git a/src/index.ts b/src/index.ts index c4426f7..8104c59 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ import Diamond from './diamond' - export default Diamond export * from './helpers' +export { Protect } from './protect' diff --git a/src/protect.ts b/src/protect.ts new file mode 100644 index 0000000..4a3875f --- /dev/null +++ b/src/protect.ts @@ -0,0 +1,144 @@ +import Diamond, { diamondHandler } from './diamond' +import { constructedObject } from './helpers' +import { allFLegs, bottomLeg, nextInLine } from './utils' + +const publicPart = (x: Ctor): Ctor => Object.getPrototypeOf(Object.getPrototypeOf(x)) + +export function Protect)[]>( + base: TBase, + properties: Keys +): Protected { + const protectedProperties: KeySet = properties.reduce( + (acc, p) => ({ ...acc, [p]: true }) as KeySet, + {} + ) + const privates = new WeakMap() + const diamond = Diamond(base) as TBase + class Protected extends (diamond as any) { + static privatePart(obj: TBase): TBase | undefined { + return privates.get(obj) + } + constructor(...args: any[]) { + super(...args) + const actThis = constructedObject(this) + privates.set( + actThis, + Object.create( + // This proxy is used to write public properties in the prototype (the public object) + new Proxy(actThis, { + set(target, p, value, receiver) { + Object.defineProperty(p in protectedProperties ? receiver : target, p, { + value, + writable: true, + enumerable: true, + configurable: true + }) + return true + }, + getPrototypeOf: (target) => target + }) + ) + ) + } + } + function whoAmI(receiver: TBase) { + const domain = privates.has(receiver) + ? 'public' + : privates.get(publicPart(receiver)) === receiver + ? 'private' + : 'error' + // If it's not tested, it means all the tests pass: this should never happen + if (domain === 'error') throw new Error('Invalid domain') + return { + domain, + public: domain === 'public' ? receiver : publicPart(receiver), + private: domain === 'private' ? receiver : privates.get(receiver)! + } + } + function fakeCtor() {} + fakeCtor.prototype = new Proxy(base, { + getOwnPropertyDescriptor(target, p) { + if (p in target.prototype) { + const pd = nextInLine(target, p)! + if ('value' in pd && typeof pd.value === 'function') + return { + ...pd, + value: function (this: any, ...args: any) { + return pd.value.apply(privates.get(this) || this, args) + } + } + else { + let modified = { ...pd } + if ('get' in pd) + modified.get = function (this: any) { + return pd.get!.call(privates.get(this) || this) + } + if ('set' in pd) + modified.set = function (this: any, value: any) { + return pd.set!.call(privates.get(this) || this, value) + } + return modified + } + } + return undefined + }, + get: (target, p, receiver) => { + if (p === 'constructor') return fakeCtor + const actor = whoAmI(receiver) + if (p in target.prototype) { + const pd = nextInLine(target, p)! + if ('get' in pd) return pd.get!.call(actor.private) + if ('value' in pd) { + const rv = pd.value! + return typeof rv === 'function' + ? function (this: any, ...args: any) { + return rv.apply(actor.private, args) + } + : rv + } + // No legacy involved: it was well defined in our classes but `readable: false` ... + return undefined + } + if (p in protectedProperties && actor.domain === 'private') + // If we arrive here, it means it's private but not set in the private part + return undefined + if (allFLegs.has(actor.public)) return diamondHandler.get(bottomLeg(target), p, receiver) + // If we arrive here, it means it's public but not set in the public part + return undefined + }, + set: (target, p, value, receiver) => { + const actor = whoAmI(receiver) + if (p in target.prototype) { + const pd = nextInLine(target, p)! + if ('set' in pd) { + pd.set!.call(actor.private, value) + return true + } + if (!pd.writable) return false + } + + if (p in protectedProperties && actor.domain === 'private') { + Object.defineProperty(receiver, p, { + value, + writable: true, + enumerable: true, + configurable: true + }) + return true + } + if (allFLegs.has(actor.public)) + return diamondHandler.set(bottomLeg(target), p, value, receiver) + Object.defineProperty(actor.public, p, { + value, + writable: true, + enumerable: true, + configurable: true + }) + return true + }, + //getPrototypeOf: (target): any => fakeCtor.prototype + getPrototypeOf: (target) => diamond.prototype + }) + Object.setPrototypeOf(Protected.prototype, fakeCtor.prototype) + return Protected as any +} diff --git a/src/types.d.ts b/src/types.d.ts index 8e92112..5eedeaa 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -2,6 +2,7 @@ * The type of the constructor of an object. */ type Ctor = abstract new (...params: any[]) => Class +type Newable = new (...args: any[]) => Class // Here, much black magic is kept in comments for research purpose. The "OmitNonAbstract" type has not yet been found. /** @@ -57,3 +58,9 @@ type HasBases = TBases extends [] : never type BuildingStrategy = Map +type KeySet = Record +type Protected)[]> = Newable< + Omit, Keys[number]> +> & { + privatePart(obj: InstanceType): InstanceType | undefined +} diff --git a/src/utils.ts b/src/utils.ts index e74d11b..a471d6c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,13 +1,3 @@ -export class LegacyConsistencyError extends Error { - constructor( - public diamond: Ctor, - public base: Ctor - ) { - super() - } - name = 'LegacyConsistencyError' -} - /** * Gives all the classes from the base up to just before Object * Note: In "uni-legacy", the parent of Diamond is Object @@ -53,11 +43,12 @@ export function fLegs(ctor: Ctor) { * @returns */ export function nextInFLeg(ctor: Ctor, name: PropertyKey, diamond: Ctor) { - const fLeg = fLegs(ctor)! + const fLeg = fLegs(ctor) + if (!fLeg) throw new Error('Inconsistent diamond hierarchy') let ndx = bottomLeg(ctor) === diamond ? 0 : -1 if (ndx < 0) { ndx = fLeg.findIndex((base) => bottomLeg(base) === diamond) + 1 - if (ndx <= 0) throw new LegacyConsistencyError(diamond, ctor) + if (ndx <= 0) throw new Error('Inconsistent diamond hierarchy') } let rv: PropertyDescriptor | undefined do rv = nextInLine(fLeg[ndx++], name) @@ -65,29 +56,5 @@ export function nextInFLeg(ctor: Ctor, name: PropertyKey, diamond: Ctor) { return rv } -export const diamondHandler: ProxyHandler = { - get(target, p, receiver) { - if (p === Symbol.hasInstance) return () => false - const pd = nextInFLeg(receiver.constructor, p, target) - return pd && ('value' in pd ? pd.value : 'get' in pd ? pd.get!.call(receiver) : undefined) - }, - set(target, p, v, receiver) { - const pd = nextInFLeg(receiver.constructor, p, target) - if (!pd || pd.writable) - Object.defineProperty(receiver, p, { - value: v, - writable: true, - enumerable: true, - configurable: true - }) - else if (pd && pd.set) pd.set.call(receiver, v) - else return false - return true - }, - getPrototypeOf(target) { - return Object - } -} - export const temporaryBuiltObjects = new WeakMap(), allFLegs = new WeakMap() diff --git a/test/protect.test.ts b/test/protect.test.ts new file mode 100644 index 0000000..eca7734 --- /dev/null +++ b/test/protect.test.ts @@ -0,0 +1,94 @@ +import Diamond, { Protect } from '../src' + +test('leg-less', () => { + class X { + pubFld = 0 + prvFld = 0 + setPrvFld(v: number) { + this.prvFld = v + } + getPrvFld() { + return this.prvFld + } + setPubFld(v: number) { + this.pubFld = v + } + getPubFld() { + return this.pubFld + } + + get accFld() { + return this.prvFld + } + set accFld(v: number) { + this.prvFld = v + } + } + const P = Protect(X, ['prvFld']) + class Y extends P { + prvFld = 0 + } + + const t = new Y() + t.pubFld = 1 + expect(t.getPubFld()).toBe(1) + t.prvFld = 2 + t.accFld = 3 + expect(t.prvFld).toBe(2) + expect(t.getPrvFld()).toBe(3) + t.setPubFld(4) + expect(t.pubFld).toBe(4) + t.setPrvFld(5) + expect(t.accFld).toBe(5) + expect(t.prvFld).toBe(2) + const pp = P.privatePart(t) + expect(pp?.prvFld).toBe(5) + expect(pp?.pubFld).toBe(4) +}) + +test('leg-full', () => { + class X { + pubFld = 0 + prvFld = 0 + setPrvFld(v: number) { + this.prvFld = v + } + getPrvFld() { + return this.prvFld + } + setPubFld(v: number) { + this.pubFld = v + } + getPubFld() { + return this.pubFld + } + + get accFld() { + return this.prvFld + } + set accFld(v: number) { + this.prvFld = v + } + } + const P = Protect(X, ['prvFld']) + class Y { + prvFld = 0 + } + class D extends Diamond(P, Y) {} + + const t = new D() + t.pubFld = 1 + expect(t.getPubFld()).toBe(1) + t.prvFld = 2 + t.accFld = 3 + expect(t.prvFld).toBe(2) + expect(t.getPrvFld()).toBe(3) + t.setPubFld(4) + expect(t.pubFld).toBe(4) + t.setPrvFld(5) + expect(t.accFld).toBe(5) + expect(t.prvFld).toBe(2) + const pp = P.privatePart(t) + expect(pp?.prvFld).toBe(5) + expect(pp?.pubFld).toBe(4) +})