Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## [1.29.5] - 2026-02-28

- Filter: preserve inverse flag filtering with stripFlags (#193)
- Types: Update Typescript typings (#194)

## [1.29.4] - 2026-02-23
Expand Down
32 changes: 29 additions & 3 deletions openapi-format.js
Original file line number Diff line number Diff line change
Expand Up @@ -794,10 +794,36 @@ async function openapiFilter(oaObj, options) {
options.unusedDepth === 0 ||
(stripUnused.length > 0 && unusedComp.meta.total > 0 && options.unusedDepth <= 10)
) {
options.unusedDepth++;
const resultObj = await openapiFilter(jsonObj, options);
const stripFlagsSet = new Set(stripFlags);
// If a flag is both inverse-kept and stripped in the same pass, recursive
// filtering would otherwise remove previously matched operations.
const hasInverseFlagsStripConflict = inverseFilterFlags.some(flag => stripFlagsSet.has(flag));
// Same conflict check for inverseFlagValues, based on the flag key of each object.
const hasInverseFlagValuesStripConflict = inverseFilterFlagValues.some(flagObj =>
stripFlagsSet.has(Object.keys(flagObj || {})[0])
);

// Recurse with inverse flag filters disabled only for this conflict case.
// This keeps the first-pass inverse selection intact while still allowing
// cleanup recursion (unused components, empty objects, etc.).
const recurseOptions =
hasInverseFlagsStripConflict || hasInverseFlagValuesStripConflict
? {
...options,
filterSet: {
...(options.filterSet || {}),
inverseFlags: [],
inverseFlagValues: []
}
}
: options;

// Preserve recursion depth semantics regardless of whether we cloned options.
recurseOptions.unusedDepth = (recurseOptions.unusedDepth || 0) + 1;
const resultObj = await openapiFilter(jsonObj, recurseOptions);
jsonObj = resultObj.data;
unusedComp = JSON.parse(JSON.stringify(options.unusedComp));
// Carry forward unused component tracking from the recurse options object.
unusedComp = JSON.parse(JSON.stringify(recurseOptions.unusedComp));
}

// Prepare totalComp for the final result
Expand Down
10 changes: 10 additions & 0 deletions test/filtering.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,16 @@ describe('openapi-format CLI filtering tests', () => {
});
});

describe('yaml-filter-inverse-flags-stripFlags', () => {
it('yaml-filter-inverse-flags-stripFlags - should match expected output', async () => {
const testName = 'yaml-filter-inverse-flags-stripFlags';
const {result, outputBefore, outputAfter} = await testUtils.loadTest(testName);
expect(result.code).toBe(0);
expect(result.stdout).toContain('formatted successfully');
expect(outputAfter).toStrictEqual(outputBefore);
});
});

describe('isUsedComp', () => {
it('returns false for non-object input', () => {
expect(isUsedComp(null, 'schemas')).toBe(false);
Expand Down
64 changes: 64 additions & 0 deletions test/openapi-core.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,70 @@ describe('openapi-format core API', () => {
expect(result.data.tags).toEqual([{name: 'pets'}]);
});

it('openapiFilter should keep inverseFlags-matched operations when stripFlags removes the same key', async () => {
const doc = {
openapi: '3.0.0',
info: {title: 'API', version: '1.0.0'},
paths: {
'/pets': {
get: {'x-public': true, responses: {200: {description: 'ok'}}},
post: {responses: {200: {description: 'ok'}}}
}
}
};

const onlyInverse = await openapiFilter(doc, {filterSet: {inverseFlags: ['x-public']}});
expect(onlyInverse.data.paths).toHaveProperty('/pets.get');

const inverseAndStrip = await openapiFilter(doc, {
filterSet: {inverseFlags: ['x-public'], stripFlags: ['x-public']}
});
expect(inverseAndStrip.data.paths).toHaveProperty('/pets.get');
expect(inverseAndStrip.data.paths['/pets'].get['x-public']).toBeUndefined();
expect(inverseAndStrip.data.paths['/pets'].post).toBeUndefined();
});

it('adding responses to unusedComponents should not remove still-referenced schemas', async () => {
const doc = {
openapi: '3.0.0',
info: {title: 'API', version: '1.0.0'},
paths: {
'/pets': {
get: {
'x-public': true,
responses: {
200: {description: 'ok', content: {'application/json': {schema: {$ref: '#/components/schemas/Pet'}}}}
}
},
post: {
responses: {
200: {description: 'ok', content: {'application/json': {schema: {$ref: '#/components/schemas/Pet'}}}}
}
}
}
},
components: {
schemas: {
Pet: {type: 'object'}
}
}
};

const base = {
inverseFlags: ['x-public'],
stripFlags: ['x-public'],
unusedComponents: ['schemas', 'parameters', 'examples', 'headers', 'requestBodies']
};
const withResponses = {...base, unusedComponents: [...base.unusedComponents, 'responses']};

const resultBase = await openapiFilter(doc, {filterSet: base});
const resultWithResponses = await openapiFilter(doc, {filterSet: withResponses});

expect(resultBase.data).toEqual(resultWithResponses.data);
expect(resultWithResponses.data.paths).toHaveProperty('/pets.get');
expect(resultWithResponses.data.components.schemas).toHaveProperty('Pet');
});

it('openapiChangeCase should apply summary, description and securitySchemes ref casing', async () => {
const doc = {
openapi: '3.0.0',
Expand Down
2 changes: 1 addition & 1 deletion test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const tests = !localTesting
? fs.readdirSync(__dirname).filter(file => {
return fs.statSync(path.join(__dirname, file)).isDirectory() && !file.startsWith('_');
})
: ['yaml-sort-component-props'];
: ['yaml-filter-inverse-flags-stripFlags'];

describe('openapi-format tests', () => {
let consoleLogSpy, consoleWarnSpy;
Expand Down
11 changes: 11 additions & 0 deletions test/yaml-filter-inverse-flags-stripFlags/customFilter.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
inverseFlags:
- x-public
stripFlags:
- x-public
unusedComponents:
- schemas
- parameters
- examples
- headers
- requestBodies
- responses
32 changes: 32 additions & 0 deletions test/yaml-filter-inverse-flags-stripFlags/input.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
openapi: 3.0.0
info:
version: 1.0.0
title: Swagger Petstore
paths:
/pets:
get:
operationId: findPets
x-public: true
responses:
'200':
description: pet response
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
post:
operationId: addPet
responses:
'200':
description: pet response
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
components:
schemas:
Pet:
type: object
properties:
id:
type: integer
4 changes: 4 additions & 0 deletions test/yaml-filter-inverse-flags-stripFlags/options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
verbose: true
no-sort: true
output: output.yaml
filterFile: customFilter.yaml
22 changes: 22 additions & 0 deletions test/yaml-filter-inverse-flags-stripFlags/output.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
openapi: 3.0.0
info:
version: 1.0.0
title: Swagger Petstore
paths:
/pets:
get:
operationId: findPets
responses:
'200':
description: pet response
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
components:
schemas:
Pet:
type: object
properties:
id:
type: integer