From fcada82f2a586ab6ec7296efdacc4247fc6e5402 Mon Sep 17 00:00:00 2001 From: Animesh Jajoo Date: Sat, 18 Oct 2025 16:44:26 +0530 Subject: [PATCH 1/2] Add pathItem reference resolution for OpenAPI 3.1 specs --- lib/common/schemaUtilsCommon.js | 38 +++++++++ lib/schemapack.js | 7 +- test/unit/bundle31.test.js | 147 ++++++++++++++++++++++++++++++++ 3 files changed, 191 insertions(+), 1 deletion(-) diff --git a/lib/common/schemaUtilsCommon.js b/lib/common/schemaUtilsCommon.js index 9ced27211..04724263d 100644 --- a/lib/common/schemaUtilsCommon.js +++ b/lib/common/schemaUtilsCommon.js @@ -367,5 +367,43 @@ module.exports = { []; return [...acc, ...newVarNames]; }, []); + }, + + /** + * Resolves $ref pointers in paths that reference components.pathItems + * This is necessary for OpenAPI 3.1 specs where the bundler creates refs to pathItems + * but the converter needs them inline for proper processing + * + * @param {Object} spec - The OpenAPI specification object (mutated in place) + * @returns {Object} - The same spec object with pathItem refs resolved + */ + resolvePathItemRefs: function(spec) { + // If no paths or no pathItems component, nothing to resolve + if (!spec || !spec.paths || !_.get(spec, 'components.pathItems')) { + return spec; + } + + // Iterate through all paths and resolve refs in place (no clone needed) + _.forEach(spec.paths, (pathValue, pathKey) => { + // Check if this path is a $ref to a pathItem + if (pathValue && pathValue.$ref && typeof pathValue.$ref === 'string') { + // Match patterns like "#/components/pathItems/paths_pets.yaml" + const match = pathValue.$ref.match(/#\/components\/pathItems\/(.+)/); + + if (match && match[1]) { + const pathItemKey = match[1]; + const pathItemValue = _.get(spec, ['components', 'pathItems', pathItemKey]); + + if (pathItemValue) { + spec.paths[pathKey] = pathItemValue; + } + else { + console.warn(`[openapi-to-postman] PathItem reference not found: ${pathItemKey}`); + } + } + } + }); + + return spec; } }; diff --git a/lib/schemapack.js b/lib/schemapack.js index 551dd9658..df61201a4 100644 --- a/lib/schemapack.js +++ b/lib/schemapack.js @@ -26,7 +26,7 @@ const { getConcreteSchemaUtils } = require('./common/versionUtils.js'), OpenApiErr = require('./error.js'), schemaUtils = require('./schemaUtils'), v2 = require('../libV2/index'), - { getServersPathVars } = require('./common/schemaUtilsCommon.js'), + { getServersPathVars, resolvePathItemRefs } = require('./common/schemaUtilsCommon.js'), { generateError } = require('./common/generateValidationError.js'), MODULE_VERSION = { V1: 'v1', @@ -176,6 +176,11 @@ class SchemaPack { } this.openapi = specParseResult.openapi; + + // Resolve pathItem refs for OpenAPI 3.1 specs + // The bundler creates $refs to components.pathItems which need to be inline for conversion + this.openapi = resolvePathItemRefs(this.openapi); + this.validated = true; this.validationResult = { result: true, diff --git a/test/unit/bundle31.test.js b/test/unit/bundle31.test.js index 6fe0e4bf7..201e0c688 100644 --- a/test/unit/bundle31.test.js +++ b/test/unit/bundle31.test.js @@ -41,6 +41,153 @@ describe('bundle files method - 3.1', function () { expect(JSON.stringify(JSON.parse(res.output.data[0].bundledContent), null, 2)).to.be.equal(expected); }); + it('Should bundle and convert OpenAPI 3.1 multifile spec with pathItems (type: json)', function (done) { + let contentRootFile = fs.readFileSync(pathItem31 + '/root.yaml', 'utf8'), + user = fs.readFileSync(pathItem31 + '/path.yaml', 'utf8'), + input = { + type: 'multiFile', + specificationVersion: '3.1', + rootFiles: [ + { + path: '/root.yaml' + } + ], + data: [ + { + path: '/root.yaml', + content: contentRootFile + }, + { + path: '/path.yaml', + content: user + } + ], + options: {}, + bundleFormat: 'JSON' + }; + + // Step 1: Bundle the spec + Converter.bundle(input).then((bundleRes) => { + expect(bundleRes).to.not.be.empty; + expect(bundleRes.result).to.be.true; + expect(bundleRes.output.specification.version).to.equal('3.1'); + + const bundledContent = bundleRes.output.data[0].bundledContent; + const bundledSpec = JSON.parse(bundledContent); + + // Step 2: Convert the bundled spec with type 'json' + Converter.convertV2WithTypes( + { type: 'json', data: bundledSpec }, + { schemaFaker: true, includeWebhooks: true }, + (err, conversionResult) => { + expect(err).to.be.null; + expect(conversionResult.result).to.be.true; + expect(conversionResult.output.length).to.equal(1); + expect(conversionResult.output[0].type).to.equal('collection'); + + const collection = conversionResult.output[0].data; + expect(collection).to.have.property('info'); + expect(collection).to.have.property('item'); + + // Verify that items are not empty (pathItems should be resolved) + expect(collection.item.length).to.be.greaterThan(0); + + // Count total requests to ensure pathItems were resolved + let requestCount = 0; + + /** + * Recursively count requests in collection items + * @param {Array} items - Collection items to count + * @returns {undefined} + */ + function countRequests(items) { + items.forEach((item) => { + if (item.request) { requestCount++; } + if (item.item) { countRequests(item.item); } + }); + } + countRequests(collection.item); + + expect(requestCount).to.be.greaterThan(0); + done(); + } + ); + }).catch(done); + }); + + it('Should bundle and convert OpenAPI 3.1 multifile spec with pathItems (type: string)', function (done) { + let contentRootFile = fs.readFileSync(pathItem31 + '/root.yaml', 'utf8'), + user = fs.readFileSync(pathItem31 + '/path.yaml', 'utf8'), + input = { + type: 'multiFile', + specificationVersion: '3.1', + rootFiles: [ + { + path: '/root.yaml' + } + ], + data: [ + { + path: '/root.yaml', + content: contentRootFile + }, + { + path: '/path.yaml', + content: user + } + ], + options: {}, + bundleFormat: 'JSON' + }; + + // Step 1: Bundle the spec + Converter.bundle(input).then((bundleRes) => { + expect(bundleRes).to.not.be.empty; + expect(bundleRes.result).to.be.true; + expect(bundleRes.output.specification.version).to.equal('3.1'); + + const bundledContentString = bundleRes.output.data[0].bundledContent; + + // Step 2: Convert the bundled spec with type 'string' (no parsing) + Converter.convertV2WithTypes( + { type: 'string', data: bundledContentString }, + { schemaFaker: true, includeWebhooks: true }, + (err, conversionResult) => { + expect(err).to.be.null; + expect(conversionResult.result).to.be.true; + expect(conversionResult.output.length).to.equal(1); + expect(conversionResult.output[0].type).to.equal('collection'); + + const collection = conversionResult.output[0].data; + expect(collection).to.have.property('info'); + expect(collection).to.have.property('item'); + + // Verify that items are not empty (pathItems should be resolved) + expect(collection.item.length).to.be.greaterThan(0); + + // Count total requests to ensure pathItems were resolved + let requestCount = 0; + + /** + * Recursively count requests in collection items + * @param {Array} items - Collection items to count + * @returns {undefined} + */ + function countRequests(items) { + items.forEach((item) => { + if (item.request) { requestCount++; } + if (item.item) { countRequests(item.item); } + }); + } + countRequests(collection.item); + + expect(requestCount).to.be.greaterThan(0); + done(); + } + ); + }).catch(done); + }); + it('Should return bundled file as json - webhook object', async function () { let contentRootFile = fs.readFileSync(webhookItem31 + '/root.yaml', 'utf8'), user = fs.readFileSync(webhookItem31 + '/webhook.yaml', 'utf8'), From d5597458ee50c6a8945d3854a300cafb3ade003e Mon Sep 17 00:00:00 2001 From: Animesh Jajoo Date: Thu, 23 Oct 2025 15:07:39 +0530 Subject: [PATCH 2/2] Refactor warning message for pathItem reference resolution --- lib/common/schemaUtilsCommon.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/common/schemaUtilsCommon.js b/lib/common/schemaUtilsCommon.js index 04724263d..d1906accb 100644 --- a/lib/common/schemaUtilsCommon.js +++ b/lib/common/schemaUtilsCommon.js @@ -383,7 +383,7 @@ module.exports = { return spec; } - // Iterate through all paths and resolve refs in place (no clone needed) + // Iterate through all paths and resolve refs in place _.forEach(spec.paths, (pathValue, pathKey) => { // Check if this path is a $ref to a pathItem if (pathValue && pathValue.$ref && typeof pathValue.$ref === 'string') { @@ -398,7 +398,7 @@ module.exports = { spec.paths[pathKey] = pathItemValue; } else { - console.warn(`[openapi-to-postman] PathItem reference not found: ${pathItemKey}`); + console.warn(`PathItem reference not found: ${pathItemKey}`); } } }