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

Fix support for using $ref in a component definition #503

Merged
merged 3 commits into from
Jun 24, 2020
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
13 changes: 13 additions & 0 deletions packages/openapi3-parser/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@
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.

### Bug Fixes

- Supports using `$ref` in the root of a component, for example:

```yaml
components:
schemas:
UserAlias:
$ref: '#/components/schemas/User'
User:
type: object
```

## 0.13.1 (2020-06-22)

### Bug Fixes
Expand Down
65 changes: 40 additions & 25 deletions packages/openapi3-parser/lib/parser/oas/parseComponentsObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const {
createInvalidMemberWarning,
} = require('../annotations');
const parseObject = require('../parseObject');
const parseReference = require('../parseReference');
const pipeParseResult = require('../../pipeParseResult');
const parseSchemaObject = require('./parseSchemaObject');
const parseParameterObject = require('./parseParameterObject');
Expand Down Expand Up @@ -71,6 +72,36 @@ const parseComponentMember = R.curry((context, parser, member) => {
return parseResult;
});

function registerComponentStateInContext(context, components) {
const { namespace } = context;

// Component referencing supports recursive (and circular in some cases)
// references and thus we must know about all of the component IDs upfront.
// Below we are putting in the unparsed components so we can keep the
// dereferencing logic simple, these are used during parsing the components
// and later on the components in our context is replaced by the final parsed
// result.
// eslint-disable-next-line no-param-reassign
context.state.components = new namespace.elements.Object();

if (isObject(components)) {
components.forEach((value, key) => {
if (isObject(value)) {
// Take each component object (f.e schemas, parameters) and convert to
// object with members for each key (discarding value). We don't want the
// value making it into final parse results under any circumstance, for
// example if the parse errors out and we leave bad state

const componentObject = new namespace.elements.Object(
value.map((value, key) => new namespace.elements.Member(key))
);

context.state.components.set(key.toValue(), componentObject);
}
});
}
}

