@redocly/openapi-core 1.25.8 → 1.25.10

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @redocly/openapi-core
2
2
 
3
+ ## 1.25.10
4
+
5
+ ### Patch Changes
6
+
7
+ - Fixed `component-name-unique` problems to include correct location.
8
+ - Fixed the `remove-x-internal` decorator, which was not removing the reference in the corresponding discriminator mapping while removing the original `$ref`.
9
+ - Updated @redocly/config to v0.16.0.
10
+
11
+ ## 1.25.9
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated @redocly/config to v0.15.0.
16
+
3
17
  ## 1.25.8
4
18
 
5
19
  ### Patch Changes
@@ -11,7 +11,7 @@ const utils_2 = require("./utils");
11
11
  const ref_utils_1 = require("../ref-utils");
12
12
  exports.IGNORE_FILE = '.redocly.lint-ignore.yaml';
13
13
  const IGNORE_BANNER = `# This file instructs Redocly's linter to ignore the rules contained for specific parts of your API.\n` +
14
- `# See https://redoc.ly/docs/cli/ for more information.\n`;
14
+ `# See https://redocly.com/docs/cli/ for more information.\n`;
15
15
  function getIgnoreFilePath(configFile) {
16
16
  if (configFile) {
17
17
  return (0, utils_1.doesYamlFileExist)(configFile)
@@ -4,22 +4,29 @@ exports.RemoveXInternal = void 0;
4
4
  const utils_1 = require("../../utils");
5
5
  const ref_utils_1 = require("../../ref-utils");
6
6
  const DEFAULT_INTERNAL_PROPERTY_NAME = 'x-internal';
7
- const RemoveXInternal = ({ internalFlagProperty }) => {
8
- const hiddenTag = internalFlagProperty || DEFAULT_INTERNAL_PROPERTY_NAME;
9
- function removeInternal(node, ctx) {
7
+ const RemoveXInternal = ({ internalFlagProperty = DEFAULT_INTERNAL_PROPERTY_NAME, }) => {
8
+ function removeInternal(node, ctx, originalMapping) {
10
9
  const { parent, key } = ctx;
11
10
  let didDelete = false;
12
11
  if (Array.isArray(node)) {
13
12
  for (let i = 0; i < node.length; i++) {
14
13
  if ((0, ref_utils_1.isRef)(node[i])) {
15
14
  const resolved = ctx.resolve(node[i]);
16
- if (resolved.node?.[hiddenTag]) {
15
+ if (resolved.node?.[internalFlagProperty]) {
16
+ // First, remove the reference in the discriminator mapping, if it exists:
17
+ if ((0, utils_1.isPlainObject)(parent.discriminator?.mapping)) {
18
+ for (const mapping in parent.discriminator.mapping) {
19
+ if (originalMapping?.[mapping] === node[i].$ref) {
20
+ delete parent.discriminator.mapping[mapping];
21
+ }
22
+ }
23
+ }
17
24
  node.splice(i, 1);
18
25
  didDelete = true;
19
26
  i--;
20
27
  }
21
28
  }
22
- if (node[i]?.[hiddenTag]) {
29
+ if (node[i]?.[internalFlagProperty]) {
23
30
  node.splice(i, 1);
24
31
  didDelete = true;
25
32
  i--;
@@ -28,15 +35,14 @@ const RemoveXInternal = ({ internalFlagProperty }) => {
28
35
  }
29
36
  else if ((0, utils_1.isPlainObject)(node)) {
30
37
  for (const key of Object.keys(node)) {
31
- node = node;
32
38
  if ((0, ref_utils_1.isRef)(node[key])) {
33
39
  const resolved = ctx.resolve(node[key]);
34
- if (resolved.node?.[hiddenTag]) {
40
+ if ((0, utils_1.isPlainObject)(resolved.node) && resolved.node?.[internalFlagProperty]) {
35
41
  delete node[key];
36
42
  didDelete = true;
37
43
  }
38
44
  }
39
- if (node[key]?.[hiddenTag]) {
45
+ if ((0, utils_1.isPlainObject)(node[key]) && node[key]?.[internalFlagProperty]) {
40
46
  delete node[key];
41
47
  didDelete = true;
42
48
  }
@@ -46,10 +52,16 @@ const RemoveXInternal = ({ internalFlagProperty }) => {
46
52
  delete parent[key];
47
53
  }
48
54
  }
55
+ let originalMapping = {};
49
56
  return {
57
+ DiscriminatorMapping: {
58
+ enter: (mapping) => {
59
+ originalMapping = structuredClone(mapping);
60
+ },
61
+ },
50
62
  any: {
51
63
  enter: (node, ctx) => {
52
- removeInternal(node, ctx);
64
+ removeInternal(node, ctx, originalMapping);
53
65
  },
54
66
  },
55
67
  };
package/lib/resolve.js CHANGED
@@ -101,7 +101,7 @@ class BaseResolver {
101
101
  }
102
102
  else {
103
103
  if (fs.lstatSync(absoluteRef).isDirectory()) {
104
- throw new Error(`Expected a file but received a folder at ${absoluteRef}`);
104
+ throw new Error(`Expected a file but received a folder at ${absoluteRef}.`);
105
105
  }
106
106
  const content = await fs.promises.readFile(absoluteRef, 'utf-8');
107
107
  // In some cases file have \r\n line delimeters like on windows, we should skip it.
@@ -165,7 +165,7 @@ const resolvableScalarType = { name: 'scalar', properties: {} };
165
165
  async function resolveDocument(opts) {
166
166
  const { rootDocument, externalRefResolver, rootType } = opts;
167
167
  const resolvedRefMap = new Map();
168
- const seedNodes = new Set(); // format "${type}::${absoluteRef}${pointer}"
168
+ const seenNodes = new Set(); // format "${type}::${absoluteRef}${pointer}"
169
169
  const resolvePromises = [];
170
170
  resolveRefsInParallel(rootDocument.parsed, rootDocument, '#/', rootType);
171
171
  let resolved;
@@ -182,10 +182,10 @@ async function resolveDocument(opts) {
182
182
  return;
183
183
  }
184
184
  const nodeId = `${type.name}::${nodeAbsoluteRef}`;
185
- if (seedNodes.has(nodeId)) {
185
+ if (seenNodes.has(nodeId)) {
186
186
  return;
187
187
  }
188
- seedNodes.add(nodeId);
188
+ seenNodes.add(nodeId);
189
189
  const [_, anchor] = Object.entries(node).find(([key]) => key === '$anchor') || [];
190
190
  if (anchor) {
191
191
  anchorRefsMap.set(`#${anchor}`, node);
@@ -34,27 +34,31 @@ const ComponentNameUnique = (options) => {
34
34
  const resolvedRef = resolve(ref);
35
35
  if (!resolvedRef.location)
36
36
  return;
37
- addComponentFromAbsoluteLocation(typeName, resolvedRef.location.absolutePointer.toString());
37
+ addComponentFromAbsoluteLocation(typeName, resolvedRef.location);
38
38
  }
39
39
  },
40
40
  },
41
41
  Root: {
42
42
  leave(root, ctx) {
43
43
  components.forEach((value, key, _) => {
44
- if (value.size > 1) {
44
+ if (value.absolutePointers.size > 1) {
45
45
  const component = getComponentFromKey(key);
46
46
  const optionComponentName = getOptionComponentNameForTypeName(component.typeName);
47
- const definitions = Array.from(value)
48
- .map((v) => `- ${v}`)
49
- .join('\n');
50
- const problem = {
51
- message: `Component '${optionComponentName}/${component.componentName}' is not unique. It is defined at:\n${definitions}`,
52
- };
53
47
  const componentSeverity = optionComponentName ? options[optionComponentName] : null;
54
- if (componentSeverity) {
55
- problem.forceSeverity = componentSeverity;
48
+ for (const location of value.locations) {
49
+ const definitions = Array.from(value.absolutePointers)
50
+ .filter((v) => v !== location.absolutePointer.toString())
51
+ .map((v) => `- ${v}`)
52
+ .join('\n');
53
+ const problem = {
54
+ message: `Component '${optionComponentName}/${component.componentName}' is not unique. It is also defined at:\n${definitions}`,
55
+ location: location,
56
+ };
57
+ if (componentSeverity) {
58
+ problem.forceSeverity = componentSeverity;
59
+ }
60
+ ctx.report(problem);
56
61
  }
57
- ctx.report(problem);
58
62
  }
59
63
  });
60
64
  },
@@ -63,28 +67,28 @@ const ComponentNameUnique = (options) => {
63
67
  if (options.schemas != 'off') {
64
68
  rule.NamedSchemas = {
65
69
  Schema(_, { location }) {
66
- addComponentFromAbsoluteLocation(TYPE_NAME_SCHEMA, location.absolutePointer.toString());
70
+ addComponentFromAbsoluteLocation(TYPE_NAME_SCHEMA, location);
67
71
  },
68
72
  };
69
73
  }
70
74
  if (options.responses != 'off') {
71
75
  rule.NamedResponses = {
72
76
  Response(_, { location }) {
73
- addComponentFromAbsoluteLocation(TYPE_NAME_RESPONSE, location.absolutePointer.toString());
77
+ addComponentFromAbsoluteLocation(TYPE_NAME_RESPONSE, location);
74
78
  },
75
79
  };
76
80
  }
77
81
  if (options.parameters != 'off') {
78
82
  rule.NamedParameters = {
79
83
  Parameter(_, { location }) {
80
- addComponentFromAbsoluteLocation(TYPE_NAME_PARAMETER, location.absolutePointer.toString());
84
+ addComponentFromAbsoluteLocation(TYPE_NAME_PARAMETER, location);
81
85
  },
82
86
  };
83
87
  }
84
88
  if (options.requestBodies != 'off') {
85
89
  rule.NamedRequestBodies = {
86
90
  RequestBody(_, { location }) {
87
- addComponentFromAbsoluteLocation(TYPE_NAME_REQUEST_BODY, location.absolutePointer.toString());
91
+ addComponentFromAbsoluteLocation(TYPE_NAME_REQUEST_BODY, location);
88
92
  },
89
93
  };
90
94
  }
@@ -98,15 +102,22 @@ const ComponentNameUnique = (options) => {
98
102
  }
99
103
  return componentName;
100
104
  }
101
- function addFoundComponent(typeName, componentName, absoluteLocation) {
105
+ function addFoundComponent(typeName, componentName, location) {
102
106
  const key = getKeyForComponent(typeName, componentName);
103
- const locations = components.get(key) ?? new Set();
104
- locations.add(absoluteLocation);
105
- components.set(key, locations);
107
+ const entry = components.get(key) ?? {
108
+ absolutePointers: new Set(),
109
+ locations: [],
110
+ };
111
+ const absoluteLocation = location.absolutePointer.toString();
112
+ if (!entry.absolutePointers.has(absoluteLocation)) {
113
+ entry.absolutePointers.add(absoluteLocation);
114
+ entry.locations.push(location);
115
+ }
116
+ components.set(key, entry);
106
117
  }
107
- function addComponentFromAbsoluteLocation(typeName, absoluteLocation) {
108
- const componentName = getComponentNameFromAbsoluteLocation(absoluteLocation);
109
- addFoundComponent(typeName, componentName, absoluteLocation);
118
+ function addComponentFromAbsoluteLocation(typeName, location) {
119
+ const componentName = getComponentNameFromAbsoluteLocation(location.absolutePointer.toString());
120
+ addFoundComponent(typeName, componentName, location);
110
121
  }
111
122
  };
112
123
  exports.ComponentNameUnique = ComponentNameUnique;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redocly/openapi-core",
3
- "version": "1.25.8",
3
+ "version": "1.25.10",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "engines": {
@@ -32,11 +32,11 @@
32
32
  "oas"
33
33
  ],
34
34
  "contributors": [
35
- "Roman Hotsiy <roman@redoc.ly> (https://redoc.ly/)"
35
+ "Roman Hotsiy <roman@redocly.com> (https://redocly.com/)"
36
36
  ],
37
37
  "dependencies": {
38
38
  "@redocly/ajv": "^8.11.2",
39
- "@redocly/config": "^0.12.1",
39
+ "@redocly/config": "^0.16.0",
40
40
  "colorette": "^1.2.0",
41
41
  "https-proxy-agent": "^7.0.4",
42
42
  "js-levenshtein": "^1.1.6",
@@ -199,7 +199,6 @@ const testPortalConfig = parseYamlToDocument(
199
199
  editPage:
200
200
  baseUrl: https://test.com
201
201
  graphql:
202
- pagination: section
203
202
  menu:
204
203
  requireExactGroups: false
205
204
  groups:
@@ -10,9 +10,6 @@ info:
10
10
  name: Rebilly
11
11
  url: 'https://www.rebilly.com/api-license/'
12
12
  termsOfService: 'https://www.rebilly.com/terms-of-use/'
13
- x-logo:
14
- url: 'https://rebilly-core.redoc.ly/rb_apiLogo.svg'
15
- backgroundColor: '#0033A0'
16
13
  description: >
17
14
  # Introduction
18
15
 
package/src/bundle.ts CHANGED
@@ -398,7 +398,7 @@ function makeBundleVisitor(
398
398
  },
399
399
  },
400
400
  Root: {
401
- enter(root: any, ctx: any) {
401
+ enter(root: any, ctx: UserContext) {
402
402
  rootLocation = ctx.location;
403
403
  if (version === SpecMajorVersion.OAS3) {
404
404
  components = root.components = root.components || {};
@@ -417,7 +417,7 @@ function makeBundleVisitor(
417
417
 
418
418
  if (version === SpecMajorVersion.OAS3) {
419
419
  visitor.DiscriminatorMapping = {
420
- leave(mapping: Record<string, string>, ctx: any) {
420
+ leave(mapping: Record<string, string>, ctx: UserContext) {
421
421
  for (const name of Object.keys(mapping)) {
422
422
  const $ref = mapping[name];
423
423
  const resolved = ctx.resolve({ $ref });
@@ -446,7 +446,7 @@ function makeBundleVisitor(
446
446
 
447
447
  function saveComponent(
448
448
  componentType: string,
449
- target: { node: any; location: Location },
449
+ target: { node: unknown; location: Location },
450
450
  ctx: UserContext
451
451
  ) {
452
452
  components[componentType] = components[componentType] || {};
@@ -464,8 +464,8 @@ function makeBundleVisitor(
464
464
  }
465
465
 
466
466
  function isEqualOrEqualRef(
467
- node: any,
468
- target: { node: any; location: Location },
467
+ node: unknown,
468
+ target: { node: unknown; location: Location },
469
469
  ctx: UserContext
470
470
  ) {
471
471
  if (
@@ -480,7 +480,7 @@ function makeBundleVisitor(
480
480
  }
481
481
 
482
482
  function getComponentName(
483
- target: { node: any; location: Location },
483
+ target: { node: unknown; location: Location },
484
484
  componentType: string,
485
485
  ctx: UserContext
486
486
  ) {
@@ -34,7 +34,7 @@ import type {
34
34
  export const IGNORE_FILE = '.redocly.lint-ignore.yaml';
35
35
  const IGNORE_BANNER =
36
36
  `# This file instructs Redocly's linter to ignore the rules contained for specific parts of your API.\n` +
37
- `# See https://redoc.ly/docs/cli/ for more information.\n`;
37
+ `# See https://redocly.com/docs/cli/ for more information.\n`;
38
38
 
39
39
  function getIgnoreFilePath(configFile?: string): string | undefined {
40
40
  if (configFile) {
@@ -271,6 +271,103 @@ describe('oas3 remove-x-internal', () => {
271
271
 
272
272
  `);
273
273
  });
274
+
275
+ it('should remove $refs and the corresponding discriminator mapping', async () => {
276
+ const testDoc = parseYamlToDocument(
277
+ outdent`
278
+ openapi: 3.1.0
279
+ info: {}
280
+ paths:
281
+ /test:
282
+ post:
283
+ requestBody:
284
+ content:
285
+ application/json:
286
+ schema:
287
+ $ref: '#/components/schemas/christmas-tree'
288
+ components:
289
+ schemas:
290
+ christmas-tree:
291
+ type: array
292
+ items:
293
+ discriminator:
294
+ propertyName: type
295
+ mapping:
296
+ candy-cane: '#/components/schemas/candy-cane'
297
+ popcorn: '#/components/schemas/popcorn'
298
+ cranberry: '#/components/schemas/cranberry'
299
+ anyOf:
300
+ - $ref: '#/components/schemas/candy-cane'
301
+ - $ref: '#/components/schemas/popcorn'
302
+ - $ref: '#/components/schemas/cranberry'
303
+ candy-cane:
304
+ x-internal: true
305
+ title: candy-cane
306
+ type: object
307
+ properties:
308
+ type:
309
+ type: string
310
+ enum: [candy-cane]
311
+ popcorn:
312
+ type: object
313
+ properties:
314
+ type:
315
+ type: string
316
+ enum: [popcorn]
317
+ cranberry:
318
+ type: object
319
+ properties:
320
+ type:
321
+ type: string
322
+ enum: [cranberry]
323
+ `
324
+ );
325
+ const { bundle: res } = await bundleDocument({
326
+ document: testDoc,
327
+ externalRefResolver: new BaseResolver(),
328
+ config: await makeConfig({ rules: {}, decorators: { 'remove-x-internal': 'on' } }),
329
+ });
330
+ expect(res.parsed).toMatchInlineSnapshot(`
331
+ openapi: 3.1.0
332
+ info: {}
333
+ paths:
334
+ /test:
335
+ post:
336
+ requestBody:
337
+ content:
338
+ application/json:
339
+ schema:
340
+ $ref: '#/components/schemas/christmas-tree'
341
+ components:
342
+ schemas:
343
+ christmas-tree:
344
+ type: array
345
+ items:
346
+ discriminator:
347
+ propertyName: type
348
+ mapping:
349
+ popcorn: '#/components/schemas/popcorn'
350
+ cranberry: '#/components/schemas/cranberry'
351
+ anyOf:
352
+ - $ref: '#/components/schemas/popcorn'
353
+ - $ref: '#/components/schemas/cranberry'
354
+ popcorn:
355
+ type: object
356
+ properties:
357
+ type:
358
+ type: string
359
+ enum:
360
+ - popcorn
361
+ cranberry:
362
+ type: object
363
+ properties:
364
+ type:
365
+ type: string
366
+ enum:
367
+ - cranberry
368
+
369
+ `);
370
+ });
274
371
  });
275
372
 
276
373
  describe('oas2 remove-x-internal', () => {
@@ -6,23 +6,35 @@ import type { UserContext } from '../../walk';
6
6
 
7
7
  const DEFAULT_INTERNAL_PROPERTY_NAME = 'x-internal';
8
8
 
9
- export const RemoveXInternal: Oas3Decorator | Oas2Decorator = ({ internalFlagProperty }) => {
10
- const hiddenTag: string = internalFlagProperty || DEFAULT_INTERNAL_PROPERTY_NAME;
11
-
12
- function removeInternal(node: any, ctx: UserContext) {
9
+ export const RemoveXInternal: Oas3Decorator | Oas2Decorator = ({
10
+ internalFlagProperty = DEFAULT_INTERNAL_PROPERTY_NAME,
11
+ }) => {
12
+ function removeInternal(
13
+ node: unknown,
14
+ ctx: UserContext,
15
+ originalMapping: Record<string, string>
16
+ ) {
13
17
  const { parent, key } = ctx;
14
18
  let didDelete = false;
15
19
  if (Array.isArray(node)) {
16
20
  for (let i = 0; i < node.length; i++) {
17
21
  if (isRef(node[i])) {
18
22
  const resolved = ctx.resolve(node[i]);
19
- if (resolved.node?.[hiddenTag]) {
23
+ if (resolved.node?.[internalFlagProperty]) {
24
+ // First, remove the reference in the discriminator mapping, if it exists:
25
+ if (isPlainObject(parent.discriminator?.mapping)) {
26
+ for (const mapping in parent.discriminator.mapping) {
27
+ if (originalMapping?.[mapping] === node[i].$ref) {
28
+ delete parent.discriminator.mapping[mapping];
29
+ }
30
+ }
31
+ }
20
32
  node.splice(i, 1);
21
33
  didDelete = true;
22
34
  i--;
23
35
  }
24
36
  }
25
- if (node[i]?.[hiddenTag]) {
37
+ if (node[i]?.[internalFlagProperty]) {
26
38
  node.splice(i, 1);
27
39
  didDelete = true;
28
40
  i--;
@@ -30,15 +42,14 @@ export const RemoveXInternal: Oas3Decorator | Oas2Decorator = ({ internalFlagPro
30
42
  }
31
43
  } else if (isPlainObject(node)) {
32
44
  for (const key of Object.keys(node)) {
33
- node = node as any;
34
45
  if (isRef(node[key])) {
35
- const resolved = ctx.resolve<any>(node[key]);
36
- if (resolved.node?.[hiddenTag]) {
46
+ const resolved = ctx.resolve(node[key]);
47
+ if (isPlainObject(resolved.node) && resolved.node?.[internalFlagProperty]) {
37
48
  delete node[key];
38
49
  didDelete = true;
39
50
  }
40
51
  }
41
- if (node[key]?.[hiddenTag]) {
52
+ if (isPlainObject(node[key]) && node[key]?.[internalFlagProperty]) {
42
53
  delete node[key];
43
54
  didDelete = true;
44
55
  }
@@ -50,10 +61,16 @@ export const RemoveXInternal: Oas3Decorator | Oas2Decorator = ({ internalFlagPro
50
61
  }
51
62
  }
52
63
 
64
+ let originalMapping: Record<string, string> = {};
53
65
  return {
66
+ DiscriminatorMapping: {
67
+ enter: (mapping: Record<string, string>) => {
68
+ originalMapping = structuredClone(mapping);
69
+ },
70
+ },
54
71
  any: {
55
72
  enter: (node, ctx) => {
56
- removeInternal(node, ctx);
73
+ removeInternal(node, ctx, originalMapping);
57
74
  },
58
75
  },
59
76
  };
package/src/resolve.ts CHANGED
@@ -126,7 +126,7 @@ export class BaseResolver {
126
126
  return new Source(absoluteRef, body, mimeType);
127
127
  } else {
128
128
  if (fs.lstatSync(absoluteRef).isDirectory()) {
129
- throw new Error(`Expected a file but received a folder at ${absoluteRef}`);
129
+ throw new Error(`Expected a file but received a folder at ${absoluteRef}.`);
130
130
  }
131
131
  const content = await fs.promises.readFile(absoluteRef, 'utf-8');
132
132
  // In some cases file have \r\n line delimeters like on windows, we should skip it.
@@ -233,7 +233,7 @@ export async function resolveDocument(opts: {
233
233
  }): Promise<ResolvedRefMap> {
234
234
  const { rootDocument, externalRefResolver, rootType } = opts;
235
235
  const resolvedRefMap: ResolvedRefMap = new Map();
236
- const seedNodes = new Set<string>(); // format "${type}::${absoluteRef}${pointer}"
236
+ const seenNodes = new Set<string>(); // format "${type}::${absoluteRef}${pointer}"
237
237
 
238
238
  const resolvePromises: Array<Promise<void>> = [];
239
239
  resolveRefsInParallel(rootDocument.parsed, rootDocument, '#/', rootType);
@@ -262,11 +262,11 @@ export async function resolveDocument(opts: {
262
262
  }
263
263
 
264
264
  const nodeId = `${type.name}::${nodeAbsoluteRef}`;
265
- if (seedNodes.has(nodeId)) {
265
+ if (seenNodes.has(nodeId)) {
266
266
  return;
267
267
  }
268
268
 
269
- seedNodes.add(nodeId);
269
+ seenNodes.add(nodeId);
270
270
 
271
271
  const [_, anchor] = Object.entries(node).find(([key]) => key === '$anchor') || [];
272
272
  if (anchor) {