Skip to content

Commit

Permalink
protect->seclude
Browse files Browse the repository at this point in the history
  • Loading branch information
eddow committed Nov 2, 2024
1 parent 52df41d commit 64ffefe
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 36 deletions.
28 changes: 14 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ import D, { constructedObject } from 'flat-diamond'
> Note: There is no need to modify it directly, all the properties initialized on the temporary object are going to be transposed on it
> Note: `constructedObject(this)` will return a relevant value _only_ in classes extending `Diamond(...)` after `super(...)`
# Protection
# Seclusion

Another big deal of diamond inheritance is field conflicts.

Expand All @@ -225,50 +225,50 @@ Don't make field conflicts. Just don't.

## Yes, but it's a library I don't write

Here it is tricky, and that's where _protection_ comes in. Let's speak about protection without speaking of diamond - and, if you wish, the protection works without the need of involving `Diamond`. (though it is also completely integrated)
Here it is tricky, and that's where _seclusion_ comes in. Let's speak about seclusion without speaking of diamond - and, if you wish, the seclusion works without the need of involving `Diamond`. (though it is also completely integrated)

Let's say we want a `DuckCourier` to implement `Plane`, and end up with a conflict of `wingSpan` (the one of the duck and the one of the device strapped on him, the `Plane` one)

A pure and simple `class DuckCourier extends Plane` would have a field conflict. So, instead, protection will be used :
A pure and simple `class DuckCourier extends Plane` would have a field conflict. So, instead, seclusion will be used :

```ts
import { Protect } from 'flat-diamond'
import { Seclude } from 'flat-diamond'

class DuckCourier extends Protect(Plane, ['wingSpan']) { ... }
class DuckCourier extends Seclude(Plane, ['wingSpan']) { ... }
```

As simple as that, methods (as well as accessors) of `Plane` and `DuckCourier` will access two different values when accessing `this.wingSpan`

## But ... How ? And, how can I ...

When a protected class is implemented, `this` (so, here, a `DuckCourier`) will be used as the prototype for a `Private<Plane>`. A Proxy is added between `Protected` and `Plane` to manage who is `this` in method calls (either `DuckCourier` or `Private<Plane>`) - et voilà!
When a secluded class is implemented, `this` (so, here, a `DuckCourier`) will be used as the prototype for a `Private<Plane>`. A Proxy is added between `Secluded` and `Plane` to manage who is `this` in method calls (either `DuckCourier` or `Private<Plane>`) - et voilà!

Because of prototyping, `Private<Plane>` has access to all the functionalities of `DuckCourier` (and therefore of `Plane`) while never interfering with `DuckCourier::wingSpan`. Also, having several protected class in the legacy list will only create several "heads" who will share a prototype.
Because of prototyping, `Private<Plane>` has access to all the functionalities of `DuckCourier` (and therefore of `Plane`) while never interfering with `DuckCourier::wingSpan`. Also, having several secluded class in the legacy list will only create several "heads" who will share a prototype.

`DuckCourier` on another hand, _can_ interfere with `Plane::wingSpan` if needed thanks to the `privatePart` exposed by the `Protected` class.
`DuckCourier` on another hand, _can_ interfere with `Plane::wingSpan` if needed thanks to the `privatePart` exposed by the `Secluded` class.

```ts
import { Protect } from 'flat-diamond'
import { Seclude } from 'flat-diamond'

class Plane {
wingSpan: number = 200
}

const ProtectedPlane = Protect(Plane, ['wingSpan'])
const SecludedPlane = Seclude(Plane, ['wingSpan'])

class DuckCourier extends ProtectedPlane {
class DuckCourier extends SecludedPlane {
wingSpan: number = 80
get isDeviceSafe(): boolean {
return ProtectedPlane.privatePart(this).wingSpan > 2 * this.wingSpan
return SecludedPlane.privatePart(this).wingSpan > 2 * this.wingSpan
}
}
```

## Limitations

Again, the object exposed in the constructor won't be the same as the one faces from inside the protected object' methods/accessors
Again, the object exposed in the constructor won't be the same as the one faces from inside the secluded object' methods/accessors

For now, only the fields can be protected, not the methods
For now, only the fields can be secluded, not the methods

# Participation

Expand Down
21 changes: 21 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,24 @@ export function instanceOf(obj: any, ctor: Ctor) {
if (base === ctor || base.prototype instanceof ctor) return true
return false
}

type Method<Class extends Ctor, Args extends any[], Return> = (
this: InstanceType<Class>,
...args: Args
) => Return