/**
* Parse Components Object
*
Expand All @@ -84,24 +115,7 @@ const parseComponentMember = R.curry((context, parser, member) => {
function parseComponentsObject(context, element) {
const { namespace } = context;

// Schema Object supports recursive (and circular) references and thus we
// must know about all of the schema IDs upfront. Below we are putting
// in the unparsed schemas so we can keep the dereferencing logic simple,
// these are used during parsing the schema components and later on the
// components in our context is replaced by the final parsed result.
// eslint-disable-next-line no-param-reassign
context.state.components = new namespace.elements.Object();

if (isObject(element) && element.get('schemas') && isObject(element.get('schemas'))) {
// Take schemas and convert to object with members for each key (discarding value)
// We don't want the value making it into final parse results under any circumstance,
// for example if the parse errors out and we leave bad state
const schemas = new namespace.elements.Object(
element.get('schemas').map((value, key) => new namespace.elements.Member(key))
);

context.state.components.set('schemas', schemas);
}
registerComponentStateInContext(context, element);

const createMemberValueNotObjectWarning = member => createWarning(namespace,
`'${name}' '${member.key.toValue()}' is not an object`, member.value);
Expand All @@ -117,24 +131,25 @@ function parseComponentsObject(context, element) {
* @returns ParseResult<ObjectElement>
* @private
*/
const parseComponentObjectMember = (parser) => {
const parseComponentObjectMember = R.curry((parser, member) => {
const component = member.key.toValue();

const parseMember = parseComponentMember(context, parser);
const parseMemberOrRef = m => parseReference(component, () => parseMember(m), context, m.value, false, true);

return member => pipeParseResult(context.namespace,
return pipeParseResult(context.namespace,
validateIsObject,
R.compose(parseObject(context, name, parseMember), getValue),
R.compose(parseObject(context, name, parseMemberOrRef), getValue),
(object) => {
const contextMember = context.state.components.getMember(member.key.toValue());
const contextMember = context.state.components.getMember(component);

if (contextMember) {
contextMember.value = object;
} else {
Copy link
Member Author

Choose a reason for hiding this comment

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

For review, this branch becomes dead code due to the first commit we only ever have the first case. This was evident in code coverage going down due to this line no longer being called/tested.

context.state.components.push(new namespace.elements.Member(member.key, object));
}

return object;
})(member);
};
});

const setDataStructureId = (dataStructure, key) => {
if (dataStructure) {
Expand Down
29 changes: 28 additions & 1 deletion packages/openapi3-parser/lib/parser/oas/parseReferenceObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,33 @@ const parseString = require('../parseString');
const name = 'Reference Object';
const requiredKeys = ['$ref'];

/**
* Recursively dereference an element in the given component
*
* @param namespace {Namespace}
* @param component {ObjectElement}
* @param ref {StringElement}
* @param element {Element}
* @param parents {string[]} an optional collections of traversed parents
*
* @returns Element
*/
function dereference(namespace, component, ref, element, parents = []) {
if (parents && parents.includes(element.element)) {
// We've already cycled through this element. We're in a circular loop
parents.shift();
return createError(namespace, `Reference cannot be circular, '${ref.toValue()}' causes a circular reference via ${parents.join(', ')}`, ref);
}

const match = component.get(element.element);
if (match) {
parents.push(element.element);
return dereference(namespace, component, ref, match, parents);
}

return element;
}

/**
* Parse Reference Object
*
Expand Down Expand Up @@ -76,7 +103,7 @@ function parseReferenceObject(context, componentName, element, returnReferenceEl
return new namespace.elements.ParseResult(
component
.filter((value, key) => key.toValue() === componentId && value)
.map(value => value)
.map(value => dereference(namespace, component, ref, value))
);
};

Expand Down
4 changes: 2 additions & 2 deletions packages/openapi3-parser/lib/parser/parseReference.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ function isReferenceObject(element) {
return isObject(element) && element.get('$ref') !== undefined;
}

function parseReference(component, parser, context, element, isInsideSchema) {
function parseReference(component, parser, context, element, isInsideSchema, returnReferenceElement) {
if (isReferenceObject(element)) {
const parseResult = parseReferenceObject(context, component, element, component === 'schemas');
const parseResult = parseReferenceObject(context, component, element, component === 'schemas' || returnReferenceElement);

// If we're referencing a schema object and we're not inside a schema
// parser (subschema), then we want to wrap the object in a data structure element
Expand Down
18 changes: 15 additions & 3 deletions packages/openapi3-parser/test/integration/components-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ describe('components', () => {
const file = path.join(fixtures, 'path-item-object-parameters-unsupported-parameter');
return testParseFixture(file);
});

it('handles parameter referencing with reference to alias', () => {
const file = path.join(fixtures, 'path-item-object-parameters-alias');
return testParseFixture(file);
});
});

describe('Media Type Object', () => {
Expand Down Expand Up @@ -72,9 +77,16 @@ describe('components', () => {
});
});

it("'Schema Object' circular references", () => {
const file = path.join(fixtures, 'schema-object-circular');
return testParseFixture(file);
describe('Schema Object', () => {
it('handles circular references', () => {
const file = path.join(fixtures, 'schema-object-circular');
return testParseFixture(file);
});

it('handles schema with reference to alias', () => {
const file = path.join(fixtures, 'schema-alias');
return testParseFixture(file);
});
});

it("'Operation Object' requestBody references", () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"element": "parseResult",
"content": [
{
"element": "category",
"meta": {
"classes": {
"element": "array",
"content": [
{
"element": "string",
"content": "api"
}
]
},
"title": {
"element": "string",
"content": "Parameter Component with alias"
}
},
"attributes": {
"version": {
"element": "string",
"content": "1.0.0"
}
},
"content": [
{
"element": "resource",
"attributes": {
"href": {
"element": "string",
"content": "/{?foo}"
},
"hrefVariables": {
"element": "hrefVariables",
"content": [
{
"element": "member",
"content": {
"key": {
"element": "string",
"content": "foo"
}
}
}
]
}
}
}
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
openapi: "3.0.0"
info:
version: 1.0.0
title: Parameter Component with alias
paths:
/:
parameters:
- $ref: '#/components/parameters/UserAlias'
components:
parameters:
User:
in: query
name: foo
UserAlias:
$ref: '#/components/parameters/User'
Loading