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

Commit ab08f52

Browse files
authored
Merge pull request #503 from apiaryio/kylef/ref
Fix support for using `$ref` in a component definition
2 parents 2d5cdfd + 8254de9 commit ab08f52

File tree

11 files changed

+442
-31
lines changed

11 files changed

+442
-31
lines changed

packages/openapi3-parser/CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,19 @@
99
or any annotation. It is not supported in the case when one of is used in
1010
conjunction with other constraints in the same schema object.
1111

12+
### Bug Fixes
13+
14+
- Supports using `$ref` in the root of a component, for example:
15+
16+
```yaml
17+
components:
18+
schemas:
19+
UserAlias:
20+
$ref: '#/components/schemas/User'
21+
User:
22+
type: object
23+
```
24+
1225
## 0.13.1 (2020-06-22)
1326
1427
### Bug Fixes

packages/openapi3-parser/lib/parser/oas/parseComponentsObject.js

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const {
88
createInvalidMemberWarning,
99
} = require('../annotations');
1010
const parseObject = require('../parseObject');
11+
const parseReference = require('../parseReference');
1112
const pipeParseResult = require('../../pipeParseResult');
1213
const parseSchemaObject = require('./parseSchemaObject');
1314
const parseParameterObject = require('./parseParameterObject');
@@ -71,6 +72,36 @@ const parseComponentMember = R.curry((context, parser, member) => {
7172
return parseResult;
7273
});
7374

