Skip to content

Commit

Permalink
fix: use a merge strategy for oneOf/anyOfs during validation (#1685)
Browse files Browse the repository at this point in the history
  • Loading branch information
janechu authored Apr 29, 2019
1 parent 895d8c5 commit 5c28382
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 15 deletions.
6 changes: 6 additions & 0 deletions packages/fast-tooling-react/app/pages/form/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ export const nestedOneOf: ExampleComponent = {
schema: NestedOneOfSchema,
};

import MergedOneOfSchema from "../../../src/__tests__/schemas/merged-one-of.schema.json";

export const mergedOneOf: ExampleComponent = {
schema: MergedOneOfSchema,
};

import ObjectsSchema from "../../../src/__tests__/schemas/objects.schema.json";

export const objects: ExampleComponent = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"$schema": "http://json-schema.org/schema#",
"type": "object",
"id": "mergedOneOf",
"properties": {
"foo": {
"title": "Foo",
"type": "object",
"required": [
"a",
"b"
],
"oneOf": [
{
"title": "Overridden Foo 1",
"description": "A is string",
"properties": {
"a": {
"title": "A",
"type": "string"
},
"b": {
"title": "B",
"type": "number"
}
}
},
{
"title": "Overridden Foo 2",
"description": "B is string",
"properties": {
"a": {
"title": "A",
"type": "number"
},
"b": {
"title": "B",
"type": "string"
}
}
}
]
}
}
}
28 changes: 22 additions & 6 deletions packages/fast-tooling-react/src/form/form/form-section.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import { get, uniqueId } from "lodash-es";
import { get, omit, uniqueId } from "lodash-es";
import manageJss, { ManagedJSSProps } from "@microsoft/fast-jss-manager-react";
import { ManagedClasses } from "@microsoft/fast-components-class-name-contracts-base";
import FormCategory from "./form-category";
Expand All @@ -26,6 +26,7 @@ import {
FormSectionClassNameContract,
FormSectionProps,
FormSectionState,
InitialOneOfAnyOfState,
} from "./form-section.props";
import FormControl from "./form-control";
import FormOneOfAnyOf from "./form-one-of-any-of";
Expand Down Expand Up @@ -55,23 +56,38 @@ class FormSection extends React.Component<
*/
public componentWillUpdate(nextProps: FormSectionProps): void {
if (checkIsDifferentSchema(this.props.schema, nextProps.schema)) {
this.setState(getInitialOneOfAnyOfState(nextProps.schema, nextProps.data));
const initialOneOfAnyOfState: InitialOneOfAnyOfState = getInitialOneOfAnyOfState(
nextProps.schema,
nextProps.data
);

this.setState(initialOneOfAnyOfState);

if (typeof nextProps.data === "undefined") {
const updatedData: any = generateExampleData(
initialOneOfAnyOfState.schema,
""
);

nextProps.onChange(nextProps.dataLocation, updatedData);
}
}
}

