Skip to content

Commit 93e2d0f

Browse files
authored
feat(ns-openapi-3-1): make servers refractor plugin idempotent (#4152)
Refs #4134
1 parent 858cec6 commit 93e2d0f

File tree

5 files changed

+241
-25
lines changed

5 files changed

+241
-25
lines changed

packages/apidom-ns-openapi-3-1/src/refractor/plugins/normalize-servers.ts

+62-24
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Namespace } from '@swagger-api/apidom-core';
1+
import { Element } from '@swagger-api/apidom-core';
22
import {
33
PathItemServersElement,
44
OperationServersElement,
@@ -9,7 +9,8 @@ import type OpenApi3_1Element from '../../elements/OpenApi3-1';
99
import type PathItemElement from '../../elements/PathItem';
1010
import type ServerElement from '../../elements/Server';
1111
import type OperationElement from '../../elements/Operation';
12-
import type { Predicates } from '../toolbox';
12+
import type { Toolbox } from '../toolbox';
13+
import NormalizeStorage from './normalize-header-examples/NormalizeStorage';
1314

1415
/**
1516
* Override of Server Objects.
@@ -24,36 +25,60 @@ import type { Predicates } from '../toolbox';
2425
* If an alternative server object is specified at the Operation Object level, it will override PathItem.servers and OpenAPI.servers respectively.
2526
*/
2627

28+
interface PluginOptions {
29+
storageField?: string;
30+
}
31+
2732
/* eslint-disable no-param-reassign */
2833
const plugin =
29-
() =>
30-
({ predicates, namespace }: { predicates: Predicates; namespace: Namespace }) => {
34+
({ storageField = 'x-normalized' }: PluginOptions = {}) =>
35+
(toolbox: Toolbox) => {
36+
const { namespace, ancestorLineageToJSONPointer, predicates } = toolbox;
37+
let storage: NormalizeStorage | undefined;
38+
3139
return {
3240
visitor: {
33-
OpenApi3_1Element(openapiElement: OpenApi3_1Element) {
34-
const isServersUndefined = typeof openapiElement.servers === 'undefined';
35-
const isServersArrayElement = predicates.isArrayElement(openapiElement.servers);
36-
const isServersEmpty = isServersArrayElement && openapiElement.servers!.length === 0;
37-
// @ts-ignore
38-
const defaultServer = namespace.elements.Server.refract({ url: '/' });
39-
40-
if (isServersUndefined || !isServersArrayElement) {
41-
openapiElement.servers = new ServersElement([defaultServer]);
42-
} else if (isServersArrayElement && isServersEmpty) {
43-
openapiElement.servers!.push(defaultServer);
44-
}
41+
OpenApi3_1Element: {
42+
enter(openapiElement: OpenApi3_1Element) {
43+
const isServersUndefined = typeof openapiElement.servers === 'undefined';
44+
const isServersArrayElement = predicates.isArrayElement(openapiElement.servers);
45+
const isServersEmpty = isServersArrayElement && openapiElement.servers!.length === 0;
46+
// @ts-ignore
47+
const defaultServer = namespace.elements.Server.refract({ url: '/' });
48+
49+
if (isServersUndefined || !isServersArrayElement) {
50+
openapiElement.servers = new ServersElement([defaultServer]);
51+
} else if (isServersArrayElement && isServersEmpty) {
52+
openapiElement.servers!.push(defaultServer);
53+
}
54+
storage = new NormalizeStorage(openapiElement, storageField, 'servers');
55+
},
56+
leave() {
57+
storage = undefined;
58+
},
4559
},
4660
PathItemElement(
4761
pathItemElement: PathItemElement,
48-
key: any,
49-
parent: any,
50-
path: any,
51-
ancestors: any[],
62+
key: string | number,
63+
parent: Element | undefined,
64+
path: (string | number)[],
65+
ancestors: [Element | Element[]],
5266
) {
5367
// skip visiting this Path Item
5468
if (ancestors.some(predicates.isComponentsElement)) return;
5569
if (!ancestors.some(predicates.isOpenApi3_1Element)) return;
5670

71+
const pathItemJSONPointer = ancestorLineageToJSONPointer([
72+
...ancestors,
73+
parent!,
74+
pathItemElement,
75+
]);
76+
77+
// skip visiting this Path Item Object if it's already normalized
78+
if (storage!.includes(pathItemJSONPointer)) {
79+
return;
80+
}
81+
5782
const parentOpenapiElement = ancestors.find(predicates.isOpenApi3_1Element);
5883
const isServersUndefined = typeof pathItemElement.servers === 'undefined';
5984
const isServersArrayElement = predicates.isArrayElement(pathItemElement.servers);
@@ -71,19 +96,31 @@ const plugin =
7196
pathItemElement.servers!.push(server);
7297
});
7398
}
99+
storage!.append(pathItemJSONPointer);
74100
}
75101
},
76102
OperationElement(
77103
operationElement: OperationElement,
78-
key: any,
79-
parent: any,
80-
path: any,
81-
ancestors: any[],
104+
key: string | number,
105+
parent: Element | undefined,
106+
path: (string | number)[],
107+
ancestors: [Element | Element[]],
82108
) {
83109
// skip visiting this Operation
84110
if (ancestors.some(predicates.isComponentsElement)) return;
85111
if (!ancestors.some(predicates.isOpenApi3_1Element)) return;
86112

113+
const operationJSONPointer = ancestorLineageToJSONPointer([
114+
...ancestors,
115+
parent!,
116+
operationElement,
117+
]);
118+
119+
// skip visiting this Operation Object if it's already normalized
120+
if (storage!.includes(operationJSONPointer)) {
121+
return;
122+
}
123+
87124
// @TODO(vladimir.gorej@gmail.com): can be replaced by Array.prototype.findLast in future
88125
const parentPathItemElement = [...ancestors].reverse().find(predicates.isPathItemElement);
89126
const isServersUndefined = typeof operationElement.servers === 'undefined';
@@ -102,6 +139,7 @@ const plugin =
102139
operationElement.servers!.push(server);
103140
});
104141
}
142+
storage!.append(operationJSONPointer);
105143
}
106144
},
107145
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`refractor plugins normalize-servers should have idempotent characteristics 1`] = `
4+
Object {
5+
openapi: 3.1.0,
6+
paths: Object {
7+
/: Object {
8+
get: Object {},
9+
},
10+
},
11+
servers: Array [
12+
Object {
13+
description: production server,
14+
url: https://example.com/,
15+
},
16+
],
17+
}
18+
`;

