Skip to content

Commit

Permalink
Fix bug where matching anyOf branch is not selected correctly (#1129)
Browse files Browse the repository at this point in the history
This change improves the logic that selects a matching anyOf branch
based on form data. Previously the behaviour was to just check if the
form data was valid against an anyOf branch, however due to the
permissive nature of JSON schema this has unexpected behaviour. For
example, given the following schema:

```json
{
  "type": "object",
  "anyOf": [
    {
      "properties": {
        "foo": {
          "type": "string"
        }
      }
    },
    {
      "properties": {
        "bar": {
          "type": "string"
        }
      }
    }
  ]
}
```

The form data `{ bar: 'baz' }` will actually match the first branch. To
mitigate this, when doing the matching, the branch schema is augmented
to require at least one of the keys in the branch. For example the
schema above would become:

```json
{
  "type": "object",
  "anyOf": [
    {
      "properties": {
        "foo": {
          "type": "string"
        }
      },
      "anyOf": [
        { "required": [ "foo" ] }
      ]
    },
    {
      "properties": {
        "bar": {
          "type": "string"
        }
      },
      "anyOf": [
        { "required": [ "bar" ] }
      ]
    }
  ]
}
```

Signed-off-by: Lucian <lucian.buzzo@gmail.com>
  • Loading branch information
LucianBuzzo authored and epicfaace committed Jan 22, 2019
1 parent 2685047 commit 2de683e
Show file tree
Hide file tree
Showing 2 changed files with 272 additions and 1 deletion.
44 changes: 43 additions & 1 deletion src/components/fields/MultiSchemaField.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,49 @@ class AnyOfField extends Component {

getMatchingOption(formData, options) {
for (let i = 0; i < options.length; i++) {
if (isValid(options[i], formData)) {
const option = options[i];

// If the schema describes an object then we need to add slightly more
// strict matching to the schema, because unless the schema uses the
// "requires" keyword, an object will match the schema as long as it
// doesn't have matching keys with a conflicting type. To do this we use an
// "anyOf" with an array of requires. This augmentation expresses that the
// schema should match if any of the keys in the schema are present on the
// object and pass validation.
if (option.properties) {
// Create an "anyOf" schema that requires at least one of the keys in the
// "properties" object
const requiresAnyOf = {
anyOf: Object.keys(option.properties).map(key => ({
required: [key],
})),
};

let augmentedSchema;

// If the "anyOf" keyword already exists, wrap the augmentation in an "allOf"
if (option.anyOf) {
// Create a shallow clone of the option
const { ...shallowClone } = option;

if (!shallowClone.allOf) {
shallowClone.allOf = [];
} else {
// If "allOf" already exists, shallow clone the array
shallowClone.allOf = shallowClone.allOf.slice();
}

shallowClone.allOf.push(requiresAnyOf);

augmentedSchema = shallowClone;
} else {
augmentedSchema = Object.assign({}, option, requiresAnyOf);
}

if (isValid(augmentedSchema, formData)) {
return i;
}
} else if (isValid(options[i], formData)) {
return i;
}
}
Expand Down
229 changes: 229 additions & 0 deletions test/anyOf_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,235 @@ describe("anyOf", () => {
expect(node.querySelector("select").value).eql("1");
});

it("should not change the selected option when entering values", () => {
const schema = {
type: "object",
anyOf: [
{
title: "First method of identification",
properties: {
firstName: {
type: "string",
},
lastName: {
type: "string",
},
},
},
{
title: "Second method of identification",
properties: {
idCode: {
type: "string",
},
},
},
],
};

const { node } = createFormComponent({
schema,
});

const $select = node.querySelector("select");

expect($select.value).eql("0");

Simulate.change($select, {
target: { value: $select.options[1].value },
});

expect($select.value).eql("1");

Simulate.change(node.querySelector("input#root_idCode"), {
target: { value: "Lorem ipsum dolor sit amet" },
});

expect($select.value).eql("1");
});

it("should not change the selected option when entering values and the subschema uses `anyOf`", () => {
const schema = {
type: "object",
anyOf: [
{
title: "First method of identification",
properties: {
firstName: {
type: "string",
},
lastName: {
type: "string",
},
},
},
{
title: "Second method of identification",
properties: {
idCode: {
type: "string",
},
},
anyOf: [
{
properties: {
foo: {
type: "string",
},
},
},
{
properties: {
bar: {
type: "string",
},
},
},
],
},
],
};

const { node } = createFormComponent({
schema,
});

const $select = node.querySelector("select");

expect($select.value).eql("0");

Simulate.change($select, {
target: { value: $select.options[1].value },
});

expect($select.value).eql("1");

Simulate.change(node.querySelector("input#root_idCode"), {
target: { value: "Lorem ipsum dolor sit amet" },
});

expect($select.value).eql("1");
});

it("should not change the selected option when entering values and the subschema uses `allOf`", () => {
const schema = {
type: "object",
anyOf: [
{
title: "First method of identification",
properties: {
firstName: {
type: "string",
},
lastName: {
type: "string",
},
},
},
{
title: "Second method of identification",
properties: {
idCode: {
type: "string",
},
},
allOf: [
{
properties: {
foo: {
type: "string",
},
},
},
{
properties: {
bar: {
type: "string",
},
},
},
],
},
],
};

const { node } = createFormComponent({
schema,
});

const $select = node.querySelector("select");

expect($select.value).eql("0");

Simulate.change($select, {
target: { value: $select.options[1].value },
});

expect($select.value).eql("1");

Simulate.change(node.querySelector("input#root_idCode"), {
target: { value: "Lorem ipsum dolor sit amet" },
});

expect($select.value).eql("1");
});

it("should not mutate a schema that contains nested anyOf and allOf", () => {
const schema = {
type: "object",
anyOf: [
{
properties: {
foo: { type: "string" },
},
allOf: [
{
properties: {
baz: { type: "string" },
},
},
],
anyOf: [
{
properties: {
buzz: { type: "string" },
},
},
],
},
],
};

createFormComponent({
schema,
});

expect(schema).to.eql({
type: "object",
anyOf: [
{
properties: {
foo: { type: "string" },
},
allOf: [
{
properties: {
baz: { type: "string" },
},
},
],
anyOf: [
{
properties: {
buzz: { type: "string" },
},
},
],
},
],
});
});

describe("Arrays", () => {
it("should correctly render form inputs for anyOf inside array items", () => {
const schema = {
Expand Down

0 comments on commit 2de683e

Please sign in to comment.