From 1285b4e207d921aba8cef98c156cfea45e25c96a Mon Sep 17 00:00:00 2001 From: Alexander Zech Date: Tue, 3 Jan 2023 16:29:28 +0100 Subject: [PATCH 01/16] chore: add draft for function to build dependency tree --- src/utils/schemas.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/utils/schemas.js b/src/utils/schemas.js index bb6c409d..73cffebb 100644 --- a/src/utils/schemas.js +++ b/src/utils/schemas.js @@ -19,3 +19,47 @@ export function getSchemaByClassName(className) { export function registerClassName(className, schemaId) { schemas[className] = schemaId; } + +function buildCase({ + parentKey, + parentValue, + childKey, + childValues, + dependencies = {}, + extraFields = {}, +}) { + return { + properties: { + [parentKey]: { + enum: [parentValue], + ...extraFields.get(parentKey, {}), + }, + [childKey]: { + enum: childValues, + ...extraFields.get(childKey, {}), + }, + }, + ...dependencies, + }; +} + +export function buildDependencies(parent, children) { + if (children.length === 0 || children.every((n) => !n.children?.length)) return {}; + const parentKey = parent.data.selector; + const childKey = children[0].data.selector; + return { + dependencies: { + [parentKey]: { + oneOf: children.map((node) => + buildCase({ + parentKey, + parentValue: parent.data[parentKey], + childKey, + childValues: node.options, + dependencies: buildDependencies(node, node.children), + }), + ), + }, + }, + }; +} From d58f78732cdc079ab3444b98876f334a7857807e Mon Sep 17 00:00:00 2001 From: Alexander Zech Date: Tue, 17 Jan 2023 17:39:29 -0800 Subject: [PATCH 02/16] chore: adjust dependency builder to support dataSelector node property --- src/utils/schemas.js | 51 +++++++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/src/utils/schemas.js b/src/utils/schemas.js index 73cffebb..89e2cce8 100644 --- a/src/utils/schemas.js +++ b/src/utils/schemas.js @@ -1,3 +1,5 @@ +import lodash from "lodash"; + import { JSONSchemasInterface } from "../JSONSchemasInterface"; export const schemas = {}; @@ -20,7 +22,19 @@ export function registerClassName(className, schemaId) { schemas[className] = schemaId; } -function buildCase({ +/** + * @summary Build dependency case (i.e. subschema) for RJSF dependency block. + * + * Note that this function assumes the subschema to be of type `"object"`. + * @param {String} parentKey - Key of fixed property + * @param {any} parentValue - Value of fixed property + * @param {String} childKey - Key of variable property + * @param {Array} childValues - Array of values of variable property + * @param {Object} dependencies - dependencies block for variable property + * @param {Object} extraFields - extra fields for each property + * @returns {{properties: {}}} + */ +function buildDependencyCase({ parentKey, parentValue, childKey, @@ -32,31 +46,44 @@ function buildCase({ properties: { [parentKey]: { enum: [parentValue], - ...extraFields.get(parentKey, {}), + ...(extraFields[parentKey] || {}), }, [childKey]: { enum: childValues, - ...extraFields.get(childKey, {}), + ...(extraFields[childKey] || {}), }, }, ...dependencies, }; } -export function buildDependencies(parent, children) { - if (children.length === 0 || children.every((n) => !n.children?.length)) return {}; - const parentKey = parent.data.selector; - const childKey = children[0].data.selector; +/** + * @summary Recursively generate `dependencies` for RJSF schema based on tree. + * @param {Object[]} nodes - Array of nodes (e.g. `[tree]` or `node.children`) + * @returns {{}|{dependencies: {}}} + */ +export function buildDependencies(nodes) { + if (nodes.length === 0 || nodes.every((n) => !n.children?.length)) return {}; + const parentKey = nodes[0].dataSelector.key; + const childKey = nodes[0].children[0].dataSelector.key; return { dependencies: { [parentKey]: { - oneOf: children.map((node) => - buildCase({ + oneOf: nodes.map((node) => + buildDependencyCase({ parentKey, - parentValue: parent.data[parentKey], + parentValue: lodash.get(node, node.dataSelector.value), childKey, - childValues: node.options, - dependencies: buildDependencies(node, node.children), + childValues: node.children.map((c) => lodash.get(c, c.dataSelector.value)), + dependencies: buildDependencies(node.children), + extraFields: { + [parentKey]: { enumNames: [lodash.get(node, node.dataSelector.name)] }, + [childKey]: { + enumNames: node.children.map((c) => + lodash.get(c, c.dataSelector.name), + ), + }, + }, }), ), }, From 50daeb9a9d4eeab7a25018864d8d740f722b6213 Mon Sep 17 00:00:00 2001 From: Alexander Zech Date: Tue, 17 Jan 2023 17:40:01 -0800 Subject: [PATCH 03/16] chore: add utility function for calling map on a tree --- src/utils/tree.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/utils/tree.js diff --git a/src/utils/tree.js b/src/utils/tree.js new file mode 100644 index 00000000..e09af340 --- /dev/null +++ b/src/utils/tree.js @@ -0,0 +1,16 @@ +/** + * @summary Return nodes with `fn` function applied to each node. + * Note that the function `fn` must take a node as an argument and must return a node object. + * @param {Object[]} nodes - Array of nodes + * @param {Function} fn - function to be applied to each node + * @returns {Object[]} - Result of map + */ +export function mapTree(nodes, fn) { + return nodes.map((node) => { + const mappedNode = fn(node); + if (node?.children?.length) { + mappedNode.children = mapTree(node.children, fn); + } + return mappedNode; + }); +} From 8541e6aa6e6b0876fc70133933ad65b3e4da0b6a Mon Sep 17 00:00:00 2001 From: Alexander Zech Date: Tue, 17 Jan 2023 17:40:39 -0800 Subject: [PATCH 04/16] test: add test for buildDependencies --- tests/utils.schemas.tests.js | 125 +++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 tests/utils.schemas.tests.js diff --git a/tests/utils.schemas.tests.js b/tests/utils.schemas.tests.js new file mode 100644 index 00000000..cf1de2eb --- /dev/null +++ b/tests/utils.schemas.tests.js @@ -0,0 +1,125 @@ +import { expect } from "chai"; + +import { buildDependencies } from "../src/utils/schemas"; + +describe("RJSF schema", () => { + const tree = { + path: "/dft", + dataSelector: { key: "type", value: "data.type.slug", name: "data.type.name" }, + data: { + type: { + slug: "dft", + name: "Density Functional Theory", + }, + }, + children: [ + { + path: "/dft/lda", + dataSelector: { + key: "subtype", + value: "data.subtype.slug", + name: "data.subtype.name", + }, + data: { + subtype: { + slug: "lda", + name: "LDA", + }, + }, + children: [ + { + path: "/dft/lda/svwn", + dataSelector: { + key: "functional", + value: "data.functional.slug", + name: "data.functional.name", + }, + data: { + functional: { + slug: "svwn", + name: "SVWN", + }, + }, + }, + { + path: "/dft/lda/pz", + dataSelector: { + key: "functional", + value: "data.functional.slug", + name: "data.functional.name", + }, + data: { + functional: { + slug: "pz", + name: "PZ", + }, + }, + }, + ], + }, + { + path: "/dft/gga", + dataSelector: { + key: "subtype", + value: "data.subtype.slug", + name: "data.subtype.name", + }, + data: { + subtype: { + slug: "gga", + name: "GGA", + }, + }, + children: [ + { + path: "/dft/gga/pbe", + dataSelector: { + key: "functional", + value: "data.functional.slug", + name: "data.functional.name", + }, + data: { + functional: { + slug: "pbe", + name: "PBE", + }, + }, + }, + { + path: "/dft/gga/pw91", + dataSelector: { + key: "functional", + value: "data.functional.slug", + name: "data.functional.name", + }, + data: { + functional: { + slug: "pw91", + name: "PW91", + }, + }, + }, + ], + }, + ], + }; + it("can be created with dependencies from tree", () => { + const dependencies = buildDependencies([tree]); + console.log(JSON.stringify(dependencies, null, 4)); + + const [dftCase] = dependencies.dependencies.type.oneOf; + expect(dftCase.properties.subtype.enum).to.have.ordered.members(["lda", "gga"]); + expect(dftCase.properties.subtype.enumNames).to.have.ordered.members(["LDA", "GGA"]); + + const [ldaCase, ggaCase] = dftCase.dependencies.subtype.oneOf; + expect(ldaCase.properties.subtype.enum).to.have.length(1); + expect(ldaCase.properties.functional.enum).to.have.ordered.members(["svwn", "pz"]); + expect(ldaCase.properties.functional.enumNames).to.have.ordered.members(["SVWN", "PZ"]); + expect(ldaCase).to.not.have.property("dependencies"); + + expect(ggaCase.properties.subtype.enum).to.have.length(1); + expect(ggaCase.properties.functional.enum).to.have.ordered.members(["pbe", "pw91"]); + expect(ggaCase.properties.functional.enumNames).to.have.ordered.members(["PBE", "PW91"]); + expect(ggaCase).to.not.have.property("dependencies"); + }); +}); From 1fcb8948c06f6d4365ddcdf30327c4c1c51db7a5 Mon Sep 17 00:00:00 2001 From: Alexander Zech Date: Tue, 17 Jan 2023 17:44:11 -0800 Subject: [PATCH 05/16] chore: add mapTree to utils index --- src/utils/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils/index.js b/src/utils/index.js index b03d0cee..df16a0d6 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -27,6 +27,7 @@ import { removeNewLinesAndExtraSpaces, toFixedLocale, } from "./str"; +import { mapTree } from "./tree"; import { containsEncodedComponents } from "./url"; import { getUUID } from "./uuid"; @@ -61,4 +62,5 @@ export { getSearchQuerySelector, containsEncodedComponents, convertArabicToRoman, + mapTree, }; From 7f65cd488a03b5d5152b722c72e4d61ab0895303 Mon Sep 17 00:00:00 2001 From: Alexander Zech Date: Wed, 18 Jan 2023 17:54:08 -0800 Subject: [PATCH 06/16] chore: add function to determine schema type --- src/utils/schemas.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/utils/schemas.js b/src/utils/schemas.js index 89e2cce8..0f6132a5 100644 --- a/src/utils/schemas.js +++ b/src/utils/schemas.js @@ -22,6 +22,18 @@ export function registerClassName(className, schemaId) { schemas[className] = schemaId; } +export function typeofSchema(schema) { + if (lodash.has(schema, "type")) { + return schema.type; + } + if (lodash.has(schema, "properties")) { + return "object"; + } + if (lodash.has(schema, "items")) { + return "array"; + } +} + /** * @summary Build dependency case (i.e. subschema) for RJSF dependency block. * From 5d6720482067d78f37010b671eb8c9cbd952e82b Mon Sep 17 00:00:00 2001 From: Alexander Zech Date: Wed, 18 Jan 2023 17:57:58 -0800 Subject: [PATCH 07/16] chore: add function for building schema with dependency --- src/utils/schemas.js | 47 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/utils/schemas.js b/src/utils/schemas.js index 0f6132a5..ce75d5f0 100644 --- a/src/utils/schemas.js +++ b/src/utils/schemas.js @@ -69,6 +69,20 @@ function buildDependencyCase({ }; } +function getEnumValues(nodes) { + if (!nodes.length) return {}; + return { + enum: nodes.map((node) => lodash.get(node, node.dataSelector.value)), + }; +} + +function getEnumNames(nodes) { + if (!nodes.length) return {}; + return { + enumNames: nodes.map((node) => lodash.get(node, node.dataSelector.name)), + }; +} + /** * @summary Recursively generate `dependencies` for RJSF schema based on tree. * @param {Object[]} nodes - Array of nodes (e.g. `[tree]` or `node.children`) @@ -102,3 +116,36 @@ export function buildDependencies(nodes) { }, }; } + +export function getSchemaWithDependencies({ + schema = {}, + schemaId, + nodes, + modifyProperties = false, +}) { + const mainSchema = + lodash.isEmpty(schema) && schemaId ? JSONSchemasInterface.schemaById(schemaId) : schema; + + if (!lodash.isEmpty(mainSchema) && typeofSchema(mainSchema) !== "object") { + console.error("getSchemaWithDependencies() only accepts schemas of type 'object'"); + return {}; + } + if (modifyProperties && nodes.length) { + const mod = { + [nodes[0].dataSelector.key]: { + ...getEnumNames(nodes), + ...getEnumValues(nodes), + }, + }; + lodash.forEach(mod, (extraFields, key) => { + if (lodash.has(mainSchema, `properties.${key}`)) { + mainSchema.properties[key] = { ...mainSchema.properties[key], ...extraFields }; + } + }); + } + + return { + ...(schemaId ? mainSchema : schema), + ...buildDependencies(nodes), + }; +} From 84c15ae6e8c573e7aef59838fd94bfe216ec74ca Mon Sep 17 00:00:00 2001 From: Alexander Zech Date: Wed, 18 Jan 2023 18:00:08 -0800 Subject: [PATCH 08/16] refactor: buildDependencyCase function --- src/utils/schemas.js | 61 +++++++++----------------------------------- 1 file changed, 12 insertions(+), 49 deletions(-) diff --git a/src/utils/schemas.js b/src/utils/schemas.js index ce75d5f0..89a7328a 100644 --- a/src/utils/schemas.js +++ b/src/utils/schemas.js @@ -34,41 +34,6 @@ export function typeofSchema(schema) { } } -/** - * @summary Build dependency case (i.e. subschema) for RJSF dependency block. - * - * Note that this function assumes the subschema to be of type `"object"`. - * @param {String} parentKey - Key of fixed property - * @param {any} parentValue - Value of fixed property - * @param {String} childKey - Key of variable property - * @param {Array} childValues - Array of values of variable property - * @param {Object} dependencies - dependencies block for variable property - * @param {Object} extraFields - extra fields for each property - * @returns {{properties: {}}} - */ -function buildDependencyCase({ - parentKey, - parentValue, - childKey, - childValues, - dependencies = {}, - extraFields = {}, -}) { - return { - properties: { - [parentKey]: { - enum: [parentValue], - ...(extraFields[parentKey] || {}), - }, - [childKey]: { - enum: childValues, - ...(extraFields[childKey] || {}), - }, - }, - ...dependencies, - }; -} - function getEnumValues(nodes) { if (!nodes.length) return {}; return { @@ -95,23 +60,21 @@ export function buildDependencies(nodes) { return { dependencies: { [parentKey]: { - oneOf: nodes.map((node) => - buildDependencyCase({ - parentKey, - parentValue: lodash.get(node, node.dataSelector.value), - childKey, - childValues: node.children.map((c) => lodash.get(c, c.dataSelector.value)), - dependencies: buildDependencies(node.children), - extraFields: { - [parentKey]: { enumNames: [lodash.get(node, node.dataSelector.name)] }, + oneOf: nodes.map((node) => { + return { + properties: { + [parentKey]: { + ...getEnumValues([node]), + ...getEnumNames([node]), + }, [childKey]: { - enumNames: node.children.map((c) => - lodash.get(c, c.dataSelector.name), - ), + ...getEnumValues(node.children), + ...getEnumNames(node.children), }, }, - }), - ), + ...buildDependencies(node.children), + }; + }), }, }, }; From 57254228fb454c4cd375a32f634643cadc984ed6 Mon Sep 17 00:00:00 2001 From: Alexander Zech Date: Wed, 18 Jan 2023 18:02:00 -0800 Subject: [PATCH 09/16] test: getSchemaWithDependencies --- tests/utils.schemas.tests.js | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/tests/utils.schemas.tests.js b/tests/utils.schemas.tests.js index cf1de2eb..0879b121 100644 --- a/tests/utils.schemas.tests.js +++ b/tests/utils.schemas.tests.js @@ -1,6 +1,6 @@ import { expect } from "chai"; -import { buildDependencies } from "../src/utils/schemas"; +import { buildDependencies, getSchemaWithDependencies } from "../src/utils/schemas"; describe("RJSF schema", () => { const tree = { @@ -103,9 +103,23 @@ describe("RJSF schema", () => { }, ], }; - it("can be created with dependencies from tree", () => { + const DFT_SCHEMA = { + type: "object", + properties: { + type: { + type: "string", + }, + subtype: { + type: "string", + }, + functional: { + type: "string", + }, + }, + }; + + it("dependencies block can be created from tree", () => { const dependencies = buildDependencies([tree]); - console.log(JSON.stringify(dependencies, null, 4)); const [dftCase] = dependencies.dependencies.type.oneOf; expect(dftCase.properties.subtype.enum).to.have.ordered.members(["lda", "gga"]); @@ -122,4 +136,15 @@ describe("RJSF schema", () => { expect(ggaCase.properties.functional.enumNames).to.have.ordered.members(["PBE", "PW91"]); expect(ggaCase).to.not.have.property("dependencies"); }); + + it("can be created with dependencies from schema", () => { + const rjsfSchema = getSchemaWithDependencies({ + schema: DFT_SCHEMA, + nodes: [tree], + }); + // console.log(JSON.stringify(rjsfSchema, null, 4)); + expect(rjsfSchema.type).to.be.eql(DFT_SCHEMA.type); + expect(rjsfSchema.properties).to.be.eql(DFT_SCHEMA.properties); + expect(rjsfSchema).to.have.property("dependencies"); + }); }); From dc4dce39615af27cc936e7db6150e77e84822030 Mon Sep 17 00:00:00 2001 From: Alexander Zech Date: Wed, 18 Jan 2023 18:02:28 -0800 Subject: [PATCH 10/16] test: typeofSchema --- tests/utils.schemas.tests.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/utils.schemas.tests.js b/tests/utils.schemas.tests.js index 0879b121..3f6c185c 100644 --- a/tests/utils.schemas.tests.js +++ b/tests/utils.schemas.tests.js @@ -1,6 +1,6 @@ import { expect } from "chai"; -import { buildDependencies, getSchemaWithDependencies } from "../src/utils/schemas"; +import { buildDependencies, getSchemaWithDependencies, typeofSchema } from "../src/utils/schemas"; describe("RJSF schema", () => { const tree = { @@ -148,3 +148,23 @@ describe("RJSF schema", () => { expect(rjsfSchema).to.have.property("dependencies"); }); }); + +describe("Schema utility", () => { + const schemas = [ + ["string", { type: "string" }], + ["integer", { type: "integer" }], + ["number", { type: "number" }], + ["object", { type: "object" }], + ["array", { type: "array" }], + ]; + const objSchemaNoType = { properties: { name: { type: "string" } } }; + const arraySchemaNoType = { items: { type: "number" } }; + it("type can be determined correctly", () => { + schemas.forEach(([type, schema]) => { + const currentType = typeofSchema(schema); + expect(currentType).to.be.equal(type); + }); + expect(typeofSchema(objSchemaNoType)).to.be.equal("object"); + expect(typeofSchema(arraySchemaNoType)).to.be.equal("array"); + }); +}); From e987abc316e30c5e4a3edfa35a1bb72d0219ef21 Mon Sep 17 00:00:00 2001 From: Alexander Zech Date: Wed, 18 Jan 2023 18:03:03 -0800 Subject: [PATCH 11/16] test: mapTree --- tests/utils.tree.tests.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/utils.tree.tests.js diff --git a/tests/utils.tree.tests.js b/tests/utils.tree.tests.js new file mode 100644 index 00000000..51bd1db7 --- /dev/null +++ b/tests/utils.tree.tests.js @@ -0,0 +1,31 @@ +import { expect } from "chai"; + +import { mapTree } from "../src/utils"; + +describe("Tree data structure", () => { + const TREE = { + path: "/A", + children: [ + { + path: "/A/B", + children: [ + { + path: "/A/B/C", + }, + ], + }, + { + path: "/A/D", + }, + ], + }; + it("map", () => { + const [mappedTree] = mapTree([TREE], (node) => { + return { ...node, foo: "bar" }; + }); + expect(mappedTree).to.have.property("foo", "bar"); + expect(mappedTree.children[0]).to.have.property("foo", "bar"); + expect(mappedTree.children[0].children[0]).to.have.property("foo", "bar"); + expect(mappedTree.children[1]).to.have.property("foo", "bar"); + }); +}); From 5f2925bbbd47c6d7dc479c3320b1da20fd619ab6 Mon Sep 17 00:00:00 2001 From: Alexander Zech Date: Wed, 18 Jan 2023 18:22:42 -0800 Subject: [PATCH 12/16] docs: add docstring for getSchemaWithDependencies --- src/utils/schemas.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/utils/schemas.js b/src/utils/schemas.js index 89a7328a..f1ed2a79 100644 --- a/src/utils/schemas.js +++ b/src/utils/schemas.js @@ -80,19 +80,28 @@ export function buildDependencies(nodes) { }; } +/** + * Combine schema and dependencies block for usage with react-jsonschema-form (RJSF) + * @param {Object} schema - Schema + * @param {String} schemaId - Schema id (takes precedence over `schema` when both are provided) + * @param {Object[]} nodes - Array of nodes + * @param {Boolean} modifyProperties - Whether properties in main schema should be modified (add `enum` and `enumNames`) + * @returns {{}|{[p: string]: *}} - RJSF schema + */ export function getSchemaWithDependencies({ schema = {}, schemaId, nodes, modifyProperties = false, }) { - const mainSchema = - lodash.isEmpty(schema) && schemaId ? JSONSchemasInterface.schemaById(schemaId) : schema; + const mainSchema = schemaId ? JSONSchemasInterface.schemaById(schemaId) : schema; if (!lodash.isEmpty(mainSchema) && typeofSchema(mainSchema) !== "object") { console.error("getSchemaWithDependencies() only accepts schemas of type 'object'"); return {}; } + + // RJSF does not automatically render dropdown widget if `enum` is not present if (modifyProperties && nodes.length) { const mod = { [nodes[0].dataSelector.key]: { From 0fe054853674e42a20bcb7c32ad2656f2b1ecb1a Mon Sep 17 00:00:00 2001 From: Alexander Zech Date: Wed, 18 Jan 2023 18:23:39 -0800 Subject: [PATCH 13/16] test: adding enum dynamically to main schema --- tests/utils.schemas.tests.js | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/tests/utils.schemas.tests.js b/tests/utils.schemas.tests.js index 3f6c185c..584e7b40 100644 --- a/tests/utils.schemas.tests.js +++ b/tests/utils.schemas.tests.js @@ -3,7 +3,7 @@ import { expect } from "chai"; import { buildDependencies, getSchemaWithDependencies, typeofSchema } from "../src/utils/schemas"; describe("RJSF schema", () => { - const tree = { + const TREE = { path: "/dft", dataSelector: { key: "type", value: "data.type.slug", name: "data.type.name" }, data: { @@ -119,7 +119,7 @@ describe("RJSF schema", () => { }; it("dependencies block can be created from tree", () => { - const dependencies = buildDependencies([tree]); + const dependencies = buildDependencies([TREE]); const [dftCase] = dependencies.dependencies.type.oneOf; expect(dftCase.properties.subtype.enum).to.have.ordered.members(["lda", "gga"]); @@ -140,13 +140,27 @@ describe("RJSF schema", () => { it("can be created with dependencies from schema", () => { const rjsfSchema = getSchemaWithDependencies({ schema: DFT_SCHEMA, - nodes: [tree], + nodes: [TREE], }); - // console.log(JSON.stringify(rjsfSchema, null, 4)); expect(rjsfSchema.type).to.be.eql(DFT_SCHEMA.type); expect(rjsfSchema.properties).to.be.eql(DFT_SCHEMA.properties); expect(rjsfSchema).to.have.property("dependencies"); }); + + it("enum and enumNames can be added to schema properties", () => { + const rjsfSchema = getSchemaWithDependencies({ + schema: DFT_SCHEMA, + nodes: [TREE], + modifyProperties: true, + }); + // console.log(JSON.stringify(rjsfSchema, null, 4)); + expect(rjsfSchema.type).to.be.eql(DFT_SCHEMA.type); + expect(rjsfSchema.properties.type).to.have.property("enum"); + expect(rjsfSchema.properties.type.enum).to.be.eql(["dft"]); + expect(rjsfSchema.properties.type).to.have.property("enumNames"); + expect(rjsfSchema.properties.type.enumNames).to.be.eql(["Density Functional Theory"]); + expect(rjsfSchema).to.have.property("dependencies"); + }); }); describe("Schema utility", () => { From 2003cba670b15b22ca4d1d0502c122b4bf4b2739 Mon Sep 17 00:00:00 2001 From: Alexander Zech Date: Thu, 19 Jan 2023 15:39:10 -0800 Subject: [PATCH 14/16] chore: add getSchemaWithDependencies to index --- src/utils/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils/index.js b/src/utils/index.js index df16a0d6..68b9cb94 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -18,6 +18,7 @@ import { sortKeysDeepForObject, stringifyObject, } from "./object"; +import { getSchemaWithDependencies } from "./schemas"; import { getSearchQuerySelector } from "./selector"; import { convertArabicToRoman, @@ -63,4 +64,5 @@ export { containsEncodedComponents, convertArabicToRoman, mapTree, + getSchemaWithDependencies, }; From 9baa4ebc39d3e8c84460478cf39e58fbe1d3edbb Mon Sep 17 00:00:00 2001 From: Alexander Zech Date: Mon, 23 Jan 2023 14:47:23 -0800 Subject: [PATCH 15/16] chore: use single lodash imports --- src/context/provider.js | 13 +++++++------ src/entity/in_memory.js | 7 ++++--- src/utils/array.js | 4 ++-- src/utils/codemirror.js | 4 ++-- src/utils/object.js | 21 +++++++++++++-------- src/utils/schemas.js | 21 ++++++++++++--------- 6 files changed, 40 insertions(+), 30 deletions(-) diff --git a/src/context/provider.js b/src/context/provider.js index 1170dd3e..2e1000f0 100644 --- a/src/context/provider.js +++ b/src/context/provider.js @@ -9,7 +9,8 @@ * to next one, for example data about material to track when it is changed. * @notes Should hold static data only (see `setData` method), no classes or functions */ -import lodash from "lodash"; +import capitalize from "lodash/capitalize"; +import get from "lodash/get"; import { deepClone } from "../utils/clone"; @@ -43,9 +44,9 @@ export class ContextProvider { } static createConfigFromContext(config) { - const data = lodash.get(config.context, config.name); - const isEdited = lodash.get(config.context, this.getIsEditedKeyByName(config.name)); - const extraData = lodash.get(config.context, this.getExtraDataKeyByName(config.name)); + const data = get(config.context, config.name); + const isEdited = get(config.context, this.getIsEditedKeyByName(config.name)); + const extraData = get(config.context, this.getExtraDataKeyByName(config.name)); return Object.assign( config, data @@ -106,11 +107,11 @@ export class ContextProvider { } get isEditedKey() { - return `is${lodash.capitalize(this.name)}Edited`; + return `is${capitalize(this.name)}Edited`; } static getIsEditedKeyByName(name) { - return `is${lodash.capitalize(name)}Edited`; + return `is${capitalize(name)}Edited`; } get isUnitContextProvider() { diff --git a/src/entity/in_memory.js b/src/entity/in_memory.js index 3e4b5930..219a9e8e 100644 --- a/src/entity/in_memory.js +++ b/src/entity/in_memory.js @@ -1,4 +1,5 @@ -import lodash from "lodash"; +import get from "lodash/get"; +import omit from "lodash/omit"; // import { ESSE } from "@exabyte-io/esse.js"; import { deepClone } from "../utils/clone"; @@ -24,7 +25,7 @@ export class InMemoryEntity { */ prop(name, defaultValue = null) { // `lodash.get` gets `null` when the value is `null`, but we still want a default value in this case, hence `||` - return lodash.get(this._json, name, defaultValue) || defaultValue; + return get(this._json, name, defaultValue) || defaultValue; } /** @@ -49,7 +50,7 @@ export class InMemoryEntity { * @param exclude {String[]} */ toJSON(exclude = []) { - const config = deepClone(lodash.omit(this._json, exclude)); + const config = deepClone(omit(this._json, exclude)); return this.clean(config); } diff --git a/src/utils/array.js b/src/utils/array.js index 815c0b4a..a6a74b3d 100644 --- a/src/utils/array.js +++ b/src/utils/array.js @@ -1,8 +1,8 @@ -import lodash from "lodash"; +import isArray from "lodash/isArray"; import _ from "underscore"; export function safeMakeArray(x) { - if (!lodash.isArray(x)) return [x]; + if (!isArray(x)) return [x]; return x; } diff --git a/src/utils/codemirror.js b/src/utils/codemirror.js index face68c1..351d6384 100644 --- a/src/utils/codemirror.js +++ b/src/utils/codemirror.js @@ -1,7 +1,7 @@ -import lodash from "lodash"; +import forEach from "lodash/forEach"; export const refreshCodeMirror = (containerId) => { const container = document.getElementById(containerId); const editors = container.getElementsByClassName("CodeMirror"); - lodash.each(editors, (cm) => cm.CodeMirror.refresh()); + forEach(editors, (cm) => cm.CodeMirror.refresh()); }; diff --git a/src/utils/object.js b/src/utils/object.js index 5cf2bcde..246117d7 100644 --- a/src/utils/object.js +++ b/src/utils/object.js @@ -1,4 +1,9 @@ -import lodash from "lodash"; +import camelCase from "lodash/camelCase"; +import filter from "lodash/filter"; +import isArray from "lodash/isArray"; +import isObject from "lodash/isObject"; +import isString from "lodash/isString"; +import mapKeys from "lodash/mapKeys"; import _ from "underscore"; import { deepClone } from "./clone"; @@ -11,8 +16,8 @@ import { deepClone } from "./clone"; export function safeMakeObject(name) { if (!name) return; let result = name; - if (lodash.isString(name)) result = { name }; - if (!lodash.isObject(result) || lodash.isArray(result) || !result.name) + if (isString(name)) result = { name }; + if (!isObject(result) || isArray(result) || !result.name) throw new Error(`safeMakeObject: failed creating named object, found ${result}`); return result; } @@ -25,7 +30,7 @@ export function safeMakeObject(name) { * @returns filtered[0] {Any|null} first matching entry if found */ export function getOneMatchFromObject(obj, attribute, value) { - const filtered = lodash.filter(obj, [attribute, value]); + const filtered = filter(obj, [attribute, value]); if (filtered.length !== 1) { console.log(`found ${filtered.length} ${attribute} matching ${value}, expected 1.`); } @@ -41,8 +46,8 @@ export function getOneMatchFromObject(obj, attribute, value) { */ export function convertKeysToCamelCaseForObject(obj) { const newObj = deepClone(obj); - return lodash.mapKeys(newObj, (v, k) => { - return lodash.camelCase(k); + return mapKeys(newObj, (v, k) => { + return camelCase(k); }); } @@ -53,11 +58,11 @@ export function convertKeysToCamelCaseForObject(obj) { * @param {Array} keysRenamed - List of keys to rename original keys to, in the same order */ export function renameKeysForObject(o, keysOriginal = [], keysRenamed = []) { - if (!lodash.isObject(o)) { + if (!isObject(o)) { return o; } - if (lodash.isArray(o)) { + if (isArray(o)) { return o.map((x) => renameKeysForObject(x, keysOriginal, keysRenamed)); } diff --git a/src/utils/schemas.js b/src/utils/schemas.js index f1ed2a79..419564fd 100644 --- a/src/utils/schemas.js +++ b/src/utils/schemas.js @@ -1,4 +1,7 @@ -import lodash from "lodash"; +import forEach from "lodash/forEach"; +import get from "lodash/get"; +import has from "lodash/has"; +import isEmpty from "lodash/isEmpty"; import { JSONSchemasInterface } from "../JSONSchemasInterface"; @@ -23,13 +26,13 @@ export function registerClassName(className, schemaId) { } export function typeofSchema(schema) { - if (lodash.has(schema, "type")) { + if (has(schema, "type")) { return schema.type; } - if (lodash.has(schema, "properties")) { + if (has(schema, "properties")) { return "object"; } - if (lodash.has(schema, "items")) { + if (has(schema, "items")) { return "array"; } } @@ -37,14 +40,14 @@ export function typeofSchema(schema) { function getEnumValues(nodes) { if (!nodes.length) return {}; return { - enum: nodes.map((node) => lodash.get(node, node.dataSelector.value)), + enum: nodes.map((node) => get(node, node.dataSelector.value)), }; } function getEnumNames(nodes) { if (!nodes.length) return {}; return { - enumNames: nodes.map((node) => lodash.get(node, node.dataSelector.name)), + enumNames: nodes.map((node) => get(node, node.dataSelector.name)), }; } @@ -96,7 +99,7 @@ export function getSchemaWithDependencies({ }) { const mainSchema = schemaId ? JSONSchemasInterface.schemaById(schemaId) : schema; - if (!lodash.isEmpty(mainSchema) && typeofSchema(mainSchema) !== "object") { + if (!isEmpty(mainSchema) && typeofSchema(mainSchema) !== "object") { console.error("getSchemaWithDependencies() only accepts schemas of type 'object'"); return {}; } @@ -109,8 +112,8 @@ export function getSchemaWithDependencies({ ...getEnumValues(nodes), }, }; - lodash.forEach(mod, (extraFields, key) => { - if (lodash.has(mainSchema, `properties.${key}`)) { + forEach(mod, (extraFields, key) => { + if (has(mainSchema, `properties.${key}`)) { mainSchema.properties[key] = { ...mainSchema.properties[key], ...extraFields }; } }); From bbed111a53e5037a6ace0686e063bebfbd7feab7 Mon Sep 17 00:00:00 2001 From: Alexander Zech Date: Mon, 30 Jan 2023 10:03:13 -0800 Subject: [PATCH 16/16] revert: single lodash imports --- src/context/provider.js | 13 ++++++------- src/entity/in_memory.js | 7 +++---- src/utils/array.js | 4 ++-- src/utils/codemirror.js | 4 ++-- src/utils/object.js | 21 ++++++++------------- src/utils/schemas.js | 21 +++++++++------------ 6 files changed, 30 insertions(+), 40 deletions(-) diff --git a/src/context/provider.js b/src/context/provider.js index 2e1000f0..1170dd3e 100644 --- a/src/context/provider.js +++ b/src/context/provider.js @@ -9,8 +9,7 @@ * to next one, for example data about material to track when it is changed. * @notes Should hold static data only (see `setData` method), no classes or functions */ -import capitalize from "lodash/capitalize"; -import get from "lodash/get"; +import lodash from "lodash"; import { deepClone } from "../utils/clone"; @@ -44,9 +43,9 @@ export class ContextProvider { } static createConfigFromContext(config) { - const data = get(config.context, config.name); - const isEdited = get(config.context, this.getIsEditedKeyByName(config.name)); - const extraData = get(config.context, this.getExtraDataKeyByName(config.name)); + const data = lodash.get(config.context, config.name); + const isEdited = lodash.get(config.context, this.getIsEditedKeyByName(config.name)); + const extraData = lodash.get(config.context, this.getExtraDataKeyByName(config.name)); return Object.assign( config, data @@ -107,11 +106,11 @@ export class ContextProvider { } get isEditedKey() { - return `is${capitalize(this.name)}Edited`; + return `is${lodash.capitalize(this.name)}Edited`; } static getIsEditedKeyByName(name) { - return `is${capitalize(name)}Edited`; + return `is${lodash.capitalize(name)}Edited`; } get isUnitContextProvider() { diff --git a/src/entity/in_memory.js b/src/entity/in_memory.js index 219a9e8e..3e4b5930 100644 --- a/src/entity/in_memory.js +++ b/src/entity/in_memory.js @@ -1,5 +1,4 @@ -import get from "lodash/get"; -import omit from "lodash/omit"; +import lodash from "lodash"; // import { ESSE } from "@exabyte-io/esse.js"; import { deepClone } from "../utils/clone"; @@ -25,7 +24,7 @@ export class InMemoryEntity { */ prop(name, defaultValue = null) { // `lodash.get` gets `null` when the value is `null`, but we still want a default value in this case, hence `||` - return get(this._json, name, defaultValue) || defaultValue; + return lodash.get(this._json, name, defaultValue) || defaultValue; } /** @@ -50,7 +49,7 @@ export class InMemoryEntity { * @param exclude {String[]} */ toJSON(exclude = []) { - const config = deepClone(omit(this._json, exclude)); + const config = deepClone(lodash.omit(this._json, exclude)); return this.clean(config); } diff --git a/src/utils/array.js b/src/utils/array.js index a6a74b3d..815c0b4a 100644 --- a/src/utils/array.js +++ b/src/utils/array.js @@ -1,8 +1,8 @@ -import isArray from "lodash/isArray"; +import lodash from "lodash"; import _ from "underscore"; export function safeMakeArray(x) { - if (!isArray(x)) return [x]; + if (!lodash.isArray(x)) return [x]; return x; } diff --git a/src/utils/codemirror.js b/src/utils/codemirror.js index 351d6384..10bd1504 100644 --- a/src/utils/codemirror.js +++ b/src/utils/codemirror.js @@ -1,7 +1,7 @@ -import forEach from "lodash/forEach"; +import lodash from "lodash"; export const refreshCodeMirror = (containerId) => { const container = document.getElementById(containerId); const editors = container.getElementsByClassName("CodeMirror"); - forEach(editors, (cm) => cm.CodeMirror.refresh()); + lodash.forEach(editors, (cm) => cm.CodeMirror.refresh()); }; diff --git a/src/utils/object.js b/src/utils/object.js index 246117d7..5cf2bcde 100644 --- a/src/utils/object.js +++ b/src/utils/object.js @@ -1,9 +1,4 @@ -import camelCase from "lodash/camelCase"; -import filter from "lodash/filter"; -import isArray from "lodash/isArray"; -import isObject from "lodash/isObject"; -import isString from "lodash/isString"; -import mapKeys from "lodash/mapKeys"; +import lodash from "lodash"; import _ from "underscore"; import { deepClone } from "./clone"; @@ -16,8 +11,8 @@ import { deepClone } from "./clone"; export function safeMakeObject(name) { if (!name) return; let result = name; - if (isString(name)) result = { name }; - if (!isObject(result) || isArray(result) || !result.name) + if (lodash.isString(name)) result = { name }; + if (!lodash.isObject(result) || lodash.isArray(result) || !result.name) throw new Error(`safeMakeObject: failed creating named object, found ${result}`); return result; } @@ -30,7 +25,7 @@ export function safeMakeObject(name) { * @returns filtered[0] {Any|null} first matching entry if found */ export function getOneMatchFromObject(obj, attribute, value) { - const filtered = filter(obj, [attribute, value]); + const filtered = lodash.filter(obj, [attribute, value]); if (filtered.length !== 1) { console.log(`found ${filtered.length} ${attribute} matching ${value}, expected 1.`); } @@ -46,8 +41,8 @@ export function getOneMatchFromObject(obj, attribute, value) { */ export function convertKeysToCamelCaseForObject(obj) { const newObj = deepClone(obj); - return mapKeys(newObj, (v, k) => { - return camelCase(k); + return lodash.mapKeys(newObj, (v, k) => { + return lodash.camelCase(k); }); } @@ -58,11 +53,11 @@ export function convertKeysToCamelCaseForObject(obj) { * @param {Array} keysRenamed - List of keys to rename original keys to, in the same order */ export function renameKeysForObject(o, keysOriginal = [], keysRenamed = []) { - if (!isObject(o)) { + if (!lodash.isObject(o)) { return o; } - if (isArray(o)) { + if (lodash.isArray(o)) { return o.map((x) => renameKeysForObject(x, keysOriginal, keysRenamed)); } diff --git a/src/utils/schemas.js b/src/utils/schemas.js index 419564fd..f1ed2a79 100644 --- a/src/utils/schemas.js +++ b/src/utils/schemas.js @@ -1,7 +1,4 @@ -import forEach from "lodash/forEach"; -import get from "lodash/get"; -import has from "lodash/has"; -import isEmpty from "lodash/isEmpty"; +import lodash from "lodash"; import { JSONSchemasInterface } from "../JSONSchemasInterface"; @@ -26,13 +23,13 @@ export function registerClassName(className, schemaId) { } export function typeofSchema(schema) { - if (has(schema, "type")) { + if (lodash.has(schema, "type")) { return schema.type; } - if (has(schema, "properties")) { + if (lodash.has(schema, "properties")) { return "object"; } - if (has(schema, "items")) { + if (lodash.has(schema, "items")) { return "array"; } } @@ -40,14 +37,14 @@ export function typeofSchema(schema) { function getEnumValues(nodes) { if (!nodes.length) return {}; return { - enum: nodes.map((node) => get(node, node.dataSelector.value)), + enum: nodes.map((node) => lodash.get(node, node.dataSelector.value)), }; } function getEnumNames(nodes) { if (!nodes.length) return {}; return { - enumNames: nodes.map((node) => get(node, node.dataSelector.name)), + enumNames: nodes.map((node) => lodash.get(node, node.dataSelector.name)), }; } @@ -99,7 +96,7 @@ export function getSchemaWithDependencies({ }) { const mainSchema = schemaId ? JSONSchemasInterface.schemaById(schemaId) : schema; - if (!isEmpty(mainSchema) && typeofSchema(mainSchema) !== "object") { + if (!lodash.isEmpty(mainSchema) && typeofSchema(mainSchema) !== "object") { console.error("getSchemaWithDependencies() only accepts schemas of type 'object'"); return {}; } @@ -112,8 +109,8 @@ export function getSchemaWithDependencies({ ...getEnumValues(nodes), }, }; - forEach(mod, (extraFields, key) => { - if (has(mainSchema, `properties.${key}`)) { + lodash.forEach(mod, (extraFields, key) => { + if (lodash.has(mainSchema, `properties.${key}`)) { mainSchema.properties[key] = { ...mainSchema.properties[key], ...extraFields }; } });