Skip to content

Commit 4524376

Browse files
authored
Add allOf support (rjsf-team#1380)
* feat: add allOf support * use patch version of json-schema-merge-allof * fix: publicPath for netlify builds * fix typo * fix: pass around shouldMergeAllOf a bit * fix: remove shouldMergeAllOf * fix: resolve allOf when calculating idSchema and pathSchema * Revert "Fix dependency defaults for uncontrolled components (rjsf-team#1371)" This reverts commit 6728977. * Revert "Revert "Fix dependency defaults for uncontrolled components (rjsf-team#1371)"" This reverts commit ad2bd68. * test: start writing some basic tests * add doc on allOf * add allOf playground sample * handle merging errors by removing allOf * add test for nested allOf's * clarify that allOf keyword is dropped * don't use merge allof fork * remove change to package-lock.json * prettier format
1 parent 58f4e07 commit 4524376

File tree

8 files changed

+247
-4
lines changed

8 files changed

+247
-4
lines changed

packages/core/docs/index.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,12 @@ This component follows [JSON Schema](http://json-schema.org/documentation.html)
190190

191191
* `anyOf`, `allOf`, and `oneOf`, or multiple `types` (i.e. `"type": ["string", "array"]`)
192192

193-
The `anyOf` and `oneOf` keywords are supported, however, properties declared inside the `anyOf/oneOf` should not overlap with properties "outside" of the `anyOf/oneOf`.
193+
The `anyOf` and `oneOf` keywords are supported; however, properties declared inside the `anyOf/oneOf` should not overlap with properties "outside" of the `anyOf/oneOf`.
194194

195195
You can also use `oneOf` with [schema dependencies](dependencies.md#schema-dependencies) to dynamically add schema properties based on input data.
196196

197+
The `allOf` keyword is supported; it uses [json-schema-merge-allof](https://github.com/mokkabonna/json-schema-merge-allof) to merge subschemas to render the final combined schema in the form. When these subschemas are incompatible, though (or if the library has an error merging it), the `allOf` keyword is dropped from the schema.
198+
197199
* `"additionalProperties":false` produces incorrect schemas when used with [schema dependencies](#schema-dependencies). This library does not remove extra properties, which causes validation to fail. It is recommended to avoid setting `"additionalProperties":false` when you use schema dependencies. See [#848](https://github.com/mozilla-services/react-jsonschema-form/issues/848) [#902](https://github.com/mozilla-services/react-jsonschema-form/issues/902) [#992](https://github.com/mozilla-services/react-jsonschema-form/issues/992)
198200

199201
## Handling of schema defaults

packages/core/package-lock.json

Lines changed: 71 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"@babel/runtime-corejs2": "^7.4.5",
4646
"ajv": "^6.7.0",
4747
"core-js": "^2.5.7",
48+
"json-schema-merge-allof": "^0.6.0",
4849
"lodash": "^4.17.15",
4950
"prop-types": "^15.7.2",
5051
"react-app-polyfill": "^1.0.4",

packages/core/playground/samples/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import arrays from "./arrays";
22
import anyOf from "./anyOf";
33
import oneOf from "./oneOf";
4+
import allOf from "./allOf";
45
import nested from "./nested";
56
import numbers from "./numbers";
67
import simple from "./simple";
@@ -48,6 +49,7 @@ export const samples = {
4849
"Additional Properties": additionalProperties,
4950
"Any Of": anyOf,
5051
"One Of": oneOf,
52+
"All Of": allOf,
5153
"Null fields": nullField,
5254
Nullable: nullable,
5355
ErrorSchema: errorSchema,

packages/core/src/utils.js

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from "react";
22
import * as ReactIs from "react-is";
3+
import mergeAllOf from "json-schema-merge-allof";
34
import fill from "core-js/library/fn/array/fill";
45
import validateFormData, { isValid } from "./validate";
56
import union from "lodash/union";
@@ -627,6 +628,13 @@ export function resolveSchema(schema, definitions = {}, formData = {}) {
627628
} else if (schema.hasOwnProperty("dependencies")) {
628629
const resolvedSchema = resolveDependencies(schema, definitions, formData);
629630
return retrieveSchema(resolvedSchema, definitions, formData);
631+
} else if (schema.hasOwnProperty("allOf")) {
632+
return {
633+
...schema,
634+
allOf: schema.allOf.map(allOfSubschema =>
635+
retrieveSchema(allOfSubschema, definitions, formData)
636+
),
637+
};
630638
} else {
631639
// No $ref or dependencies attribute found, returning the original schema.
632640
return schema;
@@ -647,7 +655,19 @@ function resolveReference(schema, definitions, formData) {
647655
}
648656

649657
export function retrieveSchema(schema, definitions = {}, formData = {}) {
650-
const resolvedSchema = resolveSchema(schema, definitions, formData);
658+
let resolvedSchema = resolveSchema(schema, definitions, formData);
659+
if ("allOf" in schema) {
660+
try {
661+
resolvedSchema = mergeAllOf({
662+
...resolvedSchema,
663+
allOf: resolvedSchema.allOf,
664+
});
665+
} catch (e) {
666+
console.warn("could not merge subschemas in allOf:\n" + e);
667+
const { allOf, ...resolvedSchemaWithoutAllOf } = resolvedSchema;
668+
return resolvedSchemaWithoutAllOf;
669+
}
670+
}
651671
const hasAdditionalProperties =
652672
resolvedSchema.hasOwnProperty("additionalProperties") &&
653673
resolvedSchema.additionalProperties !== false;
@@ -937,7 +957,7 @@ export function toIdSchema(
937957
const idSchema = {
938958
$id: id || idPrefix,
939959
};
940-
if ("$ref" in schema || "dependencies" in schema) {
960+
if ("$ref" in schema || "dependencies" in schema || "allOf" in schema) {
941961
const _schema = retrieveSchema(schema, definitions, formData);
942962
return toIdSchema(_schema, id, definitions, formData, idPrefix);
943963
}
@@ -967,7 +987,7 @@ export function toPathSchema(schema, name = "", definitions, formData = {}) {
967987
const pathSchema = {
968988
$name: name.replace(/^\./, ""),
969989
};
970-
if ("$ref" in schema || "dependencies" in schema) {
990+
if ("$ref" in schema || "dependencies" in schema || "allOf" in schema) {
971991
const _schema = retrieveSchema(schema, definitions, formData);
972992
return toPathSchema(_schema, name, definitions, formData);
973993
}

packages/core/test/utils_test.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,19 @@ import {
2626
guessType,
2727
mergeSchemas,
2828
} from "../src/utils";
29+
import { createSandbox } from "./test_utils";
2930

3031
describe("utils", () => {
32+
let sandbox;
33+
34+
beforeEach(() => {
35+
sandbox = createSandbox();
36+
});
37+
38+
afterEach(() => {
39+
sandbox.restore();
40+
});
41+
3142
describe("getDefaultFormState()", () => {
3243
describe("root default", () => {
3344
it("should map root schema default to form state, if any", () => {
@@ -2259,6 +2270,67 @@ describe("utils", () => {
22592270
});
22602271
});
22612272
});
2273+
2274+
describe("allOf", () => {
2275+
it("should merge types", () => {
2276+
const schema = {
2277+
allOf: [{ type: ["string", "number", "null"] }, { type: "string" }],
2278+
};
2279+
const definitions = {};
2280+
const formData = {};
2281+
expect(retrieveSchema(schema, definitions, formData)).eql({
2282+
type: "string",
2283+
});
2284+
});
2285+
it("should not merge incompatible types", () => {
2286+
sandbox.stub(console, "warn");
2287+
const schema = {
2288+
allOf: [{ type: "string" }, { type: "boolean" }],
2289+
};
2290+
const definitions = {};
2291+
const formData = {};
2292+
expect(retrieveSchema(schema, definitions, formData)).eql({});
2293+
expect(
2294+
console.warn.calledWithMatch(/could not merge subschemas in allOf/)
2295+
).to.be.true;
2296+
});
2297+
it("should merge types with $ref in them", () => {
2298+
const schema = {
2299+
allOf: [{ $ref: "#/definitions/1" }, { $ref: "#/definitions/2" }],
2300+
};
2301+
const definitions = {
2302+
"1": { type: "string" },
2303+
"2": { minLength: 5 },
2304+
};
2305+
const formData = {};
2306+
expect(retrieveSchema(schema, definitions, formData)).eql({
2307+
type: "string",
2308+
minLength: 5,
2309+
});
2310+
});
2311+
it("should properly merge schemas with nested allOf's", () => {
2312+
const schema = {
2313+
allOf: [
2314+
{
2315+
type: "string",
2316+
allOf: [{ minLength: 2 }, { maxLength: 5 }],
2317+
},
2318+
{
2319+
type: "string",
2320+
allOf: [{ default: "hi" }, { minLength: 4 }],
2321+
},
2322+
],
2323+
};
2324+
const definitions = {};
2325+
const formData = {};
2326+
expect(retrieveSchema(schema, definitions, formData)).eql({
2327+
type: "string",
2328+
minLength: 4,
2329+
maxLength: 5,
2330+
default: "hi",
2331+
});
2332+
});
2333+
});
22622334
});
22632335

22642336
describe("shouldRender", () => {

playground/samples/allOf.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module.exports = {
2+
schema: {
3+
type: "object",
4+
allOf: [
5+
{
6+
properties: {
7+
lorem: {
8+
type: ["string", "number"],
9+
},
10+
},
11+
},
12+
{
13+
properties: {
14+
lorem: {
15+
type: "boolean",
16+
minLength: 5,
17+
},
18+
ipsum: {
19+
type: "string",
20+
},
21+
},
22+
},
23+
],
24+
},
25+
formData: {},
26+
};

test/allOf_test.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { expect } from "chai";
2+
3+
import { createFormComponent, createSandbox } from "./test_utils";
4+
5+
describe("allOf", () => {
6+
let sandbox;
7+
8+
beforeEach(() => {
9+
sandbox = createSandbox();
10+
});
11+
12+
afterEach(() => {
13+
sandbox.restore();
14+
});
15+
16+
it("should render a regular input element with a single type, when multiple types specified", () => {
17+
const schema = {
18+
type: "object",
19+
properties: {
20+
foo: {
21+
allOf: [{ type: ["string", "number", "null"] }, { type: "string" }],
22+
},
23+
},
24+
};
25+
26+
const { node } = createFormComponent({
27+
schema,
28+
});
29+
30+
expect(node.querySelectorAll("input")).to.have.length.of(1);
31+
});
32+
33+
it("should be able to handle incompatible types and not crash", () => {
34+
const schema = {
35+
type: "object",
36+
properties: {
37+
foo: {
38+
allOf: [{ type: "string" }, { type: "boolean" }],
39+
},
40+
},
41+
};
42+
43+
const { node } = createFormComponent({
44+
schema,
45+
});
46+
47+
expect(node.querySelectorAll("input")).to.have.length.of(0);
48+
});
49+
});

0 commit comments

Comments
 (0)