Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions src/utils/class.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,28 +37,28 @@ export function extendClassStaticProps(childClass, parentClass, excludedProps =
}

/**
* Extends an object with a parent class namespace.
* Slightly different implementation of extendClass assuming excludedProps
* is contained within the child-most class definition and assigning only
* the most recent props rather than the most distant props.
* See extendClass.
* TODO : match call signature with extendClass and
* make use of getPrototypeOf rather than __proto__
* but verify correctness in additional use cases first
*/
export function extendThis(child, childClass, parentClass, excludedProps = [], ...args) {
let props;
let obj = new parentClass.prototype.constructor(...args);
export function extendThis(childClass, parentClass, config) {
let props, protos;
let obj = new parentClass.prototype.constructor(config);
const exclude = ["constructor", ...Object.getOwnPropertyNames(childClass.prototype)];
const seen = []; // remember most recent occurrence of prop name (like inheritance)
while (obj.__proto__) {
props = Object.getOwnPropertyNames(obj.__proto__);
while (Object.getPrototypeOf(obj)) {
protos = Object.getPrototypeOf(obj);
props = Object.getOwnPropertyNames(protos);
props.filter(p => !exclude.includes(p)).map((prop) => {
if (seen.includes(prop)) return;
const getter = obj.__lookupGetter__(prop);
const setter = obj.__lookupSetter__(prop);
if (getter) child.__defineGetter__(prop, getter);
if (setter) child.__defineSetter__(prop, setter);
if (!(getter || setter)) child[prop] = obj[prop];
const getter = protos.__lookupGetter__(prop);
const setter = protos.__lookupSetter__(prop);
if (getter) childClass.prototype.__defineGetter__(prop, getter);
if (setter) childClass.prototype.__defineSetter__(prop, setter);
if (!(getter || setter)) childClass.prototype[prop] = protos[prop];
seen.push(prop);
})
obj = obj.__proto__;
obj = protos;
}
}
133 changes: 133 additions & 0 deletions tests/utils.class.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { mix } from "mixwith";
import { expect } from "chai";
import { InMemoryEntity, NamedInMemoryEntity, DefaultableMixin, RuntimeItemsMixin } from "../src/entity";
import { deepClone } from "../src/utils/clone";
import { extendClass, extendThis } from "../src/utils/class";


class BaseEntity extends mix(InMemoryEntity).with(RuntimeItemsMixin) {

constructor(config) {
super(config);
}

baseMethod() {
return "base";
}

}


class ExtendClassEntity extends mix(NamedInMemoryEntity).with(DefaultableMixin) {

constructor(config, excluded = []) {
super(config);
extendClass(ExtendClassEntity, BaseEntity, excluded, [config]);

}

baseMethod() {
return "derived";
}

}


class BaseBetweenEntity extends NamedInMemoryEntity {

static staticAttr = "base";

constructor(config) {
super(config);
this.instanceAttr = "base";
}

betweenMethod() {
return "base";
}

}


class BetweenEntity extends BaseBetweenEntity {

static staticAttr = "between";

constructor(config) {
super(config);
this.instanceAttr = "between";
}

betweenMethod() {
return "between";
}
}


class ExtendThisEntity extends mix(BetweenEntity).with(DefaultableMixin) {

constructor(config, excluded = []) {
super(config);
extendThis(ExtendThisEntity, BaseEntity, config);

}

baseMethod() {
return "derived";
}

}


describe("extendClass", () => {

it("extends classes no excluded props", () => {
const obj = new ExtendClassEntity({});
expect(obj.baseMethod()).to.be.equal("base");
});

it("should support excluded props but doesnt", () => {
const obj = new ExtendClassEntity({});
expect(obj.baseMethod()).not.to.be.equal("derived");
});

it("should have results but doesnt", () => {
const obj = new ExtendClassEntity({"results": ["test"]});
expect(JSON.stringify(obj.results)).not.to.be.equal(JSON.stringify([{"name": "test"}]));
});

});


describe("extendThis", () => {

it("extends this prefer child method", () => {
const obj = new ExtendThisEntity({});
expect(obj.baseMethod()).to.be.equal("derived");
});

it("extends this support base mixins", () => {
const obj = new ExtendThisEntity({"results": ["test"]});
expect(JSON.stringify(obj.results)).to.be.equal(JSON.stringify([{"name": "test"}]));
});

it("remembers intermediate methods", () => {
const base = new BaseBetweenEntity();
expect(base.betweenMethod()).to.be.equal("base");
const obj = new ExtendThisEntity({});
expect(obj.betweenMethod()).to.be.equal("between");
});

it("propagates instance attributes", () => {
const base = new BaseBetweenEntity({});
expect(base.instanceAttr).to.be.equal("base");
const obj = new ExtendThisEntity({});
expect(obj.instanceAttr).to.be.equal("between");
});

it("propagates static attributes", () => {
expect(BaseBetweenEntity.staticAttr).to.be.equal("base");
expect(ExtendThisEntity.staticAttr).to.be.equal("between");
});

});