Skip to content

Commit

Permalink
Support oneOf and anyOf as alternative of enum/enumNames (#581)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
fbessou authored and glasserc committed Jun 12, 2017
1 parent 2a885bc commit 3df0ab7
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 35 deletions.
64 changes: 64 additions & 0 deletions playground/samples/alternatives.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
module.exports = {
schema: {
definitions: {
Color: {
title: "Color",
type: "string",
anyOf: [
{
type: "string",
enum: ["#ff0000"],
title: "Red",
},
{
type: "string",
enum: ["#00ff00"],
title: "Green",
},
{
type: "string",
enum: ["#0000ff"],
title: "Blue",
},
],
},
},
title: "Image editor",
type: "object",
required: ["currentColor", "colorMask", "blendMode"],
properties: {
currentColor: {
$ref: "#/definitions/Color",
title: "Brush color",
},
colorMask: {
type: "array",
uniqueItems: true,
items: {
$ref: "#/definitions/Color",
},
title: "Color mask",
},
colorPalette: {
type: "array",
title: "Color palette",
items: {
$ref: "#/definitions/Color",
},
},
blendMode: {
title: "Blend mode",
type: "string",
enum: ["screen", "multiply", "overlay"],
enumNames: ["Screen", "Multiply", "Overlay"],
},
},
},
uiSchema: {},
formData: {
currentColor: "#00ff00",
colorMask: ["#0000ff"],
colorPalette: ["#ff0000"],
blendMode: "screen",
},
};
2 changes: 2 additions & 0 deletions playground/samples/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import validation from "./validation";
import files from "./files";
import single from "./single";
import customArray from "./customArray";
import alternatives from "./alternatives";

export const samples = {
Simple: simple,
Expand All @@ -30,4 +31,5 @@ export const samples = {
Files: files,
Single: single,
"Custom Array": customArray,
Alternatives: alternatives,
};
11 changes: 6 additions & 5 deletions src/components/fields/ArrayField.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,14 +280,15 @@ class ArrayField extends Component {
};

render() {
const { schema, uiSchema } = this.props;
if (isFilesArray(schema, uiSchema)) {
return this.renderFiles();
}
const { schema, uiSchema, registry = getDefaultRegistry() } = this.props;
const { definitions } = registry;
if (isFixedItems(schema)) {
return this.renderFixedArray();
}
if (isMultiSelect(schema)) {
if (isFilesArray(schema, uiSchema, definitions)) {
return this.renderFiles();
}
if (isMultiSelect(schema, definitions)) {
return this.renderMultiSelect();
}
return this.renderNormalArray();
Expand Down
4 changes: 3 additions & 1 deletion src/components/fields/SchemaField.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,9 @@ function SchemaFieldRender(props) {
const uiOptions = getUiOptions(uiSchema);
let { label: displayLabel = true } = uiOptions;
if (schema.type === "array") {
displayLabel = isMultiSelect(schema) || isFilesArray(schema, uiSchema);
displayLabel =
isMultiSelect(schema, definitions) ||
isFilesArray(schema, uiSchema, definitions);
}
if (schema.type === "object") {
displayLabel = false;
Expand Down
3 changes: 2 additions & 1 deletion src/components/fields/StringField.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import PropTypes from "prop-types";
import {
getWidget,
getUiOptions,
isSelect,
optionsList,
getDefaultRegistry,
} from "../../utils";
Expand All @@ -25,7 +26,7 @@ function StringField(props) {
} = props;
const { title, format } = schema;
const { widgets, formContext } = registry;
const enumOptions = Array.isArray(schema.enum) && optionsList(schema);
const enumOptions = isSelect(schema) && optionsList(schema);
const defaultWidget = format || (enumOptions ? "select" : "text");
const { widget = defaultWidget, placeholder = "", ...options } = getUiOptions(
uiSchema
Expand Down
73 changes: 58 additions & 15 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,21 +277,55 @@ export function orderProperties(properties, order) {
return complete;
}

export function isMultiSelect(schema) {
return schema.items
? Array.isArray(schema.items.enum) && schema.uniqueItems
: false;
}

export function isFilesArray(schema, uiSchema) {
/**
* This function checks if the given schema matches a single
* constant value.
*/
export function isConstant(schema) {
return (
(schema.items &&
schema.items.type === "string" &&
schema.items.format === "data-url") ||
uiSchema["ui:widget"] === "files"
(Array.isArray(schema.enum) && schema.enum.length === 1) ||
schema.hasOwnProperty("const")
);
}

export function toConstant(schema) {
if (Array.isArray(schema.enum) && schema.enum.length === 1) {
return schema.enum[0];
} else if (schema.hasOwnProperty("const")) {
return schema.const;
} else {
throw new Error("schema cannot be inferred as a constant");
}
}

export function isSelect(_schema, definitions = {}) {
const schema = retrieveSchema(_schema, definitions);
const altSchemas = schema.oneOf || schema.anyOf;
if (Array.isArray(schema.enum)) {
return true;
} else if (Array.isArray(altSchemas)) {
return altSchemas.every(altSchemas => isConstant(altSchemas));
}
return false;
}

export function isMultiSelect(schema, definitions = {}) {
if (!schema.uniqueItems || !schema.items) {
return false;
}
return isSelect(schema.items, definitions);
}

export function isFilesArray(schema, uiSchema, definitions = {}) {
if (uiSchema["ui:widget"] === "files") {
return true;
} else if (schema.items) {
const itemsSchema = retrieveSchema(schema.items, definitions);
return itemsSchema.type === "string" && itemsSchema.format === "data-url";
}
return false;
}

export function isFixedItems(schema) {
return (
Array.isArray(schema.items) &&
Expand All @@ -308,10 +342,19 @@ export function allowAdditionalItems(schema) {
}

export function optionsList(schema) {
return schema.enum.map((value, i) => {
const label = (schema.enumNames && schema.enumNames[i]) || String(value);
return { label, value };
});
if (schema.enum) {
return schema.enum.map((value, i) => {
const label = (schema.enumNames && schema.enumNames[i]) || String(value);
return { label, value };
});
} else {
const altSchemas = schema.oneOf || schema.anyOf;
return altSchemas.map((schema, i) => {
const value = toConstant(schema);
const label = schema.title || String(value);
return { label, value };
});
}
}

function findSchemaDefinition($ref, definitions = {}) {
Expand Down
105 changes: 92 additions & 13 deletions test/utils_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
deepEquals,
getDefaultFormState,
isFilesArray,
isConstant,
toConstant,
isMultiSelect,
mergeObjects,
pad,
Expand Down Expand Up @@ -269,24 +271,101 @@ describe("utils", () => {
});
});

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

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

it("should be false if uniqueItems is false", () => {
const schema = { items: { enum: ["foo", "bar"] }, uniqueItems: false };
expect(isMultiSelect(schema)).to.be.false;
it("should return false when schema enum contains several items", () => {
const schema = { enum: ["foo", "bar", "baz"] };
expect(isConstant(schema)).to.be.false;
});

it("should return true when schema const is defined", () => {
const schema = { const: "foo" };
expect(isConstant(schema)).to.be.true;
});
});

describe("toConstant()", () => {
describe("schema contains an enum array", () => {
it("should return its first value when it contains a unique element", () => {
const schema = { enum: ["foo"] };
expect(toConstant(schema)).eql("foo");
});

it("should return schema const value when it exists", () => {
const schema = { const: "bar" };
expect(toConstant(schema)).eql("bar");
});

it("should throw when it contains more than one element", () => {
const schema = { enum: ["foo", "bar"] };
expect(() => {
toConstant(schema);
}).to.Throw(Error, "cannot be inferred");
});
});
});

describe("isMultiSelect()", () => {
describe("uniqueItems is true", () => {
describe("schema items enum is an array", () => {
it("should be true", () => {
let schema = { items: { enum: ["foo", "bar"] }, uniqueItems: true };
expect(isMultiSelect(schema)).to.be.true;
});
});

it("should be false if items is undefined", () => {
const schema = {};
expect(isMultiSelect(schema)).to.be.false;
});

describe("schema items enum is not an array", () => {
const constantSchema = { type: "string", enum: ["Foo"] };
const notConstantSchema = { type: "string" };

it("should be false if oneOf/anyOf is not in items schema", () => {
const schema = { items: {}, uniqueItems: true };
expect(isMultiSelect(schema)).to.be.false;
});

it("should be false if oneOf/anyOf schemas are not all constants", () => {
const schema = {
items: { oneOf: [constantSchema, notConstantSchema] },
uniqueItems: true,
};
expect(isMultiSelect(schema)).to.be.false;
});

it("should be true if oneOf/anyOf schemas are all constants", () => {
const schema = {
items: { oneOf: [constantSchema, constantSchema] },
uniqueItems: true,
};
expect(isMultiSelect(schema)).to.be.true;
});
});

it("should retrieve reference schema definitions", () => {
const schema = {
items: { $ref: "#/definitions/FooItem" },
uniqueItems: true,
};
const definitions = { FooItem: { type: "string", enum: ["foo"] } };
expect(isMultiSelect(schema, definitions)).to.be.true;
});
});

it("should be false if schema items enum is not an array", () => {
const schema = { items: {}, uniqueItems: true };
it("should be false if uniqueItems is false", () => {
const schema = { items: { enum: ["foo", "bar"] }, uniqueItems: false };
expect(isMultiSelect(schema)).to.be.false;
});
});
Expand Down Expand Up @@ -605,7 +684,7 @@ describe("utils", () => {
});
});

it("should retrieve reference schema definitions", () => {
it("should retrieve referenced schema definitions", () => {
const schema = {
definitions: {
testdef: {
Expand Down

0 comments on commit 3df0ab7

Please sign in to comment.