/**
* Handles updating the schema to another active oneOf/anyOf schema
*/
private handleAnyOfOneOfClick = (activeIndex: number): void => {
const updatedData: any = generateExampleData(
this.props.schema[this.state.oneOfAnyOf.type][activeIndex],
""
const updatedSchema: any = Object.assign(
omit(this.props.schema, [this.state.oneOfAnyOf.type]),
this.props.schema[this.state.oneOfAnyOf.type][activeIndex]
);
const updatedData: any = generateExampleData(updatedSchema, "");

this.props.onChange(this.props.dataLocation, updatedData);

this.setState({
schema: this.props.schema[this.state.oneOfAnyOf.type][activeIndex],
schema: updatedSchema,
oneOfAnyOf: {
type: this.state.oneOfAnyOf.type,
activeIndex,
Expand Down
42 changes: 42 additions & 0 deletions packages/fast-tooling-react/src/form/utilities/form.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import "jest";
import {
BreadcrumbItem,
getBreadcrumbs,
getInitialOneOfAnyOfState,
getNavigation,
getSchemaByDataLocation,
HandleBreadcrumbClick,
Expand All @@ -13,11 +14,13 @@ import arraysSchema from "../../__tests__/schemas/arrays.schema.json";
import generalSchema from "../../__tests__/schemas/general.schema.json";
import objectsSchema from "../../__tests__/schemas/objects.schema.json";
import oneOfSchema from "../../__tests__/schemas/one-of.schema.json";
import mergedOneOfSchema from "../../__tests__/schemas/merged-one-of.schema.json";
import anyOfSchema from "../../__tests__/schemas/any-of.schema.json";
import childrenSchema from "../../__tests__/schemas/children.schema.json";
import textFieldSchema from "../../__tests__/schemas/textarea.schema.json";
import deeplyNestedOneOfSchema from "../../__tests__/schemas/one-of-deeply-nested.schema.json";
import { reactChildrenStringSchema } from "../form/form-item.children.text";
import { InitialOneOfAnyOfState, oneOfAnyOfType } from "../form/form-section.props";

/**
* Gets the navigation
Expand Down Expand Up @@ -755,6 +758,9 @@ describe("getBreadcrumbs", () => {
});
});

/**
* Gets a schema by data location (lodash path syntax)
*/
describe("getSchemaByDataLocation", () => {
test("should return the schema given from data requiring no children", () => {
const data: any = {
Expand Down Expand Up @@ -821,3 +827,39 @@ describe("getSchemaByDataLocation", () => {
expect(schema2.id).toBe(textFieldSchema.id);
});
});

/**
* Gets an initial oneOfAnyOf state
*/
describe("getInitialOneOfAnyOfState", () => {
test("should get the initial oneOf/anyOf activeIndex and oneOf/anyOf keyword", () => {
const initialOneOfAnyOfNoData: InitialOneOfAnyOfState = getInitialOneOfAnyOfState(
mergedOneOfSchema.properties.foo,
{}
);
const initialOneOfAnyOfWithData: InitialOneOfAnyOfState = getInitialOneOfAnyOfState(
mergedOneOfSchema.properties.foo,
{
a: 5,
b: "foo",
}
);

expect(initialOneOfAnyOfNoData.oneOfAnyOf).toEqual({
type: oneOfAnyOfType.oneOf,
activeIndex: 0,
});
expect(initialOneOfAnyOfWithData.oneOfAnyOf).toEqual({
type: oneOfAnyOfType.oneOf,
activeIndex: 1,
});
});
test("should correctly get the oneOf/anyOf schema and merge it with any definitions outside of the selected oneOf/anyOf item", () => {
const initialOneOfAnyOfNoData: InitialOneOfAnyOfState = getInitialOneOfAnyOfState(
mergedOneOfSchema.properties.foo,
{}
);

expect(initialOneOfAnyOfNoData.schema.required).toEqual(["a", "b"]);
});
});
35 changes: 26 additions & 9 deletions packages/fast-tooling-react/src/form/utilities/form.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import { cloneDeep, get, mergeWith, set, unset } from "lodash-es";
import { cloneDeep, get, mergeWith, omit, set, unset } from "lodash-es";
import { getDataFromSchema } from "../../data-utilities";
import {
getChildOptionBySchemaId,
Expand Down Expand Up @@ -87,7 +87,10 @@ export function getInitialOneOfAnyOfState(
if (schema.oneOf || schema.anyOf) {
oneOfAnyOf = schema.oneOf ? oneOfAnyOfType.oneOf : oneOfAnyOfType.anyOf;
activeIndex = getOneOfAnyOfActiveIndex(oneOfAnyOf, schema, data);
updatedSchema = schema[oneOfAnyOf][activeIndex];
updatedSchema = Object.assign(
omit(schema, [oneOfAnyOf]),
schema[oneOfAnyOf][activeIndex]
);
oneOfAnyOfState = {
type: oneOfAnyOf,
activeIndex,
Expand Down Expand Up @@ -190,7 +193,12 @@ export function getOneOfAnyOfActiveIndex(type: string, schema: any, data: any):
const newData: any = removeUndefinedKeys(data);

schema[type].forEach((oneOfAnyOfItem: any, index: number) => {
if (validateSchema(oneOfAnyOfItem, newData)) {
const updatedSchema: any = Object.assign(
omit(schema, [type]),
oneOfAnyOfItem
);

if (validateSchema(updatedSchema, newData)) {
activeIndex = index;
return;
}
Expand Down Expand Up @@ -282,19 +290,28 @@ function checkIsObjectAndSetType(schemaSection: any): any {
return schemaSection.type;
}

function getOneOfAnyOfType(schemaSection: any): oneOfAnyOfType | null {
return schemaSection.oneOf
? oneOfAnyOfType.oneOf
: schemaSection.anyOf
? oneOfAnyOfType.anyOf
: null;
}

/**
* Generates example data for a newly added optional schema item
*/
export function generateExampleData(schema: any, propertyLocation: string): any {
let schemaSection: any =
propertyLocation === "" ? schema : get(schema, propertyLocation);
const oneOfAnyOf: oneOfAnyOfType | null = getOneOfAnyOfType(schemaSection);

if (schemaSection.oneOf) {
schemaSection = schemaSection.oneOf[0];
}

if (schemaSection.anyOf) {
schemaSection = schemaSection.anyOf[0];
if (oneOfAnyOf !== null) {
schemaSection = Object.assign(
{},
omit(schemaSection, [oneOfAnyOf]),
schemaSection[oneOfAnyOf][0]
);
}

schemaSection.type = checkIsObjectAndSetType(schemaSection);
Expand Down

0 comments on commit 5c28382

Please sign in to comment.