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
2 changes: 1 addition & 1 deletion src/utils/codemirror.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import lodash from "lodash";
export const refreshCodeMirror = (containerId) => {
const container = document.getElementById(containerId);
const editors = container.getElementsByClassName("CodeMirror");
lodash.each(editors, (cm) => cm.CodeMirror.refresh());
lodash.forEach(editors, (cm) => cm.CodeMirror.refresh());
};
4 changes: 4 additions & 0 deletions src/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
sortKeysDeepForObject,
stringifyObject,
} from "./object";
import { getSchemaWithDependencies } from "./schemas";
import { getSearchQuerySelector } from "./selector";
import {
convertArabicToRoman,
Expand All @@ -28,6 +29,7 @@ import {
removeNewLinesAndExtraSpaces,
toFixedLocale,
} from "./str";
import { mapTree } from "./tree";
import { containsEncodedComponents } from "./url";
import { getUUID } from "./uuid";

Expand Down Expand Up @@ -67,4 +69,6 @@ export {
addUnit,
removeUnit,
replaceUnit,
mapTree,
getSchemaWithDependencies,
};
102 changes: 102 additions & 0 deletions src/utils/schemas.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import lodash from "lodash";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to load only lodash functions that we actually need in the module, not a whole lodash library.
https://stackoverflow.com/questions/35250500/correct-way-to-import-lodash

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point! I adjusted it for all lodash imports in the repo

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@k0stik - the idea is good, provided that readability doesn't suffer, but let's deal with it in a separate task, pls see #40 (comment)


import { JSONSchemasInterface } from "../JSONSchemasInterface";

export const schemas = {};
Expand All @@ -19,3 +21,103 @@ export function getSchemaByClassName(className) {
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";
}
}

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`)
* @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: nodes.map((node) => {
return {
properties: {
[parentKey]: {
...getEnumValues([node]),
...getEnumNames([node]),
},
[childKey]: {
...getEnumValues(node.children),
...getEnumNames(node.children),
},
},
...buildDependencies(node.children),
};
}),
},
},
};
}

/**
* 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 = 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]: {
...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),
};
}
16 changes: 16 additions & 0 deletions src/utils/tree.js
Original file line number Diff line number Diff line change
@@ -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;
});
}
184 changes: 184 additions & 0 deletions tests/utils.schemas.tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { expect } from "chai";

import { buildDependencies, getSchemaWithDependencies, typeofSchema } 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",
},
},
},
],
},
],
};
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]);

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");
});

it("can be created with dependencies from schema", () => {
const rjsfSchema = getSchemaWithDependencies({
schema: DFT_SCHEMA,
nodes: [TREE],
});
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", () => {
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");
});
});
31 changes: 31 additions & 0 deletions tests/utils.tree.tests.js
Original file line number Diff line number Diff line change
@@ -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");
});
});