@redocly/openapi-core 1.0.0-beta.77 → 1.0.0-beta.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,234 @@
1
+ import { outdent } from 'outdent';
2
+ import { bundleDocument } from '../../bundle'
3
+ import { BaseResolver } from '../../resolve';
4
+ import { parseYamlToDocument, yamlSerializer } from '../../../__tests__/utils';
5
+ import { makeConfig } from './config';
6
+
7
+ describe('oas3 remove-x-internal', () => {
8
+ expect.addSnapshotSerializer(yamlSerializer);
9
+ const testDocument = parseYamlToDocument(
10
+ outdent`
11
+ openapi: 3.0.0
12
+ paths:
13
+ /pet:
14
+ removeit: true
15
+ get:
16
+ parameters:
17
+ - $ref: '#/components/parameters/x'
18
+ components:
19
+ parameters:
20
+ x:
21
+ name: x
22
+ `);
23
+
24
+ it('should use `internalFlagProperty` option to remove internal paths', async () => {
25
+ const { bundle: res } = await bundleDocument({
26
+ document: testDocument,
27
+ externalRefResolver: new BaseResolver(),
28
+ config: makeConfig({}, { 'remove-x-internal': { 'internalFlagProperty': 'removeit' } })
29
+ });
30
+ expect(res.parsed).toMatchInlineSnapshot(
31
+ `
32
+ openapi: 3.0.0
33
+ components:
34
+ parameters:
35
+ x:
36
+ name: x
37
+
38
+ `);
39
+ });
40
+
41
+ it('should clean types: Server, Operation, Parameter, PathItem, Example', async () => {
42
+ const testDoc = parseYamlToDocument(
43
+ outdent`
44
+ openapi: 3.1.0
45
+ servers:
46
+ - url: //petstore.swagger.io/v2
47
+ description: Default server
48
+ x-internal: true
49
+ paths:
50
+ /pet:
51
+ get:
52
+ x-internal: true
53
+ operationId: getPet
54
+ parameters:
55
+ - $ref: '#/components/parameters/x'
56
+ put:
57
+ parameters:
58
+ - name: Accept-Language
59
+ x-internal: true
60
+ in: header
61
+ example: en-US
62
+ required: false
63
+ - name: cookieParam
64
+ x-internal: true
65
+ in: cookie
66
+ description: Some cookie
67
+ required: true
68
+ /admin:
69
+ x-internal: true
70
+ post:
71
+ parameters:
72
+ - $ref: '#/components/parameters/y'
73
+ /store/order:
74
+ post:
75
+ operationId: placeOrder
76
+ responses:
77
+ '200':
78
+ description: successful operation
79
+ content:
80
+ application/json:
81
+ examples:
82
+ response:
83
+ x-internal: true
84
+ value: OK
85
+ components:
86
+ parameters:
87
+ x:
88
+ name: x
89
+ y:
90
+ name: y
91
+ `);
92
+ const { bundle: res } = await bundleDocument({
93
+ document: testDoc,
94
+ externalRefResolver: new BaseResolver(),
95
+ config: makeConfig({}, { 'remove-x-internal': 'on' })
96
+ });
97
+ expect(res.parsed).toMatchInlineSnapshot(
98
+ `
99
+ openapi: 3.1.0
100
+ paths:
101
+ /pet:
102
+ put: {}
103
+ /store/order:
104
+ post:
105
+ operationId: placeOrder
106
+ responses:
107
+ '200':
108
+ description: successful operation
109
+ content:
110
+ application/json: {}
111
+ components:
112
+ parameters:
113
+ x:
114
+ name: x
115
+ 'y':
116
+ name: 'y'
117
+
118
+ `
119
+ );
120
+ });
121
+
122
+ it('should clean types: Schema, Response, RequestBody, MediaType, Callback', async () => {
123
+ const testDoc = parseYamlToDocument(
124
+ outdent`
125
+ openapi: 3.1.0
126
+ paths:
127
+ /pet:
128
+ post:
129
+ summary: test
130
+ requestBody:
131
+ x-internal: true
132
+ content:
133
+ application/x-www-form-urlencoded:
134
+ schema:
135
+ type: object
136
+ /store/order:
137
+ post:
138
+ operationId: storeOrder
139
+ parameters:
140
+ - name: api_key
141
+ schema:
142
+ x-internal: true
143
+ type: string
144
+ responses:
145
+ '200':
146
+ x-internal: true
147
+ content:
148
+ application/json:
149
+ examples:
150
+ response:
151
+ value: OK
152
+ requestBody:
153
+ content:
154
+ application/x-www-form-urlencoded:
155
+ x-internal: true
156
+ schema:
157
+ type: object
158
+ callbacks:
159
+ orderInProgress:
160
+ x-internal: true
161
+ '{$request.body#/callbackUrl}?event={$request.body#/eventName}':
162
+ servers:
163
+ - url: //callback-url.path-level/v1
164
+ description: Path level server
165
+ `);
166
+ const { bundle: res } = await bundleDocument({
167
+ document: testDoc,
168
+ externalRefResolver: new BaseResolver(),
169
+ config: makeConfig({}, { 'remove-x-internal': 'on' })
170
+ });
171
+ expect(res.parsed).toMatchInlineSnapshot(
172
+ `
173
+ openapi: 3.1.0
174
+ paths:
175
+ /pet:
176
+ post:
177
+ summary: test
178
+ /store/order:
179
+ post:
180
+ operationId: storeOrder
181
+ parameters:
182
+ - name: api_key
183
+ requestBody: {}
184
+ components: {}
185
+
186
+ `);
187
+ });
188
+ });
189
+
190
+ describe('oas2 remove-x-internal', () => {
191
+ it('should clean types - base test', async () => {
192
+ const testDoc = parseYamlToDocument(
193
+ outdent`
194
+ swagger: '2.0'
195
+ host: api.instagram.com
196
+ paths:
197
+ '/geographies/{geo-id}/media/recent':
198
+ get:
199
+ parameters:
200
+ - description: The geography ID.
201
+ x-internal: true
202
+ in: path
203
+ name: geo-id
204
+ required: true
205
+ type: string
206
+ - description: Max number of media to return.
207
+ x-internal: true
208
+ format: int32
209
+ in: query
210
+ name: count
211
+ required: false
212
+ type: integer
213
+ responses:
214
+ '200':
215
+ x-internal: true
216
+ description: List of recent media entries.
217
+ `);
218
+ const { bundle: res } = await bundleDocument({
219
+ document: testDoc,
220
+ externalRefResolver: new BaseResolver(),
221
+ config: makeConfig({}, { 'remove-x-internal': 'on' })
222
+ });
223
+ expect(res.parsed).toMatchInlineSnapshot(
224
+ `
225
+ swagger: '2.0'
226
+ host: api.instagram.com
227
+ paths:
228
+ /geographies/{geo-id}/media/recent:
229
+ get: {}
230
+
231
+ `
232
+ );
233
+ });
234
+ });
@@ -0,0 +1,42 @@
1
+ import { Oas3Decorator, Oas2Decorator } from '../../visitors';
2
+ import { isEmptyArray, isEmptyObject, isPlainObject } from '../../utils';
3
+ import { UserContext } from '../../walk';
4
+
5
+ const DEFAULT_INTERNAL_PROPERTY_NAME = 'x-internal';
6
+
7
+ export const RemoveXInternal: Oas3Decorator | Oas2Decorator = ({ internalFlagProperty }) => {
8
+ const hiddenTag = internalFlagProperty || DEFAULT_INTERNAL_PROPERTY_NAME;
9
+
10
+ function removeInternal(node: any, ctx: UserContext) {
11
+ const { parent, key } = ctx;
12
+ let didDelete = false;
13
+ if (Array.isArray(node)) {
14
+ for (let i = 0; i < node.length; i++) {
15
+ if (node[i] && node[i][hiddenTag]) {
16
+ node.splice(i, 1);
17
+ didDelete = true;
18
+ i--;
19
+ }
20
+ }
21
+ } else if (isPlainObject(node)) {
22
+ for (const key of Object.keys(node)) {
23
+ if ((node as any)[key][hiddenTag]) {
24
+ delete (node as any)[key];
25
+ didDelete = true;
26
+ }
27
+ }
28
+ }
29
+
30
+ if (didDelete && (isEmptyObject(node) || isEmptyArray(node))) {
31
+ delete parent[key];
32
+ }
33
+ }
34
+
35
+ return {
36
+ any: {
37
+ enter: (node, ctx) => {
38
+ removeInternal(node, ctx);
39
+ }
40
+ }
41
+ }
42
+ }
@@ -1,3 +1,4 @@
1
+ import { Oas2Decorator, Oas2Rule } from '../../visitors';
1
2
  import { OasSpec } from '../common/spec';
