Skip to content

Commit e5a71be

Browse files
authored
chore/SOF-6428: build dependency tree (#40)
* chore: add draft for function to build dependency tree * chore: adjust dependency builder to support dataSelector node property * chore: add utility function for calling map on a tree * test: add test for buildDependencies * chore: add mapTree to utils index * chore: add function to determine schema type * chore: add function for building schema with dependency * refactor: buildDependencyCase function * test: getSchemaWithDependencies * test: typeofSchema * test: mapTree * docs: add docstring for getSchemaWithDependencies * test: adding enum dynamically to main schema * chore: add getSchemaWithDependencies to index * chore: use single lodash imports * revert: single lodash imports
1 parent fdc94c7 commit e5a71be

File tree

6 files changed

+338
-1
lines changed

6 files changed

+338
-1
lines changed

src/utils/codemirror.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ import lodash from "lodash";
33
export const refreshCodeMirror = (containerId) => {
44
const container = document.getElementById(containerId);
55
const editors = container.getElementsByClassName("CodeMirror");
6-
lodash.each(editors, (cm) => cm.CodeMirror.refresh());
6+
lodash.forEach(editors, (cm) => cm.CodeMirror.refresh());
77
};

src/utils/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
sortKeysDeepForObject,
2020
stringifyObject,
2121
} from "./object";
22+
import { getSchemaWithDependencies } from "./schemas";
2223
import { getSearchQuerySelector } from "./selector";
2324
import {
2425
convertArabicToRoman,
@@ -28,6 +29,7 @@ import {
2829
removeNewLinesAndExtraSpaces,
2930
toFixedLocale,
3031
} from "./str";
32+
import { mapTree } from "./tree";
3133
import { containsEncodedComponents } from "./url";
3234
import { getUUID } from "./uuid";
3335

@@ -67,4 +69,6 @@ export {
6769
addUnit,
6870
removeUnit,
6971
replaceUnit,
72+
mapTree,
73+
getSchemaWithDependencies,
7074
};

src/utils/schemas.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import lodash from "lodash";
2+
13
import { JSONSchemasInterface } from "../JSONSchemasInterface";
24

35
export const schemas = {};
@@ -19,3 +21,103 @@ export function getSchemaByClassName(className) {
1921
export function registerClassName(className, schemaId) {
2022
schemas[className] = schemaId;
2123
}
24+
25+
export function typeofSchema(schema) {
26+
if (lodash.has(schema, "type")) {
27+
return schema.type;
28+
}
29+
if (lodash.has(schema, "properties")) {
30+
return "object";
31+
}
32+
if (lodash.has(schema, "items")) {
33+
return "array";
34+
}
35+
}
36+
37+
function getEnumValues(nodes) {
38+
if (!nodes.length) return {};
39+
return {
40+
enum: nodes.map((node) => lodash.get(node, node.dataSelector.value)),
41+
};
42+
}
43+
44+
function getEnumNames(nodes) {
45+
if (!nodes.length) return {};
46+
return {
47+
enumNames: nodes.map((node) => lodash.get(node, node.dataSelector.name)),
48+
};
49+
}
50+
51+
/**
52+
* @summary Recursively generate `dependencies` for RJSF schema based on tree.
53+
* @param {Object[]} nodes - Array of nodes (e.g. `[tree]` or `node.children`)
54+
* @returns {{}|{dependencies: {}}}
55+
*/
56+
export function buildDependencies(nodes) {
57+
if (nodes.length === 0 || nodes.every((n) => !n.children?.length)) return {};
58+
const parentKey = nodes[0].dataSelector.key;
59+
const childKey = nodes[0].children[0].dataSelector.key;
60+
return {
61+
dependencies: {
62+
[parentKey]: {
63+
oneOf: nodes.map((node) => {
64+
return {
65+
properties: {
66+
[parentKey]: {
67+
...getEnumValues([node]),
68+
...getEnumNames([node]),
69+
},
70+
[childKey]: {
71+
...getEnumValues(node.children),
72+
...getEnumNames(node.children),
73+
},
74+
},
75+
...buildDependencies(node.children),
76+
};
77+
}),
78+
},
79+
},
80+
};
81+
}
82+
83+
/**
84+
* Combine schema and dependencies block for usage with react-jsonschema-form (RJSF)
85+
* @param {Object} schema - Schema
86+
* @param {String} schemaId - Schema id (takes precedence over `schema` when both are provided)
87+
* @param {Object[]} nodes - Array of nodes
88+
* @param {Boolean} modifyProperties - Whether properties in main schema should be modified (add `enum` and `enumNames`)
89+
* @returns {{}|{[p: string]: *}} - RJSF schema
90+
*/
91+
export function getSchemaWithDependencies({
92+
schema = {},
93+
schemaId,
94+
nodes,
95+
modifyProperties = false,
96+
}) {
97+
const mainSchema = schemaId ? JSONSchemasInterface.schemaById(schemaId) : schema;
98+
99+
if (!lodash.isEmpty(mainSchema) && typeofSchema(mainSchema) !== "object") {
100+
console.error("getSchemaWithDependencies() only accepts schemas of type 'object'");
101+
return {};
102+
}
103+
104+
// RJSF does not automatically render dropdown widget if `enum` is not present
105+
if (modifyProperties && nodes.length) {
106+
const mod = {
107+
[nodes[0].dataSelector.key]: {
108+
...getEnumNames(nodes),
109+
...getEnumValues(nodes),
110+
},
111+
};
112+
lodash.forEach(mod, (extraFields, key) => {
113+
if (lodash.has(mainSchema, `properties.${key}`)) {
114+
mainSchema.properties[key] = { ...mainSchema.properties[key], ...extraFields };
115+
}
116+
});
117+
}
118+
119+
return {
120+
...(schemaId ? mainSchema : schema),
121+
...buildDependencies(nodes),
122+
};
123+
}

src/utils/tree.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* @summary Return nodes with `fn` function applied to each node.
3+
* Note that the function `fn` must take a node as an argument and must return a node object.
4+
* @param {Object[]} nodes - Array of nodes
5+
* @param {Function} fn - function to be applied to each node
6+
* @returns {Object[]} - Result of map
7+
*/
8+
export function mapTree(nodes, fn) {
9+
return nodes.map((node) => {
10+
const mappedNode = fn(node);
11+
if (node?.children?.length) {
12+
mappedNode.children = mapTree(node.children, fn);
13+
}
14+
return mappedNode;
15+
});
16+
}

tests/utils.schemas.tests.js

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { expect } from "chai";
2+
3+
import { buildDependencies, getSchemaWithDependencies, typeofSchema } from "../src/utils/schemas";
4+
5+
describe("RJSF schema", () => {
6+
const TREE = {
7+
path: "/dft",
8+
dataSelector: { key: "type", value: "data.type.slug", name: "data.type.name" },
9+
data: {
10+
type: {
11+
slug: "dft",
12+
name: "Density Functional Theory",
13+
},
14+
},
15+
children: [
16+
{
17+
path: "/dft/lda",
18+
dataSelector: {
19+
key: "subtype",
20+
value: "data.subtype.slug",
21+
name: "data.subtype.name",
22+
},
23+
data: {
24+
subtype: {
25+
slug: "lda",
26+
name: "LDA",
27+
},
28+
},
29+
children: [
30+
{
31+
path: "/dft/lda/svwn",
32+
dataSelector: {
33+
key: "functional",
34+
value: "data.functional.slug",
35+
name: "data.functional.name",
36+
},
37+
data: {
38+
functional: {
39+
slug: "svwn",
40+
name: "SVWN",
41+
},
42+
},
43+
},
44+
{
45+
path: "/dft/lda/pz",
46+
dataSelector: {
47+
key: "functional",
48+
value: "data.functional.slug",
49+
name: "data.functional.name",
50+
},
51+
data: {
52+
functional: {
53+
slug: "pz",
54+
name: "PZ",
55+
},
56+
},
57+
},
58+
],
59+
},
60+
{
61+
path: "/dft/gga",
62+
dataSelector: {
63+
key: "subtype",
64+
value: "data.subtype.slug",
65+
name: "data.subtype.name",
66+
},
67+
data: {
68+
subtype: {
69+
slug: "gga",
70+
name: "GGA",
71+
},
72+
},
73+
children: [
74+
{
75+
path: "/dft/gga/pbe",
76+
dataSelector: {
77+
key: "functional",
78+
value: "data.functional.slug",
79+
name: "data.functional.name",
80+
},
81+
data: {
82+
functional: {
83+
slug: "pbe",
84+
name: "PBE",
85+
},
86+
},
87+
},
88+
{
89+
path: "/dft/gga/pw91",
90+
dataSelector: {
91+
key: "functional",
92+
value: "data.functional.slug",
93+
name: "data.functional.name",
94+
},
95+
data: {
96+
functional: {
97+
slug: "pw91",
98+
name: "PW91",
99+
},
100+
},
101+
},
102+
],
103+
},
104+
],
105+
};
106+
const DFT_SCHEMA = {
107+
type: "object",
108+
properties: {
109+
type: {
110+
type: "string",
111+
},
112+
subtype: {
113+
type: "string",
114+
},
115+
functional: {
116+
type: "string",
117+
},
118+
},
119+
};
120+
121+
it("dependencies block can be created from tree", () => {
122+
const dependencies = buildDependencies([TREE]);
123+
124+
const [dftCase] = dependencies.dependencies.type.oneOf;
125+
expect(dftCase.properties.subtype.enum).to.have.ordered.members(["lda", "gga"]);
126+
expect(dftCase.properties.subtype.enumNames).to.have.ordered.members(["LDA", "GGA"]);
127+
128+
const [ldaCase, ggaCase] = dftCase.dependencies.subtype.oneOf;
129+
expect(ldaCase.properties.subtype.enum).to.have.length(1);
130+
expect(ldaCase.properties.functional.enum).to.have.ordered.members(["svwn", "pz"]);
131+
expect(ldaCase.properties.functional.enumNames).to.have.ordered.members(["SVWN", "PZ"]);
132+
expect(ldaCase).to.not.have.property("dependencies");
133+
134+
expect(ggaCase.properties.subtype.enum).to.have.length(1);
135+
expect(ggaCase.properties.functional.enum).to.have.ordered.members(["pbe", "pw91"]);
136+
expect(ggaCase.properties.functional.enumNames).to.have.ordered.members(["PBE", "PW91"]);
137+
expect(ggaCase).to.not.have.property("dependencies");
138+
});
139+
140+
it("can be created with dependencies from schema", () => {
141+
const rjsfSchema = getSchemaWithDependencies({
142+
schema: DFT_SCHEMA,
143+
nodes: [TREE],
144+
});
145+
expect(rjsfSchema.type).to.be.eql(DFT_SCHEMA.type);
146+
expect(rjsfSchema.properties).to.be.eql(DFT_SCHEMA.properties);
147+
expect(rjsfSchema).to.have.property("dependencies");
148+
});
149+
150+
it("enum and enumNames can be added to schema properties", () => {
151+
const rjsfSchema = getSchemaWithDependencies({
152+
schema: DFT_SCHEMA,
153+
nodes: [TREE],
154+
modifyProperties: true,
155+
});
156+
// console.log(JSON.stringify(rjsfSchema, null, 4));
157+
expect(rjsfSchema.type).to.be.eql(DFT_SCHEMA.type);
158+
expect(rjsfSchema.properties.type).to.have.property("enum");
159+
expect(rjsfSchema.properties.type.enum).to.be.eql(["dft"]);
160+
expect(rjsfSchema.properties.type).to.have.property("enumNames");
161+
expect(rjsfSchema.properties.type.enumNames).to.be.eql(["Density Functional Theory"]);
162+
expect(rjsfSchema).to.have.property("dependencies");
163+
});
164+
});
165+
166+
describe("Schema utility", () => {
167+
const schemas = [
168+
["string", { type: "string" }],
169+
["integer", { type: "integer" }],
170+
["number", { type: "number" }],
171+
["object", { type: "object" }],
172+
["array", { type: "array" }],
173+
];
174+
const objSchemaNoType = { properties: { name: { type: "string" } } };
175+
const arraySchemaNoType = { items: { type: "number" } };
176+
it("type can be determined correctly", () => {
177+
schemas.forEach(([type, schema]) => {
178+
const currentType = typeofSchema(schema);
179+
expect(currentType).to.be.equal(type);
180+
});
181+
expect(typeofSchema(objSchemaNoType)).to.be.equal("object");
182+
expect(typeofSchema(arraySchemaNoType)).to.be.equal("array");
183+
});
184+
});

tests/utils.tree.tests.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { expect } from "chai";
2+
3+
import { mapTree } from "../src/utils";
4+
5+
describe("Tree data structure", () => {
6+
const TREE = {
7+
path: "/A",
8+
children: [
9+
{
10+
path: "/A/B",
11+
children: [
12+
{
13+
path: "/A/B/C",
14+
},
15+
],
16+
},
17+
{
18+
path: "/A/D",
19+
},
20+
],
21+
};
22+
it("map", () => {
23+
const [mappedTree] = mapTree([TREE], (node) => {
24+
return { ...node, foo: "bar" };
25+
});
26+
expect(mappedTree).to.have.property("foo", "bar");
27+
expect(mappedTree.children[0]).to.have.property("foo", "bar");
28+
expect(mappedTree.children[0].children[0]).to.have.property("foo", "bar");
29+
expect(mappedTree.children[1]).to.have.property("foo", "bar");
30+
});
31+
});

0 commit comments

Comments
 (0)