/**
*
* @param target
* @param factory
* @deprecated Not deprecated but incomplete - just pushed away with other changes : *unreliable!*
*/
export function overrideMethods<Class extends Ctor, Args extends any[], Return>(
target: Class,
factory: (source: InstanceType<Class>) => Record<PropertyKey, Method<Class, Args, Return>>
) {
Object.defineProperties(
target.prototype,
Object.getOwnPropertyDescriptors(factory(Object.getPrototypeOf(target.prototype)))
)
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Diamond from './diamond'
export default Diamond
export * from './helpers'
export * from './protect'
export * from './seclude'
export * from './types'
26 changes: 13 additions & 13 deletions src/protect.ts → src/seclude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,40 @@ import { allFLegs, bottomLeg, fLegs, nextInLine } from './utils'

const publicPart = (x: Ctor): Ctor => Object.getPrototypeOf(Object.getPrototypeOf(x))

export type Prutected<TBase extends Ctor, Keys extends (keyof InstanceType<TBase>)[]> = Newable<
export type Secluded<TBase extends Ctor, Keys extends (keyof InstanceType<TBase>)[]> = Newable<
Omit<InstanceType<TBase>, Keys[number]>
> & {
privatePart(obj: InstanceType<TBase>): InstanceType<TBase> | undefined
}
export function Protect<TBase extends Ctor, Keys extends (keyof InstanceType<TBase>)[]>(
export function Seclude<TBase extends Ctor, Keys extends (keyof InstanceType<TBase>)[]>(
base: TBase,
properties: Keys
): Prutected<TBase, Keys> {
const protectedProperties: KeySet = properties.reduce(
): Secluded<TBase, Keys> {
const secludedProperties: KeySet = properties.reduce(
(acc, p) => ({ ...acc, [p]: true }) as KeySet,
{}
),
initPropertiesBasket: PropertyDescriptorMap[] = []
/**
* In order to integrate well in diamonds, we need to be a diamond
* When we create a diamond between the Protected and the base, the private properties of the base *have to*
* When we create a diamond between the Secludeded and the base, the private properties of the base *have to*
* be collected before the diamond propagate them to the `constructedObject`
*/
abstract class PropertyCollector extends base {
constructor(...args: any[]) {
super(...args)
const init = initPropertiesBasket[0],
allProps = Object.getOwnPropertyDescriptors(this)
for (const p in protectedProperties)
for (const p in secludedProperties)
if (p in allProps) {
init[p] = allProps[p]
delete this[p]
}
}
}
const privates = new WeakMap<Protected, TBase>()
const privates = new WeakMap<Secluded, TBase>()
const diamond = fLegs(base) ? PropertyCollector : (Diamond(PropertyCollector) as TBase)
class Protected extends (diamond as any) {
class Secluded extends (diamond as any) {
static privatePart(obj: TBase): TBase | undefined {
return privates.get(obj)
}
Expand All @@ -57,7 +57,7 @@ export function Protect<TBase extends Ctor, Keys extends (keyof InstanceType<TBa
// 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, {
Object.defineProperty(p in secludedProperties ? receiver : target, p, {
value,
writable: true,
enumerable: true,
Expand Down Expand Up @@ -130,7 +130,7 @@ export function Protect<TBase extends Ctor, Keys extends (keyof InstanceType<TBa
// No legacy involved: it was well defined in our classes but `readable: false` ...
return undefined
}
if (p in protectedProperties && actor.domain === 'private')
if (p in secludedProperties && 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)
Expand All @@ -148,7 +148,7 @@ export function Protect<TBase extends Ctor, Keys extends (keyof InstanceType<TBa
if (!pd.writable) return false
}

if (p in protectedProperties && actor.domain === 'private') {
if (p in secludedProperties && actor.domain === 'private') {
Object.defineProperty(receiver, p, {
value,
writable: true,
Expand All @@ -169,6 +169,6 @@ export function Protect<TBase extends Ctor, Keys extends (keyof InstanceType<TBa
},
getPrototypeOf: (target) => diamond.prototype
})
Object.setPrototypeOf(Protected.prototype, fakeCtor.prototype)
return Protected as any
Object.setPrototypeOf(Secluded.prototype, fakeCtor.prototype)
return Secluded as any
}
4 changes: 0 additions & 4 deletions test/dynamic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ class A {
}

class B extends Diamond(A) {
constructor(...args: any[]) {
const [myArg, ...rest] = args
super(...rest)
}
method(x: number) {
return super.method(x + 2)
}
Expand Down
8 changes: 4 additions & 4 deletions test/protect.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Diamond, { Protect } from '../src'
import Diamond, { Seclude } from '../src'

interface Scenario {
pubFld: number
Expand Down Expand Up @@ -56,7 +56,7 @@ test('leg-less', () => {
this.prvFld = v
}
}
const P = Protect(X, ['prvFld'])
const P = Seclude(X, ['prvFld'])
class Y extends P {
prvFld = 10
}
Expand Down Expand Up @@ -88,7 +88,7 @@ test('leg-half', () => {
this.prvFld = v
}
}
const P = Protect(X, ['prvFld'])
const P = Seclude(X, ['prvFld'])
class Y {
prvFld = 10
}
Expand Down Expand Up @@ -123,7 +123,7 @@ test('leg-full', () => {
this.prvFld = v
}
}
const P = Protect(X, ['prvFld'])
const P = Seclude(X, ['prvFld'])
class Y {
prvFld = 10
}
Expand Down
38 changes: 38 additions & 0 deletions test/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { overrideMethods } from '~/helpers'
import { log, logs } from './logger'

beforeEach(() => {
logs()
})
test('overrideMethods', () => {
class A {
method() {
log('A', 'method')
}
get value() {
return 42
}
}
class B extends A {
method() {
log('B', 'method')
super.method()
}
get value() {
return 5 + super.value
}
}
const b = new B()
b.method()
expect(logs()).toEqual(['B method', 'A method'])
expect(b.value).toBe(47)
overrideMethods(B, (source) => ({
method() {
log('B', 'modified method')
source.method()
} /*
get value() {
return 15 + source.value
}*/
}))
})

0 comments on commit 64ffefe

Please sign in to comment.