75+
function registerComponentStateInContext(context, components) {
76+
const { namespace } = context;
77+
78+
// Component referencing supports recursive (and circular in some cases)
79+
// references and thus we must know about all of the component IDs upfront.
80+
// Below we are putting in the unparsed components so we can keep the
81+
// dereferencing logic simple, these are used during parsing the components
82+
// and later on the components in our context is replaced by the final parsed
83+
// result.
84+
// eslint-disable-next-line no-param-reassign
85+
context.state.components = new namespace.elements.Object();
86+
87+
if (isObject(components)) {
88+
components.forEach((value, key) => {
89+
if (isObject(value)) {
90+
// Take each component object (f.e schemas, parameters) and convert to
91+
// object with members for each key (discarding value). We don't want the
92+
// value making it into final parse results under any circumstance, for
93+
// example if the parse errors out and we leave bad state
94+
95+
const componentObject = new namespace.elements.Object(
96+
value.map((value, key) => new namespace.elements.Member(key))
97+
);
98+
99+
context.state.components.set(key.toValue(), componentObject);
100+
}
101+
});
102+
}
103+
}
104+
74105
/**
75106
* Parse Components Object
76107
*
@@ -84,24 +115,7 @@ const parseComponentMember = R.curry((context, parser, member) => {
84115
function parseComponentsObject(context, element) {
85116
const { namespace } = context;
86117

87-
// Schema Object supports recursive (and circular) references and thus we
88-
// must know about all of the schema IDs upfront. Below we are putting
89-
// in the unparsed schemas so we can keep the dereferencing logic simple,
90-
// these are used during parsing the schema components and later on the
91-
// components in our context is replaced by the final parsed result.
92-
// eslint-disable-next-line no-param-reassign
93-
context.state.components = new namespace.elements.Object();
94-
95-
if (isObject(element) && element.get('schemas') && isObject(element.get('schemas'))) {
96-
// Take schemas and convert to object with members for each key (discarding value)
97-
// We don't want the value making it into final parse results under any circumstance,
98-
// for example if the parse errors out and we leave bad state
99-
const schemas = new namespace.elements.Object(
100-
element.get('schemas').map((value, key) => new namespace.elements.Member(key))
101-
);
102-
103-
context.state.components.set('schemas', schemas);
104-
}
118+
registerComponentStateInContext(context, element);
105119

106120
const createMemberValueNotObjectWarning = member => createWarning(namespace,
107121
`'${name}' '${member.key.toValue()}' is not an object`, member.value);
@@ -117,24 +131,25 @@ function parseComponentsObject(context, element) {
117131
* @returns ParseResult<ObjectElement>
118132
* @private
119133
*/
120-
const parseComponentObjectMember = (parser) => {
134+
const parseComponentObjectMember = R.curry((parser, member) => {
135+
const component = member.key.toValue();
136+
121137
const parseMember = parseComponentMember(context, parser);
138+
const parseMemberOrRef = m => parseReference(component, () => parseMember(m), context, m.value, false, true);
122139

123-
return member => pipeParseResult(context.namespace,
140+
return pipeParseResult(context.namespace,
124141
validateIsObject,
125-
R.compose(parseObject(context, name, parseMember), getValue),
142+
R.compose(parseObject(context, name, parseMemberOrRef), getValue),
126143
(object) => {
127-
const contextMember = context.state.components.getMember(member.key.toValue());
144+
const contextMember = context.state.components.getMember(component);
128145

129146
if (contextMember) {
130147
contextMember.value = object;
131-
} else {
132-
context.state.components.push(new namespace.elements.Member(member.key, object));
133148
}
134149

135150
return object;
136151
})(member);
137-
};
152+
});
138153

139154
const setDataStructureId = (dataStructure, key) => {
140155
if (dataStructure) {

packages/openapi3-parser/lib/parser/oas/parseReferenceObject.js

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,33 @@ const parseString = require('../parseString');
1212
const name = 'Reference Object';
1313
const requiredKeys = ['$ref'];
1414

15+
/**
16+
* Recursively dereference an element in the given component
17+
*
18+
* @param namespace {Namespace}
19+
* @param component {ObjectElement}
20+
* @param ref {StringElement}
21+
* @param element {Element}
22+
* @param parents {string[]} an optional collections of traversed parents
23+
*
24+
* @returns Element
25+
*/
26+
function dereference(namespace, component, ref, element, parents = []) {
27+
if (parents && parents.includes(element.element)) {
28+
// We've already cycled through this element. We're in a circular loop
29+
parents.shift();
30+
return createError(namespace, `Reference cannot be circular, '${ref.toValue()}' causes a circular reference via ${parents.join(', ')}`, ref);
31+
}
32+
33+
const match = component.get(element.element);
34+
if (match) {
35+
parents.push(element.element);
36+
return dereference(namespace, component, ref, match, parents);
37+
}
38+
39+
return element;
40+
}
41+
1542
/**
1643
* Parse Reference Object
1744
*
@@ -76,7 +103,7 @@ function parseReferenceObject(context, componentName, element, returnReferenceEl
76103
return new namespace.elements.ParseResult(
77104
component
78105
.filter((value, key) => key.toValue() === componentId && value)
79-
.map(value => value)
106+
.map(value => dereference(namespace, component, ref, value))
80107
);
81108
};
82109

packages/openapi3-parser/lib/parser/parseReference.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ function isReferenceObject(element) {
66
return isObject(element) && element.get('$ref') !== undefined;
77
}
88

9-
function parseReference(component, parser, context, element, isInsideSchema) {
9+
function parseReference(component, parser, context, element, isInsideSchema, returnReferenceElement) {
1010
if (isReferenceObject(element)) {
11-
const parseResult = parseReferenceObject(context, component, element, component === 'schemas');
11+
const parseResult = parseReferenceObject(context, component, element, component === 'schemas' || returnReferenceElement);
1212

1313
// If we're referencing a schema object and we're not inside a schema
1414
// parser (subschema), then we want to wrap the object in a data structure element

packages/openapi3-parser/test/integration/components-test.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ describe('components', () => {
1919
const file = path.join(fixtures, 'path-item-object-parameters-unsupported-parameter');
2020
return testParseFixture(file);
2121
});
22+
23+
it('handles parameter referencing with reference to alias', () => {
24+
const file = path.join(fixtures, 'path-item-object-parameters-alias');
25+
return testParseFixture(file);
26+
});
2227
});
2328

2429
describe('Media Type Object', () => {
@@ -72,9 +77,16 @@ describe('components', () => {
7277
});
7378
});
7479

75-
it("'Schema Object' circular references", () => {
76-
const file = path.join(fixtures, 'schema-object-circular');
77-
return testParseFixture(file);
80+
describe('Schema Object', () => {
81+
it('handles circular references', () => {
82+
const file = path.join(fixtures, 'schema-object-circular');
83+
return testParseFixture(file);
84+
});
85+
86+
it('handles schema with reference to alias', () => {
87+
const file = path.join(fixtures, 'schema-alias');
88+
return testParseFixture(file);
89+
});
7890
});
7991

8092
it("'Operation Object' requestBody references", () => {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"element": "parseResult",
3+
"content": [
4+
{
5+
"element": "category",
6+
"meta": {
7+
"classes": {
8+
"element": "array",
9+
"content": [
10+
{
11+
"element": "string",
12+
"content": "api"
13+
}
14+
]
15+
},
16+
"title": {
17+
"element": "string",
18+
"content": "Parameter Component with alias"
19+
}
20+
},
21+
"attributes": {
22+
"version": {
23+
"element": "string",
24+
"content": "1.0.0"
25+
}
26+
},
27+
"content": [
28+
{
29+
"element": "resource",
30+
"attributes": {
31+
"href": {
32+
"element": "string",
33+
"content": "/{?foo}"
34+
},
35+
"hrefVariables": {
36+
"element": "hrefVariables",
37+
"content": [
38+
{
39+
"element": "member",
40+
"content": {
41+
"key": {
42+
"element": "string",
43+
"content": "foo"
44+
}
45+
}
46+
}
47+
]
48+
}
49+
}
50+
}
51+
]
52+
}
53+
]
54+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
openapi: "3.0.0"
2+
info:
3+
version: 1.0.0
4+
title: Parameter Component with alias
5+
paths:
6+
/:
7+
parameters:
8+
- $ref: '#/components/parameters/UserAlias'
9+
components:
10+
parameters:
11+
User:
12+
in: query
13+
name: foo
14+
UserAlias:
15+
$ref: '#/components/parameters/User'

0 commit comments

Comments
 (0)