Skip to content

Commit a24fa90

Browse files
authored
Filter: combine inverseTags and inverseFlag (#199)
* test: Added test with inverseTags with inverseFlags * Filter: combine inverseTags and inverseFlag
1 parent f20ac79 commit a24fa90

7 files changed

Lines changed: 161 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## [1.29.5] - 2026-02-28
44

5+
- Filter: combine inverseTags and inverseFlag (#192)
56
- Filter: preserve inverse flag filtering with stripFlags (#193)
67
- Types: Update Typescript typings (#194)
78

openapi-format.js

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@ async function openapiFilter(oaObj, options) {
381381
// Filter out object matching the inverse "tags"
382382
if (
383383
inverseFilterArray.length > 0 &&
384+
inverseFilterFlags.length === 0 &&
384385
this.key === 'tags' &&
385386
!inverseFilterArray.some(i => node.includes(i)) &&
386387
this.parent.parent !== undefined
@@ -390,7 +391,12 @@ async function openapiFilter(oaObj, options) {
390391
}
391392

392393
// Filter out the top level tags matching the inverse "tags"
393-
if (inverseFilterArray.length > 0 && this.key === 'tags' && this.parent.parent === undefined) {
394+
if (
395+
inverseFilterArray.length > 0 &&
396+
inverseFilterFlags.length === 0 &&
397+
this.key === 'tags' &&
398+
this.parent.parent === undefined
399+
) {
394400
// debugFilterStep = 'Filter - inverse top tags'
395401
node = node.filter(value => inverseFilterArray.includes(value.name));
396402
this.update(node);
@@ -437,6 +443,7 @@ async function openapiFilter(oaObj, options) {
437443
// Keep fields matching the inverseFlags array
438444
if (
439445
inverseFilterFlags.length > 0 &&
446+
inverseFilterArray.length === 0 &&
440447
(this.path[0] === 'tags' || this.path[0] === 'x-tagGroups') &&
441448
this.level === 1
442449
) {
@@ -551,23 +558,36 @@ async function openapiFilter(oaObj, options) {
551558
}
552559
}
553560

554-
// Filter out operations not matching inverseFilterArray
555-
if (inverseFilterArray.length > 0 && this.parent && this.parent.parent && this.parent.parent.key === 'paths') {
556-
if (node.tags === undefined || !inverseFilterArray.some(i => node.tags.includes(i))) {
557-
this.delete();
558-
}
559-
}
560-
561-
// Keep fields matching the inverseFlags
562-
if (inverseFilterFlags.length > 0 && this.path[0] === 'paths' && this.level === 3) {
563-
const itmObj = node;
564-
const matchesInverseFlag = inverseFilterFlags.some(flagKey => {
565-
return itmObj.hasOwnProperty(flagKey);
566-
});
567-
568-
if (!matchesInverseFlag) {
569-
// debugFilterStep = 'Filter - Single field - inverseFlags'
570-
this.remove();
561+
// Filter operations for inverseTags / inverseFlags.
562+
// When both are configured, treat them as a union (keep if either matches).
563+
if (
564+
this.path[0] === 'paths' &&
565+
this.level === 3 &&
566+
this.parent &&
567+
this.parent.parent &&
568+
this.parent.parent.key === 'paths'
569+
) {
570+
const hasInverseTags = inverseFilterArray.length > 0;
571+
const hasInverseFlags = inverseFilterFlags.length > 0;
572+
if (hasInverseTags || hasInverseFlags) {
573+
const operation = node || {};
574+
const matchesInverseTag =
575+
hasInverseTags && Array.isArray(operation.tags)
576+
? inverseFilterArray.some(i => operation.tags.includes(i))
577+
: false;
578+
const matchesInverseFlag = hasInverseFlags
579+
? inverseFilterFlags.some(flagKey => operation.hasOwnProperty(flagKey))
580+
: false;
581+
const shouldKeep =
582+
hasInverseTags && hasInverseFlags
583+
? matchesInverseTag || matchesInverseFlag
584+
: hasInverseTags
585+
? matchesInverseTag
586+
: matchesInverseFlag;
587+
588+
if (!shouldKeep) {
589+
this.delete();
590+
}
571591
}
572592
}
573593

@@ -636,6 +656,25 @@ async function openapiFilter(oaObj, options) {
636656
}
637657
});
638658

659+
// Keep top-level tags that are still referenced by remaining operations.
660+
// Apply this only when inverseTags and inverseFlags are both configured.
661+
// (inverseFlags-only mode should preserve the existing flagged-tag behavior.)
662+
if (Array.isArray(jsonObj.tags) && inverseFilterArray.length > 0 && inverseFilterFlags.length > 0) {
663+
const usedTags = new Set();
664+
if (jsonObj.paths && typeof jsonObj.paths === 'object') {
665+
Object.values(jsonObj.paths).forEach(pathItem => {
666+
if (pathItem && typeof pathItem === 'object') {
667+
Object.values(pathItem).forEach(operation => {
668+
if (operation && typeof operation === 'object' && Array.isArray(operation.tags)) {
669+
operation.tags.forEach(tag => usedTags.add(tag));
670+
}
671+
});
672+
}
673+
});
674+
}
675+
jsonObj.tags = jsonObj.tags.filter(tagObj => tagObj && usedTags.has(tagObj.name));
676+
}
677+
639678
// Calculate comps.meta.total at the end
640679
// comps.meta.total = Object.keys(comps.schemas).length +
641680
// Object.keys(comps.responses).length +

test/filtering.test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,16 @@ describe('openapi-format CLI filtering tests', () => {
320320
});
321321
});
322322

323+
describe('yaml-filter-inverse-flags-inverse-tags', () => {
324+
it('yaml-filter-inverse-flags-inverse-tags - should match expected output', async () => {
325+
const testName = 'yaml-filter-inverse-flags-inverse-tags';
326+
const {result, outputBefore, outputAfter} = await testUtils.loadTest(testName);
327+
expect(result.code).toBe(0);
328+
expect(result.stdout).toContain('formatted successfully');
329+
expect(outputAfter).toStrictEqual(outputBefore);
330+
});
331+
});
332+
323333
describe('isUsedComp', () => {
324334
it('returns false for non-object input', () => {
325335
expect(isUsedComp(null, 'schemas')).toBe(false);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
inverseTags:
2+
- foo
3+
inverseFlags:
4+
- x-public
5+
unusedComponents:
6+
- schemas
7+
- parameters
8+
- examples
9+
- headers
10+
- requestBodies
11+
- responses
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
openapi: 3.0.0
2+
info:
3+
title: API
4+
version: 1.0.0
5+
tags:
6+
- name: foo
7+
- name: bar
8+
paths:
9+
/foo-op:
10+
get:
11+
operationId: fooOp
12+
tags:
13+
- foo
14+
responses:
15+
'200':
16+
description: ok
17+
content:
18+
application/json:
19+
schema:
20+
$ref: '#/components/schemas/Foo'
21+
/public-op:
22+
get:
23+
operationId: publicOp
24+
tags:
25+
- bar
26+
x-public: true
27+
responses:
28+
'200':
29+
description: ok
30+
content:
31+
application/json:
32+
schema:
33+
$ref: '#/components/schemas/Bar'
34+
components:
35+
schemas:
36+
Foo:
37+
type: object
38+
Bar:
39+
type: object
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
verbose: true
2+
no-sort: true
3+
output: output.yaml
4+
filterFile: customFilter.yaml
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
openapi: 3.0.0
2+
info:
3+
title: API
4+
version: 1.0.0
5+
tags:
6+
- name: foo
7+
- name: bar
8+
paths:
9+
/foo-op:
10+
get:
11+
operationId: fooOp
12+
tags:
13+
- foo
14+
responses:
15+
'200':
16+
description: ok
17+
content:
18+
application/json:
19+
schema:
20+
$ref: '#/components/schemas/Foo'
21+
/public-op:
22+
get:
23+
operationId: publicOp
24+
tags:
25+
- bar
26+
x-public: true
27+
responses:
28+
'200':
29+
description: ok
30+
content:
31+
application/json:
32+
schema:
33+
$ref: '#/components/schemas/Bar'
34+
components:
35+
schemas:
36+
Foo:
37+
type: object
38+
Bar:
39+
type: object

0 commit comments

Comments
 (0)