2
3
  import { NoInvalidSchemaExamples } from '../common/no-invalid-schema-examples';
3
4
  import { NoInvalidParameterExamples } from '../common/no-invalid-parameter-examples';
@@ -26,7 +27,6 @@ import { OperationSingularTag } from '../common/operation-singular-tag';
26
27
  import { OperationSecurityDefined } from '../common/operation-security-defined';
27
28
  import { NoUnresolvedRefs } from '../no-unresolved-refs';
28
29
  import { PathHttpVerbsOrder } from '../common/path-http-verbs-order';
29
- import { Oas2Decorator, Oas2Rule } from '../../visitors';
30
30
  import { RegistryDependencies } from '../common/registry-dependencies';
31
31
  import { NoIdenticalPaths } from '../common/no-identical-paths';
32
32
  import { OperationOperationId } from '../common/operation-operationId';
@@ -40,6 +40,7 @@ import { PathSegmentPlural } from '../common/path-segment-plural';
40
40
  import { OperationDescriptionOverride } from '../common/operation-description-override';
41
41
  import { TagDescriptionOverride } from '../common/tag-description-override';
42
42
  import { InfoDescriptionOverride } from '../common/info-description-override';
43
+ import { RemoveXInternal } from '../common/remove-x-internal';
43
44
 
