Skip to content

Commit

Permalink
feat(reference): add support for OAS 3.1 Path Item dereference
Browse files Browse the repository at this point in the history
Closes #458
  • Loading branch information
char0n committed Jun 29, 2021
1 parent 1bc0d14 commit 02717c9
Show file tree
Hide file tree
Showing 46 changed files with 663 additions and 21 deletions.
1 change: 1 addition & 0 deletions apidom/packages/apidom-ns-openapi-3-1/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export {
isOperationElement,
isParameterElement,
isPathItemElement,
isPathItemElementExternal,
isPathsElement,
isReferenceElement,
isReferenceElementExternal,
Expand Down
13 changes: 13 additions & 0 deletions apidom/packages/apidom-ns-openapi-3-1/src/predicates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,19 @@ export const isPathItemElement = createPredicate(
},
);

export const isPathItemElementExternal = (element: any): element is PathItemElement => {
if (!isPathItemElement(element)) {
return false;
}
if (!isStringElement(element.$ref)) {
return false;
}

const value = element.$ref.toValue();

return isNonEmptyString(value) && !startsWith('#', value);
};

export const isPathsElement = createPredicate(
({ hasBasicElementProps, isElementType, primitiveEq }) => {
const isElementTypePaths = isElementType('paths');
Expand Down
18 changes: 3 additions & 15 deletions apidom/packages/apidom-ns-openapi-3-1/src/refractor/predicates.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MemberElement, isStringElement, isObjectElement, Element } from 'apidom';
import { startsWith, all } from 'ramda';
import { startsWith } from 'ramda';

export const isOpenApi3_1LikeElement = <T extends Element>(element: T): boolean => {
// @ts-ignore
Expand All @@ -12,20 +12,8 @@ export const isParameterLikeElement = <T extends Element>(element: T): boolean =
};

export const isReferenceLikeElement = <T extends Element>(element: T): boolean => {
const isAllowedProperty = (property: string): boolean => {
// @ts-ignore
return ['$ref', 'description', 'summary'].includes(property);
};

return (
isObjectElement(element) &&
// @ts-ignore
element.hasKey('$ref') &&
// @ts-ignore
element.keys.length <= 3 &&
// @ts-ignore
all(isAllowedProperty)(element.keys)
);
// @ts-ignore
return isObjectElement(element) && element.hasKey('$ref');
};

export const isRequestBodyLikeElement = <T extends Element>(element: T): boolean => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,5 +115,14 @@ exports[`refractor elements ComponentsElement should refract to semantic ApiDOM
(ReferenceElement
(MemberElement
(StringElement)
(StringElement)))))))
(StringElement))))
(MemberElement
(StringElement)
(ReferenceElement
(MemberElement
(StringElement)
(StringElement))
(MemberElement
(StringElement)
(ObjectElement)))))))
`;
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ describe('refractor', function () {
pathItems: {
PathItem1: {},
PathItem2: { $ref: '#/components/pathsItems/PathItem1' },
PathItem3: {
$ref: '#/components/pathsItems/PathItem1',
get: {},
},
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import {
isReferenceLikeElement,
keyMap,
ReferenceElement,
PathItemElement,
SchemaElement,
isReferenceElementExternal,
isPathItemElementExternal,
isSchemaElementExternal,
} from 'apidom-ns-openapi-3-1';

Expand Down Expand Up @@ -161,6 +163,81 @@ const OpenApi3_1DereferenceVisitor = stampit({
return fragment;
},

async PathItemElement(pathItemElement: PathItemElement) {
// ignore PathItemElement without $ref field
if (!isStringElement(pathItemElement.$ref)) {
return undefined;
}

// ignore resolving external Reference Objects
if (!this.options.resolve.external && isPathItemElementExternal(pathItemElement)) {
return undefined;
}

// @ts-ignore
const reference = await this.toReference(pathItemElement.$ref.toValue());

this.indirections.push(pathItemElement);

const jsonPointer = uriToPointer(pathItemElement.$ref.toValue());

// possibly non-semantic fragment
let referencedElement = jsonPointerEvaluate(jsonPointer, reference.value.result);

// applying semantics to a fragment
if (isPrimitiveElement(referencedElement)) {
referencedElement = PathItemElement.refract(referencedElement);
}

// detect direct or indirect reference
if (this.indirections.includes(referencedElement)) {
throw new Error('Recursive JSON Pointer detected');
}

// detect maximum depth of dereferencing
if (this.indirections.length > this.options.dereference.maxDepth) {
throw new MaximumDereferenceDepthError(
`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`,
);
}

// dive deep into the fragment
const visitor: any = OpenApi3_1DereferenceVisitor({
reference,
namespace: this.namespace,
indirections: [...this.indirections],
options: this.options,
});
referencedElement = await visitAsync(referencedElement, visitor, {
keyMap,
nodeTypeGetter: getNodeType,
});

this.indirections.pop();

// merge fields from referenced Path Item with referencing one
const mergedResult = new PathItemElement(
// @ts-ignore
[...referencedElement.content],
referencedElement.meta.clone(),
referencedElement.attributes.clone(),
);
// existing keywords from referencing PathItemElement overrides ones from referenced schema
pathItemElement.forEach((value: Element, key: Element, item: Element) => {
mergedResult.remove(key.toValue());
mergedResult.content.push(item);
});
mergedResult.remove('$ref');

// annotate referencing element with info about original referenced element
mergedResult.setMetaProperty('ref-fields', {
$ref: pathItemElement.$ref?.toValue(),
});

// transclude referencing element with merged referenced element
return mergedResult;
},

async SchemaElement(referencingElement: SchemaElement) {
/**
* Skip traversal for already visited schemas and all their child schemas.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[
{
"openapi": "3.1.0",
"paths": {
"/path1": {
"summary": "path1 item summary",
"description": "path item description",
"get": {}
},
"/path2": {
"summary": "path2 item summary",
"description": "path item description",
"get": {}
}
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"openapi": "3.1.0",
"paths": {
"/path1": {
"$ref": "#/paths/~1path2",
"summary": "path1 item summary"
},
"/path2": {
"summary": "path2 item summary",
"description": "path item description",
"get": {}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"$ref": "./root.json#/paths/~1path1"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"openapi": "3.1.0",
"paths": {
"/path1": {
"$ref": "./ex.json"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"openapi": "3.1.0",
"paths": {
"/path1": {
"$ref": "#/paths/~1path2"
},
"/path2": {
"$ref": "#/paths/~1path1"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"openapi": "3.1.0",
"paths": {
"/path1": {
"summary": "path item summary",
"description": "path item description",
"get": {}
}
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"$ref": "./ex2.json#/~1path3"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"/path3": {
"summary": "path item summary",
"description": "path item description",
"get": {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"openapi": "3.1.0",
"paths": {
"/path1": {
"$ref": "./ex1.json"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"openapi": "3.1.0",
"paths": {
"/path1": {
"summary": "path item summary",
"description": "path item description",
"get": {}
}
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"/path2": {
"summary": "path item summary",
"description": "path item description",
"get": {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"openapi": "3.1.0",
"paths": {
"/path1": {
"$ref": "./ex.json#/~1path2"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
"openapi": "3.1.0",
"paths": {
"/path1": {
"$ref": "./ex.json#/~1path2"
}
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"/path2": {
"summary": "path item summary",
"description": "path item description",
"get": {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"openapi": "3.1.0",
"paths": {
"/path1": {
"$ref": "./ex.json#/~1path2"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"$ref": "./ex2.json"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"$ref": "./root.json#/paths/~1path1"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"openapi": "3.1.0",
"paths": {
"/path1": {
"$ref": "./ex1.json"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"openapi": "3.1.0",
"paths": {
"/path1": {
"$ref": "#/paths/~1path2"
},
"/path2": {
"$ref": "#/paths/~1path3"
},
"/path3": {
"$ref": "#/paths/~1path1"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[
{
"openapi": "3.1.0",
"paths": {
"/path1": {
"summary": "path item summary",
"description": "path item description",
"get": {}
},
"/path3": {
"summary": "path item summary",
"description": "path item description",
"get": {}
},
"/path4": {
"summary": "path item summary",
"description": "path item description",
"get": {}
}
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"/path2": {
"summary": "path item summary",
"description": "path item description",
"get": {}
}
}
Loading

0 comments on commit 02717c9

Please sign in to comment.