Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support oneOf and anyOf as alternative of enum/enumNames #581

Merged
merged 6 commits into from
Jun 12, 2017
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
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 @@ -265,14 +265,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", () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

referenced

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