Skip to content

Commit

Permalink
fix: clean up the discriminator mapping in the remove-x-internal deco…
Browse files Browse the repository at this point in the history
…rator (#1694)

* fix: remove the discriminator mapping in the remove-x-internal decorator

* add a test for discriminators with $refs to files

* collect original discriminator mapping before resolving to compare $refs to the actual files

* add check for mapping to be an object
  • Loading branch information
tatomyr authored Oct 28, 2024
1 parent 05d6fd3 commit ca08c1b
Show file tree
Hide file tree
Showing 10 changed files with 227 additions and 15 deletions.
6 changes: 6 additions & 0 deletions .changeset/seven-geckos-behave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@redocly/openapi-core": patch
"@redocly/cli": patch
---

Fixed the `remove-x-internal` decorator, which was not removing the reference in the corresponding discriminator mapping while removing the original `$ref`.
25 changes: 25 additions & 0 deletions __tests__/bundle/remove-x-internal/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
openapi: 3.1.0
info: {}
paths:
/test:
post:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/christmas-tree'
components:
schemas:
christmas-tree:
type: array
items:
discriminator:
propertyName: type
mapping:
candy-cane: schemas/candy-cane.yaml
popcorn: schemas/popcorn.yaml
cranberry: schemas/cranberry.yaml
anyOf:
- $ref: schemas/candy-cane.yaml
- $ref: schemas/popcorn.yaml
- $ref: schemas/cranberry.yaml
5 changes: 5 additions & 0 deletions __tests__/bundle/remove-x-internal/redocly.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
apis:
main:
root: ./main.yaml
decorators:
remove-x-internal: on
7 changes: 7 additions & 0 deletions __tests__/bundle/remove-x-internal/schemas/candy-cane.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
x-internal: true
title: candy-cane
type: object
properties:
type:
type: string
enum: [candy-cane]
5 changes: 5 additions & 0 deletions __tests__/bundle/remove-x-internal/schemas/cranberry.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type: object
properties:
type:
type: string
enum: [cranberry]
5 changes: 5 additions & 0 deletions __tests__/bundle/remove-x-internal/schemas/popcorn.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type: object
properties:
type:
type: string
enum: [popcorn]
45 changes: 45 additions & 0 deletions __tests__/bundle/remove-x-internal/snapshot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`E2E bundle remove-x-internal 1`] = `
openapi: 3.1.0
info: {}
paths:
/test:
post:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/christmas-tree'
components:
schemas:
christmas-tree:
type: array
items:
discriminator:
propertyName: type
mapping:
popcorn: '#/components/schemas/popcorn'
cranberry: '#/components/schemas/cranberry'
anyOf:
- $ref: '#/components/schemas/popcorn'
- $ref: '#/components/schemas/cranberry'
popcorn:
type: object
properties:
type:
type: string
enum:
- popcorn
cranberry:
type: object
properties:
type:
type: string
enum:
- cranberry
bundling ./main.yaml...
📦 Created a bundle for ./main.yaml at stdout <test>ms.
`;
8 changes: 4 additions & 4 deletions packages/core/src/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ function makeBundleVisitor(

function saveComponent(
componentType: string,
target: { node: any; location: Location },
target: { node: unknown; location: Location },
ctx: UserContext
) {
components[componentType] = components[componentType] || {};
Expand All @@ -464,8 +464,8 @@ function makeBundleVisitor(
}

function isEqualOrEqualRef(
node: any,
target: { node: any; location: Location },
node: unknown,
target: { node: unknown; location: Location },
ctx: UserContext
) {
if (
Expand All @@ -480,7 +480,7 @@ function makeBundleVisitor(
}

function getComponentName(
target: { node: any; location: Location },
target: { node: unknown; location: Location },
componentType: string,
ctx: UserContext
) {
Expand Down
97 changes: 97 additions & 0 deletions packages/core/src/decorators/__tests__/remove-x-internal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,103 @@ describe('oas3 remove-x-internal', () => {
`);
});

it('should remove $refs and the corresponding discriminator mapping', async () => {
const testDoc = parseYamlToDocument(
outdent`
openapi: 3.1.0
info: {}
paths:
/test:
post:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/christmas-tree'
components:
schemas:
christmas-tree:
type: array
items:
discriminator:
propertyName: type
mapping:
candy-cane: '#/components/schemas/candy-cane'
popcorn: '#/components/schemas/popcorn'
cranberry: '#/components/schemas/cranberry'
anyOf:
- $ref: '#/components/schemas/candy-cane'
- $ref: '#/components/schemas/popcorn'
- $ref: '#/components/schemas/cranberry'
candy-cane:
x-internal: true
title: candy-cane
type: object
properties:
type:
type: string
enum: [candy-cane]
popcorn:
type: object
properties:
type:
type: string
enum: [popcorn]
cranberry:
type: object
properties:
type:
type: string
enum: [cranberry]
`
);
const { bundle: res } = await bundleDocument({
document: testDoc,
externalRefResolver: new BaseResolver(),
config: await makeConfig({ rules: {}, decorators: { 'remove-x-internal': 'on' } }),
});
expect(res.parsed).toMatchInlineSnapshot(`
openapi: 3.1.0
info: {}
paths:
/test:
post:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/christmas-tree'
components:
schemas:
christmas-tree:
type: array
items:
discriminator:
propertyName: type
mapping:
popcorn: '#/components/schemas/popcorn'
cranberry: '#/components/schemas/cranberry'
anyOf:
- $ref: '#/components/schemas/popcorn'
- $ref: '#/components/schemas/cranberry'
popcorn:
type: object
properties:
type:
type: string
enum:
- popcorn
cranberry:
type: object
properties:
type:
type: string
enum:
- cranberry
`);
});
});

describe('oas2 remove-x-internal', () => {
Expand Down
39 changes: 28 additions & 11 deletions packages/core/src/decorators/common/remove-x-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,50 @@ import type { UserContext } from '../../walk';

const DEFAULT_INTERNAL_PROPERTY_NAME = 'x-internal';

export const RemoveXInternal: Oas3Decorator | Oas2Decorator = ({ internalFlagProperty }) => {
const hiddenTag: string = internalFlagProperty || DEFAULT_INTERNAL_PROPERTY_NAME;

function removeInternal(node: any, ctx: UserContext) {
export const RemoveXInternal: Oas3Decorator | Oas2Decorator = ({
internalFlagProperty = DEFAULT_INTERNAL_PROPERTY_NAME,
}) => {
function removeInternal(
node: unknown,
ctx: UserContext,
originalMapping: Record<string, string>
) {
const { parent, key } = ctx;
let didDelete = false;
if (Array.isArray(node)) {
for (let i = 0; i < node.length; i++) {
if (isRef(node[i])) {
const resolved = ctx.resolve(node[i]);
if (resolved.node?.[hiddenTag]) {
if (resolved.node?.[internalFlagProperty]) {
// First, remove the reference in the discriminator mapping, if it exists:
if (isPlainObject(parent.discriminator?.mapping)) {
for (const mapping in parent.discriminator.mapping) {
if (originalMapping?.[mapping] === node[i].$ref) {
delete parent.discriminator.mapping[mapping];
}
}
}
node.splice(i, 1);
didDelete = true;
i--;
}
}
if (node[i]?.[hiddenTag]) {
if (node[i]?.[internalFlagProperty]) {
node.splice(i, 1);
didDelete = true;
i--;
}
}
} else if (isPlainObject(node)) {
for (const key of Object.keys(node)) {
node = node as any;
if (isRef(node[key])) {
const resolved = ctx.resolve<any>(node[key]);
if (resolved.node?.[hiddenTag]) {
const resolved = ctx.resolve(node[key]);
if (isPlainObject(resolved.node) && resolved.node?.[internalFlagProperty]) {
delete node[key];
didDelete = true;
}
}
if (node[key]?.[hiddenTag]) {
if (isPlainObject(node[key]) && node[key]?.[internalFlagProperty]) {
delete node[key];
didDelete = true;
}
Expand All @@ -50,10 +61,16 @@ export const RemoveXInternal: Oas3Decorator | Oas2Decorator = ({ internalFlagPro
}
}

let originalMapping: Record<string, string> = {};
return {
DiscriminatorMapping: {
enter: (mapping: Record<string, string>) => {
originalMapping = structuredClone(mapping);
},
},
any: {
enter: (node, ctx) => {
removeInternal(node, ctx);
removeInternal(node, ctx, originalMapping);
},
},
};
Expand Down

1 comment on commit ca08c1b

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

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

Coverage report

St.
Category Percentage Covered / Total
🟡 Statements 78.81% 5011/6358
🟡 Branches 67.52% 2075/3073
🟡 Functions 73.3% 829/1131
🟡 Lines 79.12% 4729/5977

Test suite run success

811 tests passing in 121 suites.

Report generated by 🧪jest coverage report action from ca08c1b

Please sign in to comment.