Skip to content

Commit 3df0ab7

Browse files
fbessouglasserc
authored andcommitted
Support oneOf and anyOf as alternative of enum/enumNames (#581)
* Add "alternatives" sample using oneOf/anyOf schemas * Retrieve items schema in isMultiSelect and isFilesArray * Support oneOf/anyOf constants in multiselect widgets * Add tests for isMultiSelect with support of oneOf and anyOf * Add support to oneOf/anyOf with constant schemas in StringField * Add tests for isConstant and toConstant
1 parent 2a885bc commit 3df0ab7

File tree

7 files changed

+227
-35
lines changed

7 files changed

+227
-35
lines changed

playground/samples/alternatives.js

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
module.exports = {
2+
schema: {
3+
definitions: {
4+
Color: {
5+
title: "Color",
6+
type: "string",
7+
anyOf: [
8+
{
9+
type: "string",
10+
enum: ["#ff0000"],
11+
title: "Red",
12+
},
13+
{
14+
type: "string",
15+
enum: ["#00ff00"],
16+
title: "Green",
17+
},
18+
{
19+
type: "string",
20+
enum: ["#0000ff"],
21+
title: "Blue",
22+
},
23+
],
24+
},
25+
},
26+
title: "Image editor",
27+
type: "object",
28+
required: ["currentColor", "colorMask", "blendMode"],
29+
properties: {
30+
currentColor: {
31+
$ref: "#/definitions/Color",
32+
title: "Brush color",
33+
},
34+
colorMask: {
35+
type: "array",
36+
uniqueItems: true,
37+
items: {
38+
$ref: "#/definitions/Color",
39+
},
40+
title: "Color mask",
41+
},
42+
colorPalette: {
43+
type: "array",
44+
title: "Color palette",
45+
items: {
46+
$ref: "#/definitions/Color",
47+
},
48+
},
49+
blendMode: {
50+
title: "Blend mode",
51+
type: "string",
52+
enum: ["screen", "multiply", "overlay"],
53+
enumNames: ["Screen", "Multiply", "Overlay"],
54+
},
55+
},
56+
},
57+
uiSchema: {},
58+
formData: {
59+
currentColor: "#00ff00",
60+
colorMask: ["#0000ff"],
61+
colorPalette: ["#ff0000"],
62+
blendMode: "screen",
63+
},
64+
};

playground/samples/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import validation from "./validation";
1313
import files from "./files";
1414
import single from "./single";
1515
import customArray from "./customArray";
16+
import alternatives from "./alternatives";
1617

1718
export const samples = {
1819
Simple: simple,
@@ -30,4 +31,5 @@ export const samples = {
3031
Files: files,
3132
Single: single,
3233
"Custom Array": customArray,
34+
Alternatives: alternatives,
3335
};

src/components/fields/ArrayField.js

+6-5
Original file line numberDiff line numberDiff line change
@@ -280,14 +280,15 @@ class ArrayField extends Component {
280280
};
281281

282282
render() {
283-
const { schema, uiSchema } = this.props;
284-
if (isFilesArray(schema, uiSchema)) {
285-
return this.renderFiles();
286-
}
283+
const { schema, uiSchema, registry = getDefaultRegistry() } = this.props;
284+
const { definitions } = registry;
287285
if (isFixedItems(schema)) {
288286
return this.renderFixedArray();
289287
}
290-
if (isMultiSelect(schema)) {
288+
if (isFilesArray(schema, uiSchema, definitions)) {
289+
return this.renderFiles();
290+
}
291+
if (isMultiSelect(schema, definitions)) {
291292
return this.renderMultiSelect();
292293
}
293294
return this.renderNormalArray();

src/components/fields/SchemaField.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,9 @@ function SchemaFieldRender(props) {
161161
const uiOptions = getUiOptions(uiSchema);
162162
let { label: displayLabel = true } = uiOptions;
163163
if (schema.type === "array") {
164-
displayLabel = isMultiSelect(schema) || isFilesArray(schema, uiSchema);
164+
displayLabel =
165+
isMultiSelect(schema, definitions) ||
166+
isFilesArray(schema, uiSchema, definitions);
165167
}
166168
if (schema.type === "object") {
167169
displayLabel = false;

src/components/fields/StringField.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import PropTypes from "prop-types";
44
import {
55
getWidget,
66
getUiOptions,
7+
isSelect,
78
optionsList,
89
getDefaultRegistry,
910
} from "../../utils";
@@ -25,7 +26,7 @@ function StringField(props) {
2526
} = props;
2627
const { title, format } = schema;
2728
const { widgets, formContext } = registry;
28-
const enumOptions = Array.isArray(schema.enum) && optionsList(schema);
29+
const enumOptions = isSelect(schema) && optionsList(schema);
2930
const defaultWidget = format || (enumOptions ? "select" : "text");
3031
const { widget = defaultWidget, placeholder = "", ...options } = getUiOptions(
3132
uiSchema

src/utils.js

+58-15
Original file line numberDiff line numberDiff line change
@@ -277,21 +277,55 @@ export function orderProperties(properties, order) {
277277
return complete;
278278
}
279279

280-
export function isMultiSelect(schema) {
281-
return schema.items
282-
? Array.isArray(schema.items.enum) && schema.uniqueItems
283-
: false;
284-
}
285-
286-
export function isFilesArray(schema, uiSchema) {
280+
/**
281+
* This function checks if the given schema matches a single
282+
* constant value.
283+
*/
284+
export function isConstant(schema) {
287285
return (
288-
(schema.items &&
289-
schema.items.type === "string" &&
290-
schema.items.format === "data-url") ||
291-
uiSchema["ui:widget"] === "files"
286+
(Array.isArray(schema.enum) && schema.enum.length === 1) ||
287+
schema.hasOwnProperty("const")
292288
);
293289
}
294290

291+
export function toConstant(schema) {
292+
if (Array.isArray(schema.enum) && schema.enum.length === 1) {
293+
return schema.enum[0];
294+
} else if (schema.hasOwnProperty("const")) {
295+
return schema.const;
296+
} else {
297+
throw new Error("schema cannot be inferred as a constant");
298+
}
299+
}
300+
301+
export function isSelect(_schema, definitions = {}) {
302+
const schema = retrieveSchema(_schema, definitions);
303+
const altSchemas = schema.oneOf || schema.anyOf;
304+
if (Array.isArray(schema.enum)) {
305+
return true;
306+
} else if (Array.isArray(altSchemas)) {
307+
return altSchemas.every(altSchemas => isConstant(altSchemas));
308+
}
309+
return false;
310+
}
311+
312+
export function isMultiSelect(schema, definitions = {}) {
313+
if (!schema.uniqueItems || !schema.items) {
314+
return false;
315+
}
316+
return isSelect(schema.items, definitions);
317+
}
318+
319+
export function isFilesArray(schema, uiSchema, definitions = {}) {
320+
if (uiSchema["ui:widget"] === "files") {
321+
return true;
322+
} else if (schema.items) {
323+
const itemsSchema = retrieveSchema(schema.items, definitions);
324+
return itemsSchema.type === "string" && itemsSchema.format === "data-url";
325+
}
326+
return false;
327+
}
328+
295329
export function isFixedItems(schema) {
296330
return (
297331
Array.isArray(schema.items) &&
@@ -308,10 +342,19 @@ export function allowAdditionalItems(schema) {
308342
}
309343

310344
export function optionsList(schema) {
311-
return schema.enum.map((value, i) => {
312-
const label = (schema.enumNames && schema.enumNames[i]) || String(value);
313-
return { label, value };
314-
});
345+
if (schema.enum) {
346+
return schema.enum.map((value, i) => {
347+
const label = (schema.enumNames && schema.enumNames[i]) || String(value);
348+
return { label, value };
349+
});
350+
} else {
351+
const altSchemas = schema.oneOf || schema.anyOf;
352+
return altSchemas.map((schema, i) => {
353+
const value = toConstant(schema);
354+
const label = schema.title || String(value);
355+
return { label, value };
356+
});
357+
}
315358
}
316359

317360
function findSchemaDefinition($ref, definitions = {}) {

test/utils_test.js

+92-13
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
deepEquals,
77
getDefaultFormState,
88
isFilesArray,
9+
isConstant,
10+
toConstant,
911
isMultiSelect,
1012
mergeObjects,
1113
pad,
@@ -269,24 +271,101 @@ describe("utils", () => {
269271
});
270272
});
271273

272-
describe("isMultiSelect()", () => {
273-
it("should be true if schema items enum is an array and uniqueItems is true", () => {
274-
let schema = { items: { enum: ["foo", "bar"] }, uniqueItems: true };
275-
expect(isMultiSelect(schema)).to.be.true;
274+
describe("isConstant", () => {
275+
it("should return false when neither enum nor const is defined", () => {
276+
const schema = {};
277+
expect(isConstant(schema)).to.be.false;
276278
});
277279

278-
it("should be false if items is undefined", () => {
279-
const schema = {};
280-
expect(isMultiSelect(schema)).to.be.false;
280+
it("should return true when schema enum is an array of one item", () => {
281+
const schema = { enum: ["foo"] };
282+
expect(isConstant(schema)).to.be.true;
281283
});
282284

283-
it("should be false if uniqueItems is false", () => {
284-
const schema = { items: { enum: ["foo", "bar"] }, uniqueItems: false };
285-
expect(isMultiSelect(schema)).to.be.false;
285+
it("should return false when schema enum contains several items", () => {
286+
const schema = { enum: ["foo", "bar", "baz"] };
287+
expect(isConstant(schema)).to.be.false;
288+
});
289+
290+
it("should return true when schema const is defined", () => {
291+
const schema = { const: "foo" };
292+
expect(isConstant(schema)).to.be.true;
293+
});
294+
});
295+
296+
describe("toConstant()", () => {
297+
describe("schema contains an enum array", () => {
298+
it("should return its first value when it contains a unique element", () => {
299+
const schema = { enum: ["foo"] };
300+
expect(toConstant(schema)).eql("foo");
301+
});
302+
303+
it("should return schema const value when it exists", () => {
304+
const schema = { const: "bar" };
305+
expect(toConstant(schema)).eql("bar");
306+
});
307+
308+
it("should throw when it contains more than one element", () => {
309+
const schema = { enum: ["foo", "bar"] };
310+
expect(() => {
311+
toConstant(schema);
312+
}).to.Throw(Error, "cannot be inferred");
313+
});
314+
});
315+
});
316+
317+
describe("isMultiSelect()", () => {
318+
describe("uniqueItems is true", () => {
319+
describe("schema items enum is an array", () => {
320+
it("should be true", () => {
321+
let schema = { items: { enum: ["foo", "bar"] }, uniqueItems: true };
322+
expect(isMultiSelect(schema)).to.be.true;
323+
});
324+
});
325+
326+
it("should be false if items is undefined", () => {
327+
const schema = {};
328+
expect(isMultiSelect(schema)).to.be.false;
329+
});
330+
331+
describe("schema items enum is not an array", () => {
332+
const constantSchema = { type: "string", enum: ["Foo"] };
333+
const notConstantSchema = { type: "string" };
334+
335+
it("should be false if oneOf/anyOf is not in items schema", () => {
336+
const schema = { items: {}, uniqueItems: true };
337+
expect(isMultiSelect(schema)).to.be.false;
338+
});
339+
340+
it("should be false if oneOf/anyOf schemas are not all constants", () => {
341+
const schema = {
342+
items: { oneOf: [constantSchema, notConstantSchema] },
343+
uniqueItems: true,
344+
};
345+
expect(isMultiSelect(schema)).to.be.false;
346+
});
347+
348+
it("should be true if oneOf/anyOf schemas are all constants", () => {
349+
const schema = {
350+
items: { oneOf: [constantSchema, constantSchema] },
351+
uniqueItems: true,
352+
};
353+
expect(isMultiSelect(schema)).to.be.true;
354+
});
355+
});
356+
357+
it("should retrieve reference schema definitions", () => {
358+
const schema = {
359+
items: { $ref: "#/definitions/FooItem" },
360+
uniqueItems: true,
361+
};
362+
const definitions = { FooItem: { type: "string", enum: ["foo"] } };
363+
expect(isMultiSelect(schema, definitions)).to.be.true;
364+
});
286365
});
287366

288-
it("should be false if schema items enum is not an array", () => {
289-
const schema = { items: {}, uniqueItems: true };
367+
it("should be false if uniqueItems is false", () => {
368+
const schema = { items: { enum: ["foo", "bar"] }, uniqueItems: false };
290369
expect(isMultiSelect(schema)).to.be.false;
291370
});
292371
});
@@ -605,7 +684,7 @@ describe("utils", () => {
605684
});
606685
});
607686

608-
it("should retrieve reference schema definitions", () => {
687+
it("should retrieve referenced schema definitions", () => {
609688
const schema = {
610689
definitions: {
611690
testdef: {

0 commit comments

Comments
 (0)