Skip to content
Open
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
38 changes: 38 additions & 0 deletions lib/common/schemaUtilsCommon.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
_.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"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is this a requirement ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm assuming you're asking this for the typeof check.

Type checking for string refs ensures valid spec references are being used. Instead of throwing an error, I'm ignoring that path reference.

Also .match() on line 391 will throw an error if the ref is not a string.

Copy link
Collaborator

Choose a reason for hiding this comment

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

No I meant about this match pattern itself ? Can you share the documentation link that mentions that the $refs needs to follow the pattern ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

OAS 3.1 Components: https://spec.openapis.org/oas/v3.1.0.html#components-object

$refs are defined similar to other versions, but generateObjectName() adds the file extension while bundling.

"referenceMap": {
    "#/components/pathItems/paths_pets.yaml": {
        "path": "paths/pets.yaml",
        "type": "component"
    },
}
Screenshot 2025-10-23 at 3 04 42 PM

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(`PathItem reference not found: ${pathItemKey}`);
}
}
}
});

return spec;
}
};
7 changes: 6 additions & 1 deletion lib/schemapack.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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,
Expand Down
147 changes: 147 additions & 0 deletions test/unit/bundle31.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
Loading