From 71e276f5076cae56c779a4e5f32279411a9b50f9 Mon Sep 17 00:00:00 2001 From: Tim Haselaars Date: Sat, 28 Feb 2026 17:39:13 +0100 Subject: [PATCH 1/3] tests: inverse with strip --- test/filtering.test.js | 10 ++++ test/openapi-core.test.js | 50 +++++++++++++++++++ test/test.js | 4 +- .../customFilter.yaml | 11 ++++ .../input.yaml | 32 ++++++++++++ .../options.yaml | 4 ++ .../output.yaml | 4 ++ 7 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 test/yaml-filter-inverse-flags-stripFlags/customFilter.yaml create mode 100644 test/yaml-filter-inverse-flags-stripFlags/input.yaml create mode 100644 test/yaml-filter-inverse-flags-stripFlags/options.yaml create mode 100644 test/yaml-filter-inverse-flags-stripFlags/output.yaml diff --git a/test/filtering.test.js b/test/filtering.test.js index e39e422..212c669 100644 --- a/test/filtering.test.js +++ b/test/filtering.test.js @@ -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); diff --git a/test/openapi-core.test.js b/test/openapi-core.test.js index bddcaf4..d52eb1c 100644 --- a/test/openapi-core.test.js +++ b/test/openapi-core.test.js @@ -26,6 +26,56 @@ describe('openapi-format core API', () => { expect(result.data.tags).toEqual([{name: 'pets'}]); }); + it('openapiFilter inverseFlags + stripFlags currently removes previously kept operations after recurse', 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).toBeUndefined(); + }); + + it('adding responses to unusedComponents does not change inverseFlags+stripFlags outcome', 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'}}} + } + }, + 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).toBeUndefined(); + }); + it('openapiChangeCase should apply summary, description and securitySchemes ref casing', async () => { const doc = { openapi: '3.0.0', diff --git a/test/test.js b/test/test.js index a1b08b3..2055f09 100644 --- a/test/test.js +++ b/test/test.js @@ -8,7 +8,7 @@ const {run} = require('../bin/cli'); const {parseFile, stringify, writeFile} = require('../openapi-format'); // SELECTIVE TESTING DEBUG -const localTesting = false; +const localTesting = true; const destroyOutput = false; // Load tests @@ -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; diff --git a/test/yaml-filter-inverse-flags-stripFlags/customFilter.yaml b/test/yaml-filter-inverse-flags-stripFlags/customFilter.yaml new file mode 100644 index 0000000..b26078b --- /dev/null +++ b/test/yaml-filter-inverse-flags-stripFlags/customFilter.yaml @@ -0,0 +1,11 @@ +inverseFlags: + - x-public +stripFlags: + - x-public +unusedComponents: + - schemas + - parameters + - examples + - headers + - requestBodies + - responses diff --git a/test/yaml-filter-inverse-flags-stripFlags/input.yaml b/test/yaml-filter-inverse-flags-stripFlags/input.yaml new file mode 100644 index 0000000..9cf8adc --- /dev/null +++ b/test/yaml-filter-inverse-flags-stripFlags/input.yaml @@ -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 diff --git a/test/yaml-filter-inverse-flags-stripFlags/options.yaml b/test/yaml-filter-inverse-flags-stripFlags/options.yaml new file mode 100644 index 0000000..d581280 --- /dev/null +++ b/test/yaml-filter-inverse-flags-stripFlags/options.yaml @@ -0,0 +1,4 @@ +verbose: true +no-sort: true +output: output.yaml +filterFile: customFilter.yaml diff --git a/test/yaml-filter-inverse-flags-stripFlags/output.yaml b/test/yaml-filter-inverse-flags-stripFlags/output.yaml new file mode 100644 index 0000000..92092f3 --- /dev/null +++ b/test/yaml-filter-inverse-flags-stripFlags/output.yaml @@ -0,0 +1,4 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: Swagger Petstore From 320c9f7759082e05683e30cfdb572c3b71390370 Mon Sep 17 00:00:00 2001 From: Tim Haselaars Date: Sat, 28 Feb 2026 17:47:50 +0100 Subject: [PATCH 2/3] Filter: preserve inverse flag filtering with stripFlags (#193) --- CHANGELOG.md | 1 + openapi-format.js | 32 +++++++++++++++++-- test/openapi-core.test.js | 24 ++++++++++---- test/test.js | 2 +- .../output.yaml | 18 +++++++++++ 5 files changed, 67 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9d67dc..4e5394e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/openapi-format.js b/openapi-format.js index f0f127c..faaf5ac 100644 --- a/openapi-format.js +++ b/openapi-format.js @@ -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 diff --git a/test/openapi-core.test.js b/test/openapi-core.test.js index d52eb1c..1fa2f1e 100644 --- a/test/openapi-core.test.js +++ b/test/openapi-core.test.js @@ -26,7 +26,7 @@ describe('openapi-format core API', () => { expect(result.data.tags).toEqual([{name: 'pets'}]); }); - it('openapiFilter inverseFlags + stripFlags currently removes previously kept operations after recurse', async () => { + 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'}, @@ -42,17 +42,28 @@ describe('openapi-format core API', () => { expect(onlyInverse.data.paths).toHaveProperty('/pets.get'); const inverseAndStrip = await openapiFilter(doc, {filterSet: {inverseFlags: ['x-public'], stripFlags: ['x-public']}}); - expect(inverseAndStrip.data.paths).toBeUndefined(); + 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 does not change inverseFlags+stripFlags outcome', async () => { + 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'}}}, - post: {responses: {'200': {description: 'ok'}}} + 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: { @@ -73,7 +84,8 @@ describe('openapi-format core API', () => { const resultWithResponses = await openapiFilter(doc, {filterSet: withResponses}); expect(resultBase.data).toEqual(resultWithResponses.data); - expect(resultWithResponses.data.paths).toBeUndefined(); + 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 () => { diff --git a/test/test.js b/test/test.js index 2055f09..65aedc2 100644 --- a/test/test.js +++ b/test/test.js @@ -8,7 +8,7 @@ const {run} = require('../bin/cli'); const {parseFile, stringify, writeFile} = require('../openapi-format'); // SELECTIVE TESTING DEBUG -const localTesting = true; +const localTesting = false; const destroyOutput = false; // Load tests diff --git a/test/yaml-filter-inverse-flags-stripFlags/output.yaml b/test/yaml-filter-inverse-flags-stripFlags/output.yaml index 92092f3..1acd3e3 100644 --- a/test/yaml-filter-inverse-flags-stripFlags/output.yaml +++ b/test/yaml-filter-inverse-flags-stripFlags/output.yaml @@ -2,3 +2,21 @@ 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 From 98922421f89f6b1c1bb06f42fa3322994ecbc734 Mon Sep 17 00:00:00 2001 From: Tim Haselaars Date: Sat, 28 Feb 2026 17:49:56 +0100 Subject: [PATCH 3/3] Filter: preserve inverse flag filtering with stripFlags --- test/openapi-core.test.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/openapi-core.test.js b/test/openapi-core.test.js index 1fa2f1e..e842441 100644 --- a/test/openapi-core.test.js +++ b/test/openapi-core.test.js @@ -32,8 +32,8 @@ describe('openapi-format core API', () => { info: {title: 'API', version: '1.0.0'}, paths: { '/pets': { - get: {'x-public': true, responses: {'200': {description: 'ok'}}}, - post: {responses: {'200': {description: 'ok'}}} + get: {'x-public': true, responses: {200: {description: 'ok'}}}, + post: {responses: {200: {description: 'ok'}}} } } }; @@ -41,7 +41,9 @@ describe('openapi-format core API', () => { 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']}}); + 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(); @@ -56,12 +58,12 @@ describe('openapi-format core API', () => { get: { 'x-public': true, responses: { - '200': {description: 'ok', content: {'application/json': {schema: {$ref: '#/components/schemas/Pet'}}}} + 200: {description: 'ok', content: {'application/json': {schema: {$ref: '#/components/schemas/Pet'}}}} } }, post: { responses: { - '200': {description: 'ok', content: {'application/json': {schema: {$ref: '#/components/schemas/Pet'}}}} + 200: {description: 'ok', content: {'application/json': {schema: {$ref: '#/components/schemas/Pet'}}}} } } }