Skip to content
This repository has been archived by the owner on Nov 8, 2024. It is now read-only.

Commit

Permalink
feat(oas3): add support for Schema Object 'oneOf'
Browse files Browse the repository at this point in the history
  • Loading branch information
kylef committed Jun 23, 2020
1 parent aa85066 commit 47223e4
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 3 deletions.
9 changes: 9 additions & 0 deletions packages/openapi3-parser/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Fury OAS3 Parser Changelog

## Master

### Enhancements

- Adds partial support for using `oneOf` in a Schema Object. One of is
supported when used in a schema object alone, or with the nullable constraint
or any annotation. It is not supported in the case when one of is used in
conjunction with other constraints in the same schema object.

## 0.13.1 (2020-06-22)

### Bug Fixes
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi3-parser/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ support.
| minProperties ||
| required ||
| allOf ||
| oneOf | |
| oneOf | ~ |
| anyOf ||
| not ||
| items ||
Expand Down
62 changes: 60 additions & 2 deletions packages/openapi3-parser/lib/parser/oas/parseSchemaObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const unsupportedKeys = [
'minItems', 'uniqueItems', 'maxProperties', 'minProperties', 'required',

// JSON Schema + OAS 3 specific rules
'allOf', 'oneOf', 'anyOf', 'not', 'additionalProperties', 'format',
'allOf', 'anyOf', 'not', 'additionalProperties', 'format',

// OAS 3 specific
'discriminator', 'readOnly', 'writeOnly', 'xml', 'externalDocs', 'deprecated',
Expand Down Expand Up @@ -124,6 +124,51 @@ function validateValuesMatchSchema(context, schema) {
return parseObject(context, name, parseMember)(schema);
}

// Warns if oneOf is used with other unsupported constraints
function validateOneOfIsNotUsedWithUnsupportedConstraints(context) {
// oneOf can be used like the following:
//
// oneOf:
// - ...
// - ...
// ...
//
// where its effectively a combination of "one of these two constraints"
// and "also these other constraits" which is effectively:
//
// allOf:
// - oneOf:
// - ...
// - ...
// - ...
//
// API Element's doesn't have a way to support `allOf` and thus using
// `oneOf` alongside other constraints is unsupported.
//
// We can allow annotations and (nullable as that is simple to support).

// is a JSON Schema annotation (not constraint)
const isAnnotation = R.anyPass([
hasKey('title'),
hasKey('description'),
hasKey('default'),
hasKey('example'),
]);

const createUnsupportedWithOneOfWarning = member => createWarning(context.namespace,
`'${name}' has limited support for 'oneOf', use of '${member.key.toValue()}' with 'oneOf' is not supported`,
member.key);

const parseMember = R.cond([
[hasKey('oneOf'), R.identity],
[hasKey('nullable'), R.identity],
[isAnnotation, R.identity],
[R.T, createUnsupportedWithOneOfWarning],
]);

return parseObject(context, name, parseMember);
}

function parseSchema(context) {
const { namespace } = context;

Expand All @@ -146,6 +191,14 @@ function parseSchema(context) {
createWarning(namespace, `'${name}' 'required' array value is not a string`));
const parseRequired = parseArray(context, `${name}' 'required`, parseRequiredString);

const parseOneOf = pipeParseResult(namespace,
parseArray(context, `${name}' 'oneOf`, parseSubSchema),
(oneOf) => {
const element = new namespace.elements.Enum();
element.enumerations = oneOf;
return element;
});

const parseMember = R.cond([
[hasKey('type'), parseType],
[hasKey('enum'), R.compose(parseEnum(context, name), getValue)],
Expand All @@ -156,6 +209,7 @@ function parseSchema(context) {
[hasKey('description'), parseString(context, name, false)],
[hasKey('default'), e => e.clone()],
[hasKey('example'), e => e.clone()],
[hasKey('oneOf'), R.compose(parseOneOf, getValue)],

[isUnsupportedKey, createUnsupportedMemberWarning(namespace, name)],

Expand All @@ -166,13 +220,17 @@ function parseSchema(context) {
return pipeParseResult(namespace,
parseObject(context, name, parseMember),
R.curry(validateValuesMatchSchema)(context),
R.when(object => object.hasKey('oneOf'), validateOneOfIsNotUsedWithUnsupportedConstraints(context)),
(schema) => {
let element;

const oneOf = schema.get('oneOf');
const enumerations = schema.get('enum');
const type = schema.getValue('type');

if (enumerations) {
if (oneOf) {
element = oneOf;
} else if (enumerations) {
element = enumerations;
} else if (type === 'object') {
element = constructObjectStructure(namespace, schema);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -822,4 +822,145 @@ describe('Schema Object', () => {
"'Schema Object' 'default' does not match expected type 'number'",
]);
});

describe('#oneOf', () => {
it('can parse oneOf', () => {
const schema = new namespace.elements.Object({
oneOf: [
{ type: 'string' },
{ type: 'number' },
],
});
const parseResult = parse(context, schema);

expect(parseResult.length).to.equal(1);
expect(parseResult.get(0)).to.be.instanceof(namespace.elements.DataStructure);
expect(parseResult).to.not.contain.annotations;

const element = parseResult.get(0).content;
expect(element).to.be.instanceof(namespace.elements.Enum);

expect(element.enumerations.length).to.equal(2);
expect(element.enumerations.get(0)).to.be.instanceof(namespace.elements.String);
expect(element.enumerations.get(1)).to.be.instanceof(namespace.elements.Number);
});

it('can parse oneOf with description', () => {
const schema = new namespace.elements.Object({
oneOf: [
{ type: 'string' },
{ type: 'number' },
],
description: 'string or number',
});
const parseResult = parse(context, schema);

expect(parseResult.length).to.equal(1);
expect(parseResult.get(0)).to.be.instanceof(namespace.elements.DataStructure);
expect(parseResult).to.not.contain.annotations;

const element = parseResult.get(0).content;
expect(element).to.be.instanceof(namespace.elements.Enum);
expect(element.description.toValue()).to.equal('string or number');

expect(element.enumerations.length).to.equal(2);
expect(element.enumerations.get(0)).to.be.instanceof(namespace.elements.String);
expect(element.enumerations.get(1)).to.be.instanceof(namespace.elements.Number);
});

it('can parse oneOf with default', () => {
const schema = new namespace.elements.Object({
oneOf: [
{ type: 'string' },
{ type: 'number' },
],
default: 'unset',
});
const parseResult = parse(context, schema);

expect(parseResult.length).to.equal(1);
expect(parseResult.get(0)).to.be.instanceof(namespace.elements.DataStructure);
expect(parseResult).to.not.contain.annotations;

const element = parseResult.get(0).content;
expect(element).to.be.instanceof(namespace.elements.Enum);
expect(element.attributes.getValue('default')).to.equal('unset');

expect(element.enumerations.length).to.equal(2);
expect(element.enumerations.get(0)).to.be.instanceof(namespace.elements.String);
expect(element.enumerations.get(1)).to.be.instanceof(namespace.elements.Number);
});

it('can parse oneOf with example', () => {
const schema = new namespace.elements.Object({
oneOf: [
{ type: 'string' },
{ type: 'number' },
],
example: 52,
});
const parseResult = parse(context, schema);

expect(parseResult.length).to.equal(1);
expect(parseResult.get(0)).to.be.instanceof(namespace.elements.DataStructure);
expect(parseResult).to.not.contain.annotations;

const element = parseResult.get(0).content;
expect(element).to.be.instanceof(namespace.elements.Enum);
expect(element.attributes.getValue('samples')).to.deep.equal([52]);

expect(element.enumerations.length).to.equal(2);
expect(element.enumerations.get(0)).to.be.instanceof(namespace.elements.String);
expect(element.enumerations.get(1)).to.be.instanceof(namespace.elements.Number);
});

it('can parse oneOf with nullable', () => {
const schema = new namespace.elements.Object({
oneOf: [
{ type: 'string' },
{ type: 'number' },
],
nullable: true,
});
const parseResult = parse(context, schema);

expect(parseResult.length).to.equal(1);
expect(parseResult.get(0)).to.be.instanceof(namespace.elements.DataStructure);
expect(parseResult).to.not.contain.annotations;

const element = parseResult.get(0).content;
expect(element).to.be.instanceof(namespace.elements.Enum);
expect(element.attributes.getValue('typeAttributes')).to.deep.equal(['nullable']);

expect(element.enumerations.length).to.equal(2);
expect(element.enumerations.get(0)).to.be.instanceof(namespace.elements.String);
expect(element.enumerations.get(1)).to.be.instanceof(namespace.elements.Number);
});

it('warns for unsupported oneOf with other schema properties', () => {
const schema = new namespace.elements.Object({
oneOf: [
{ type: 'string' },
{ type: 'array' },
],
items: {
type: 'string',
},
});
const parseResult = parse(context, schema);

expect(parseResult.get(0)).to.be.instanceof(namespace.elements.DataStructure);

expect(parseResult).to.contain.warning(
"'Schema Object' has limited support for 'oneOf', use of 'items' with 'oneOf' is not supported"
);

const element = parseResult.get(0).content;
expect(element).to.be.instanceof(namespace.elements.Enum);

expect(element.enumerations.length).to.equal(2);
expect(element.enumerations.get(0)).to.be.instanceof(namespace.elements.String);
expect(element.enumerations.get(1)).to.be.instanceof(namespace.elements.Array);
});
});
});

0 comments on commit 47223e4

Please sign in to comment.