packages/apidom-ns-openapi-3-1/test/refractor/plugins/normalize-servers/__snapshots__/index.ts.snap

+61-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,15 @@ exports[`refractor plugins normalize-servers given OpenAPI.server defined should
4343
(StringElement))
4444
(MemberElement
4545
(StringElement)
46-
(StringElement))))))))))
46+
(StringElement)))))))))
47+
(MemberElement
48+
(StringElement)
49+
(ObjectElement
50+
(MemberElement
51+
(StringElement)
52+
(ArrayElement
53+
(StringElement)
54+
(StringElement))))))
4755
`;
4856

4957
exports[`refractor plugins normalize-servers given OpenAPI.servers defined and PathItem.servers defined should duplicate Server Objects from PathItem.servers 1`] = `
@@ -517,6 +525,42 @@ exports[`refractor plugins normalize-servers given OpenAPI.servers defined and P
517525
]
518526
}
519527
}
528+
},
529+
{
530+
"element": "member",
531+
"content": {
532+
"key": {
533+
"element": "string",
534+
"content": "x-normalized"
535+
},
536+
"value": {
537+
"element": "object",
538+
"content": [
539+
{
540+
"element": "member",
541+
"content": {
542+
"key": {
543+
"element": "string",
544+
"content": "servers"
545+
},
546+
"value": {
547+
"element": "array",
548+
"content": [
549+
{
550+
"element": "string",
551+
"content": "/paths/~1"
552+
},
553+
{
554+
"element": "string",
555+
"content": "/paths/~1/get"
556+
}
557+
]
558+
}
559+
}
560+
}
561+
]
562+
}
563+
}
520564
}
521565
]
522566
}
@@ -561,6 +605,14 @@ exports[`refractor plugins normalize-servers given PathItem.servers defined shou
561605
(ArrayElement
562606
(ServerElement
563607
(MemberElement
608+
(StringElement)
609+
(StringElement)))))
610+
(MemberElement
611+
(StringElement)
612+
(ObjectElement
613+
(MemberElement
614+
(StringElement)
615+
(ArrayElement
564616
(StringElement)
565617
(StringElement))))))
566618
`;
@@ -598,6 +650,14 @@ exports[`refractor plugins normalize-servers given no servers field is defined s
598650
(ArrayElement
599651
(ServerElement
600652
(MemberElement
653+
(StringElement)
654+
(StringElement)))))
655+
(MemberElement
656+
(StringElement)
657+
(ObjectElement
658+
(MemberElement
659+
(StringElement)
660+
(ArrayElement
601661
(StringElement)
602662
(StringElement))))))
603663
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { expect } from 'chai';
2+
import dedent from 'dedent';
3+
import { toValue, dispatchRefractorPlugins } from '@swagger-api/apidom-core';
4+
import { parse } from '@swagger-api/apidom-parser-adapter-yaml-1-2';
5+
6+
import {
7+
createToolbox,
8+
OpenApi3_1Element,
9+
refractorPluginNormalizeServers,
10+
keyMap,
11+
getNodeType,
12+
} from '../../../../src';
13+
14+
describe('refractor', function () {
15+
context('plugins', function () {
16+
context('normalize-servers', function () {
17+
specify('should have idempotent characteristics', async function () {
18+
const yamlDefinition = dedent`
19+
openapi: 3.1.0
20+
servers:
21+
- url: https://example.com/
22+
description: production server
23+
paths:
24+
/:
25+
get: {}
26+
`;
27+
const apiDOM = await parse(yamlDefinition);
28+
const openApiElement = OpenApi3_1Element.refract(apiDOM.result) as OpenApi3_1Element;
29+
const options = {
30+
toolboxCreator: createToolbox,
31+
visitorOptions: { keyMap, nodeTypeGetter: getNodeType },
32+
};
33+
34+
dispatchRefractorPlugins(openApiElement, [refractorPluginNormalizeServers()], options);
35+
dispatchRefractorPlugins(openApiElement, [refractorPluginNormalizeServers()], options);
36+
dispatchRefractorPlugins(openApiElement, [refractorPluginNormalizeServers()], options);
37+
38+
expect(toValue(apiDOM.result)).toMatchSnapshot();
39+
});
40+
});
41+
});
42+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { assert } from 'chai';
2+
import dedent from 'dedent';
3+
import { toValue } from '@swagger-api/apidom-core';
4+
import { parse } from '@swagger-api/apidom-parser-adapter-yaml-1-2';
5+
6+
import { OpenApi3_1Element, refractorPluginNormalizeServers } from '../../../../src';
7+
8+
describe('refractor', function () {
9+
context('plugins', function () {
10+
context('normalize-servers', function () {
11+
specify('should use sub-field to store normalized scopes', async function () {
12+
const yamlDefinition = dedent`
13+
openapi: 3.1.0
14+
servers:
15+
- url: https://example.com/
16+
description: production server
17+
paths:
18+
/:
19+
get: {}
20+
`;
21+
const apiDOM = await parse(yamlDefinition);
22+
const openApiElement = OpenApi3_1Element.refract(apiDOM.result, {
23+
plugins: [refractorPluginNormalizeServers()],
24+
}) as OpenApi3_1Element;
25+
26+
assert.deepEqual(toValue(openApiElement.get('x-normalized')), {
27+
servers: ['/paths/~1', '/paths/~1/get'],
28+
});
29+
});
30+
31+
context('given custom storage field', function () {
32+
specify('should use custom storage field to store normalized scopes', async function () {
33+
const yamlDefinition = dedent`
34+
openapi: 3.1.0
35+
servers:
36+
- url: https://example.com/
37+
description: production server
38+
paths:
39+
/:
40+
get: {}
41+
`;
42+
const apiDOM = await parse(yamlDefinition);
43+
const openApiElement = OpenApi3_1Element.refract(apiDOM.result, {
44+
plugins: [
45+
refractorPluginNormalizeServers({
46+
storageField: '$$normalized',
47+
}),
48+
],
49+
}) as OpenApi3_1Element;
50+
51+
assert.deepEqual(toValue(openApiElement.get('$$normalized')), {
52+
servers: ['/paths/~1', '/paths/~1/get'],
53+
});
54+
});
55+
});
56+
});
57+
});
58+
});

0 commit comments

Comments
 (0)