44
45
  export const rules = {
45
46
  spec: OasSpec as Oas2Rule,
@@ -88,4 +89,5 @@ export const decorators = {
88
89
  'operation-description-override': OperationDescriptionOverride as Oas2Decorator,
89
90
  'tag-description-override': TagDescriptionOverride as Oas2Decorator,
90
91
  'info-description-override': InfoDescriptionOverride as Oas2Decorator,
92
+ 'remove-x-internal': RemoveXInternal as Oas2Decorator
91
93
  };
@@ -0,0 +1,74 @@
1
+ import { Oas2Rule } from '../../visitors';
2
+ import { Location } from '../../ref-utils';
3
+ import { Oas2Components } from '../../typings/swagger';
4
+ import { isEmptyObject } from '../../utils';
5
+
6
+ export const RemoveUnusedComponents: Oas2Rule = () => {
7
+ let components = new Map<string, { used: boolean; componentType?: keyof Oas2Components; name: string }>();
8
+
9
+ function registerComponent(location: Location, componentType: keyof Oas2Components, name: string): void {
10
+ components.set(location.absolutePointer, {
11
+ used: components.get(location.absolutePointer)?.used || false,
12
+ componentType,
13
+ name,
14
+ });
15
+ }
16
+
17
+ return {
18
+ ref(ref, { type, resolve, key }) {
19
+ if (
20
+ ['Schema', 'Parameter', 'Response', 'SecurityScheme'].includes(type.name)
21
+ ) {
22
+ const resolvedRef = resolve(ref);
23
+ if (!resolvedRef.location) return;
24
+ components.set(resolvedRef.location.absolutePointer, {
25
+ used: true,
26
+ name: key.toString(),
27
+ });
28
+ }
29
+ },
30
+ DefinitionRoot: {
31
+ leave(root, ctx) {
32
+ const data = ctx.getVisitorData() as { removedCount: number };
33
+ data.removedCount = 0;
34
+
35
+ let rootComponents = new Set<keyof Oas2Components>();
36
+ components.forEach(usageInfo => {
37
+ const { used, name, componentType } = usageInfo;
38
+ if (!used && componentType) {
39
+ rootComponents.add(componentType);
40
+ delete root[componentType]![name];
41
+ data.removedCount++;
42
+ }
43
+ });
44
+ for (const component of rootComponents) {
45
+ if (isEmptyObject(root[component])) {
46
+ delete root[component];
47
+ }
48
+ }
49
+ },
50
+ },
51
+ NamedSchemas: {
52
+ Schema(schema, { location, key }) {
53
+ if (!schema.allOf) {
54
+ registerComponent(location, 'definitions', key.toString());
55
+ }
56
+ },
57
+ },
58
+ NamedParameters: {
59
+ Parameter(_parameter, { location, key }) {
60
+ registerComponent(location, 'parameters', key.toString());
61
+ },
62
+ },
63
+ NamedResponses: {
64
+ Response(_response, { location, key }) {
65
+ registerComponent(location, 'responses', key.toString());
66
+ },
67
+ },
68
+ NamedSecuritySchemes: {
69
+ SecurityScheme(_securityScheme, { location, key }) {
70
+ registerComponent(location, 'securityDefinitions', key.toString());
71
+ },
72
+ }
73
+ };
74
+ };
@@ -49,6 +49,7 @@ import { InfoDescriptionOverride } from '../common/info-description-override';
49
49
  import { PathExcludesPatterns } from '../common/path-excludes-patterns';
50
50
  import { NoInvalidSchemaExamples } from '../common/no-invalid-schema-examples';
51
51
  import { NoInvalidParameterExamples } from '../common/no-invalid-parameter-examples';
52
+ import { RemoveXInternal } from '../common/remove-x-internal';
52
53
 
53
54
  export const rules = {
54
55
  spec: OasSpec,
@@ -106,4 +107,5 @@ export const decorators = {
106
107
  'operation-description-override': OperationDescriptionOverride as Oas3Decorator,
107
108
  'tag-description-override': TagDescriptionOverride as Oas3Decorator,
108
109
  'info-description-override': InfoDescriptionOverride as Oas3Decorator,
110
+ 'remove-x-internal': RemoveXInternal as Oas3Decorator
109
111
  };
@@ -0,0 +1,82 @@
1
+ import { Oas3Rule } from '../../visitors';
2
+ import { Location } from '../../ref-utils';
3
+ import { Oas3Components } from '../../typings/openapi'
4
+ import { isEmptyObject } from '../../utils';
5
+
6
+ export const RemoveUnusedComponents: Oas3Rule = () => {
7
+ let components = new Map<string, { used: boolean; componentType?: keyof Oas3Components; name: string }>();
8
+
9
+ function registerComponent(location: Location, componentType: keyof Oas3Components, name: string): void {
10
+ components.set(location.absolutePointer, {
11
+ used: components.get(location.absolutePointer)?.used || false,
12
+ componentType,
13
+ name,
14
+ });
15
+ }
16
+
17
+ return {
18
+ ref(ref, { type, resolve, key }) {
19
+ if (
20
+ ['Schema', 'Header', 'Parameter', 'Response', 'Example', 'RequestBody'].includes(type.name)
21
+ ) {
22
+ const resolvedRef = resolve(ref);
23
+ if (!resolvedRef.location) return;
24
+ components.set(resolvedRef.location.absolutePointer, {
25
+ used: true,
26
+ name: key.toString(),
27
+ });
28
+ }
29
+ },
30
+ DefinitionRoot: {
31
+ leave(root, ctx) {
32
+ const data = ctx.getVisitorData() as { removedCount: number };
33
+ data.removedCount = 0;
34
+
35
+ components.forEach(usageInfo => {
36
+ const { used, componentType, name } = usageInfo;
37
+ if (!used && componentType) {
38
+ let componentChild = root.components![componentType];
39
+ delete componentChild![name];
40
+ data.removedCount++;
41
+ if (isEmptyObject(componentChild)) {
42
+ delete root.components![componentType];
43
+ }
44
+ }
45
+ });
46
+ if (isEmptyObject(root.components)) { delete root.components; }
47
+ },
48
+ },
49
+ NamedSchemas: {
50
+ Schema(schema, { location, key }) {
51
+ if (!schema.allOf) {
52
+ registerComponent(location, 'schemas', key.toString());
53
+ }
54
+ },
55
+ },
56
+ NamedParameters: {
57
+ Parameter(_parameter, { location, key }) {
58
+ registerComponent(location, 'parameters', key.toString());
59
+ },
60
+ },
61
+ NamedResponses: {
62
+ Response(_response, { location, key }) {
63
+ registerComponent(location, 'responses', key.toString());
64
+ },
65
+ },
66
+ NamedExamples: {
67
+ Example(_example, { location, key }) {
68
+ registerComponent(location, 'examples', key.toString());
69
+ },
70
+ },
71
+ NamedRequestBodies: {
72
+ RequestBody(_requestBody, { location, key }) {
73
+ registerComponent(location, 'requestBodies', key.toString());
74
+ },
75
+ },
76
+ NamedHeaders: {
77
+ Header(_header, { location, key }) {
78
+ registerComponent(location, 'headers', key.toString());
79
+ },
80
+ },
81
+ };
82
+ };
package/src/types/oas2.ts CHANGED
@@ -383,13 +383,10 @@ export const Oas2Types: Record<string, NodeType> = {
383
383
  Schema,
384
384
  Xml,
385
385
  SchemaProperties,
386
-
387
386
  NamedSchemas: mapOf('Schema'),
388
387
  NamedResponses: mapOf('Response'),
389
388
  NamedParameters: mapOf('Parameter'),
390
389
  NamedSecuritySchemes: mapOf('SecurityScheme'),
391
-
392
390
  SecurityScheme,
393
-
394
391
  XCodeSample,
395
392
  };
@@ -20,6 +20,13 @@ export interface Oas2Definition {
20
20
  externalDocs?: Oas2ExternalDocs;
21
21
  }
22
22
 
23
+ export interface Oas2Components {
24
+ definitions?: { [name: string]: Record<string, Oas2Schema> };
25
+ securityDefinitions?: { [name: string]: Record<string, Oas2SecurityScheme> };
26
+ responses?: { [name: string]: Record<string, Oas2Response> };
27
+ parameters?: { [name: string]: Record<string, Oas2Parameter> };
28
+ }
29
+
23
30
  export interface Oas2Info {
24
31
  title: string;
25
32
  version: string;
package/src/utils.ts CHANGED
@@ -40,6 +40,14 @@ export function isPlainObject(value: any): value is object {
40
40
  return value !== null && typeof value === 'object' && !Array.isArray(value);
41
41
  }
42
42
 
43
+ export function isEmptyObject(value: any): value is object {
44
+ return isPlainObject(value) && Object.keys(value).length === 0;
45
+ }
46
+
47
+ export function isEmptyArray(value: any) {
48
+ return Array.isArray(value) && value.length === 0;
49
+ }
50
+
43
51
  export async function readFileFromUrl(url: string, config: HttpResolveConfig) {
44
52
  const headers: Record<string, string> = {};
45
53
  for (const header of config.headers) {
package/src/walk.ts CHANGED
@@ -212,16 +212,14 @@ export function walkDocument<T>(opts: {
212
212
  if (!activatedOn.skipped) {
213
213
  visitedBySome = true;
214
214
  enteredContexts.add(context);
215
- const ignoreNextVisitorsOnNode = visitWithContext(
215
+ const { ignoreNextVisitorsOnNode } = visitWithContext(
216
216
  visit,
217
217
  resolvedNode,
218
218
  context,
219
219
  ruleId,
220
220
  severity,
221
221
  );
222
- if (ignoreNextVisitorsOnNode) {
223
- break;
224
- }
222
+ if (ignoreNextVisitorsOnNode) break;
225
223
  }
226
224
  }
227
225
  }
@@ -358,16 +356,13 @@ export function walkDocument<T>(opts: {
358
356
  key,
359
357
  parentLocations: collectParentsLocations(context),
360
358
  oasVersion: ctx.oasVersion,
361
- ignoreNextVisitorsOnNode: () => {
362
- ignoreNextVisitorsOnNode = true;
363
- },
359
+ ignoreNextVisitorsOnNode: () => { ignoreNextVisitorsOnNode = true; },
364
360
  getVisitorData: getVisitorDataFn.bind(undefined, ruleId),
365
361
  },
366
362
  collectParents(context),
367
363
  context,
368
364
  );
369
-
370
- return ignoreNextVisitorsOnNode;
365
+ return { ignoreNextVisitorsOnNode };
371
366
  }
372
367
 
373
368
  function resolve<T>(