Skip to content

feat(reference): add support for OAS 3.1 Path Item dereference #460

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 29, 2021
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
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: {
Copy link
Member Author

Choose a reason for hiding this comment

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

We currently refract everything in components/pathItems object as Reference Object if it has $ref property.

Copy link
Member Author

Choose a reason for hiding this comment

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

$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