diff --git a/src/entity/in_memory.js b/src/entity/in_memory.js index c3a510be..22293113 100644 --- a/src/entity/in_memory.js +++ b/src/entity/in_memory.js @@ -95,7 +95,12 @@ export class InMemoryEntity { if (!ctx.isValid()) { console.log(JSON.stringify(this.toJSON())); - console.log(ctx.getErrorObject()); + if (ctx.getErrorObject) { + console.log(ctx.getErrorObject()); + } + if (ctx.validationErrors) { + console.log(ctx.validationErrors()); + } } return ctx.isValid(); @@ -135,7 +140,8 @@ export class InMemoryEntity { } /** - * @summary Pluck an entity from a collection by name + * @summary Pluck an entity from a collection by name. + * If no name is provided and no entity has prop isDefault, return the first entity * @param entities {Array} the entities * @param entity {string} the kind of entities * @param name {string} the name of the entity to choose @@ -145,6 +151,7 @@ export class InMemoryEntity { let filtered; if (!name) { filtered = entities.filter(entity => entity.prop("isDefault") === true); + if (!filtered.length) filtered = [entities[0]]; } else { filtered = entities.filter(entity => entity.prop("name") === name); } diff --git a/src/entity/mixins/runtime_items.js b/src/entity/mixins/runtime_items.js index 5bc44bae..d90d8bb4 100644 --- a/src/entity/mixins/runtime_items.js +++ b/src/entity/mixins/runtime_items.js @@ -47,6 +47,13 @@ export const RuntimeItemsMixin = (superclass) => { export const RuntimeItemsUILogicMixin = (superclass) => { return class extends RuntimeItemsMixin(superclass) { + constructor(config) { + super(config); + this._initRuntimeItems( + ["results", "monitors", "preProcessors", "postProcessors"], + config, + ); + } setRuntimeItemsToDefaultValues() { ["results", "monitors", "preProcessors", "postProcessors"].map((name) => diff --git a/src/utils/class.js b/src/utils/class.js index 233b9304..ca1072d9 100644 --- a/src/utils/class.js +++ b/src/utils/class.js @@ -35,3 +35,30 @@ export function extendClassStaticProps(childClass, parentClass, excludedProps = childClass[prop] = parentClass[prop] }); } + +/** + * 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. + */ +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 (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 = 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 = protos; + } +} diff --git a/src/utils/index.js b/src/utils/index.js index e8edca59..32df15cd 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,6 +1,6 @@ import { compareEntitiesInOrderedSetForSorting } from "../entity/set/ordered/utils"; import { safeMakeArray, convertToCompactCSVArrayOfObjects } from "./array"; -import { extendClass, extendClassStaticProps, cloneClass } from "./class"; +import { extendThis, extendClass, extendClassStaticProps, cloneClass } from "./class"; import { deepClone } from "./clone"; import { refreshCodeMirror } from "./codemirror"; import { getProgrammingLanguageFromFileExtension, formatFileSize } from "./file"; @@ -32,6 +32,7 @@ export { convertToCompactCSVArrayOfObjects, cloneClass, + extendThis, extendClass, extendClassStaticProps, diff --git a/tests/utils.class.js b/tests/utils.class.js new file mode 100644 index 00000000..0616d282 --- /dev/null +++ b/tests/utils.class.js @@ -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"); + }); + +});