Skip to content

Commit fcada82

Browse files
Animesh JajooAnimesh Jajoo
authored andcommitted
Add pathItem reference resolution for OpenAPI 3.1 specs
1 parent 11b04ff commit fcada82

File tree

3 files changed

+191
-1
lines changed

3 files changed

+191
-1
lines changed

lib/common/schemaUtilsCommon.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,5 +367,43 @@ module.exports = {
367367
[];
368368
return [...acc, ...newVarNames];
369369
}, []);
370+
},
371+
372+
/**
373+
* Resolves $ref pointers in paths that reference components.pathItems
374+
* This is necessary for OpenAPI 3.1 specs where the bundler creates refs to pathItems
375+
* but the converter needs them inline for proper processing
376+
*
377+
* @param {Object} spec - The OpenAPI specification object (mutated in place)
378+
* @returns {Object} - The same spec object with pathItem refs resolved
379+
*/
380+
resolvePathItemRefs: function(spec) {
381+
// If no paths or no pathItems component, nothing to resolve
382+
if (!spec || !spec.paths || !_.get(spec, 'components.pathItems')) {
383+
return spec;
384+
}
385+
386+
// Iterate through all paths and resolve refs in place (no clone needed)
387+
_.forEach(spec.paths, (pathValue, pathKey) => {
388+
// Check if this path is a $ref to a pathItem
389+
if (pathValue && pathValue.$ref && typeof pathValue.$ref === 'string') {
390+
// Match patterns like "#/components/pathItems/paths_pets.yaml"
391+
const match = pathValue.$ref.match(/#\/components\/pathItems\/(.+)/);
392+
393+
if (match && match[1]) {
394+
const pathItemKey = match[1];
395+
const pathItemValue = _.get(spec, ['components', 'pathItems', pathItemKey]);
396+
397+
if (pathItemValue) {
398+
spec.paths[pathKey] = pathItemValue;
399+
}
400+
else {
401+
console.warn(`[openapi-to-postman] PathItem reference not found: ${pathItemKey}`);
402+
}
403+
}
404+
}
405+
});
406+
407+
return spec;
370408
}
371409
};

lib/schemapack.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const { getConcreteSchemaUtils } = require('./common/versionUtils.js'),
2626
OpenApiErr = require('./error.js'),
2727
schemaUtils = require('./schemaUtils'),
2828
v2 = require('../libV2/index'),
29-
{ getServersPathVars } = require('./common/schemaUtilsCommon.js'),
29+
{ getServersPathVars, resolvePathItemRefs } = require('./common/schemaUtilsCommon.js'),
3030
{ generateError } = require('./common/generateValidationError.js'),
3131
MODULE_VERSION = {
3232
V1: 'v1',
@@ -176,6 +176,11 @@ class SchemaPack {
176176
}
177177

178178
this.openapi = specParseResult.openapi;
179+
180+
// Resolve pathItem refs for OpenAPI 3.1 specs
181+
// The bundler creates $refs to components.pathItems which need to be inline for conversion
182+
this.openapi = resolvePathItemRefs(this.openapi);
183+
179184
this.validated = true;
180185
this.validationResult = {
181186
result: true,

test/unit/bundle31.test.js

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,153 @@ describe('bundle files method - 3.1', function () {
4141
expect(JSON.stringify(JSON.parse(res.output.data[0].bundledContent), null, 2)).to.be.equal(expected);
4242
});
4343

44+
it('Should bundle and convert OpenAPI 3.1 multifile spec with pathItems (type: json)', function (done) {
45+
let contentRootFile = fs.readFileSync(pathItem31 + '/root.yaml', 'utf8'),
46+
user = fs.readFileSync(pathItem31 + '/path.yaml', 'utf8'),
47+
input = {
48+
type: 'multiFile',
49+
specificationVersion: '3.1',
50+
rootFiles: [
51+
{
52+
path: '/root.yaml'
53+
}
54+
],
55+
data: [
56+
{
57+
path: '/root.yaml',
58+
content: contentRootFile
59+
},
60+
{
61+
path: '/path.yaml',
62+
content: user
63+
}
64+
],
65+
options: {},
66+
bundleFormat: 'JSON'
67+
};
68+
69+
// Step 1: Bundle the spec
70+
Converter.bundle(input).then((bundleRes) => {
71+
expect(bundleRes).to.not.be.empty;
72+
expect(bundleRes.result).to.be.true;
73+
expect(bundleRes.output.specification.version).to.equal('3.1');
74+
75+
const bundledContent = bundleRes.output.data[0].bundledContent;
76+
const bundledSpec = JSON.parse(bundledContent);
77+
78+
// Step 2: Convert the bundled spec with type 'json'
79+
Converter.convertV2WithTypes(
80+
{ type: 'json', data: bundledSpec },
81+
{ schemaFaker: true, includeWebhooks: true },
82+
(err, conversionResult) => {
83+
expect(err).to.be.null;
84+
expect(conversionResult.result).to.be.true;
85+
expect(conversionResult.output.length).to.equal(1);
86+
expect(conversionResult.output[0].type).to.equal('collection');
87+
88+
const collection = conversionResult.output[0].data;
89+
expect(collection).to.have.property('info');
90+
expect(collection).to.have.property('item');
91+
92+
// Verify that items are not empty (pathItems should be resolved)
93+
expect(collection.item.length).to.be.greaterThan(0);
94+
95+
// Count total requests to ensure pathItems were resolved
96+
let requestCount = 0;
97+
98+
/**
99+
* Recursively count requests in collection items
100+
* @param {Array} items - Collection items to count
101+
* @returns {undefined}
102+
*/
103+
function countRequests(items) {
104+
items.forEach((item) => {
105+
if (item.request) { requestCount++; }
106+
if (item.item) { countRequests(item.item); }
107+
});
108+
}
109+
countRequests(collection.item);
110+
111+
expect(requestCount).to.be.greaterThan(0);
112+
done();
113+
}
114+
);
115+
}).catch(done);
116+
});
117+
118+
it('Should bundle and convert OpenAPI 3.1 multifile spec with pathItems (type: string)', function (done) {
119+
let contentRootFile = fs.readFileSync(pathItem31 + '/root.yaml', 'utf8'),
120+
user = fs.readFileSync(pathItem31 + '/path.yaml', 'utf8'),
121+
input = {
122+
type: 'multiFile',
123+
specificationVersion: '3.1',
124+
rootFiles: [
125+
{
126+
path: '/root.yaml'
127+
}
128+
],
129+
data: [
130+
{
131+
path: '/root.yaml',
132+
content: contentRootFile
133+
},
134+
{
135+
path: '/path.yaml',
136+
content: user
137+
}
138+
],
139+
options: {},
140+
bundleFormat: 'JSON'
141+
};
142+
143+
// Step 1: Bundle the spec
144+
Converter.bundle(input).then((bundleRes) => {
145+
expect(bundleRes).to.not.be.empty;
146+
expect(bundleRes.result).to.be.true;
147+
expect(bundleRes.output.specification.version).to.equal('3.1');
148+
149+
const bundledContentString = bundleRes.output.data[0].bundledContent;
150+
151+
// Step 2: Convert the bundled spec with type 'string' (no parsing)
152+
Converter.convertV2WithTypes(
153+
{ type: 'string', data: bundledContentString },
154+
{ schemaFaker: true, includeWebhooks: true },
155+
(err, conversionResult) => {
156+
expect(err).to.be.null;
157+
expect(conversionResult.result).to.be.true;
158+
expect(conversionResult.output.length).to.equal(1);
159+
expect(conversionResult.output[0].type).to.equal('collection');
160+
161+
const collection = conversionResult.output[0].data;
162+
expect(collection).to.have.property('info');
163+
expect(collection).to.have.property('item');
164+
165+
// Verify that items are not empty (pathItems should be resolved)
166+
expect(collection.item.length).to.be.greaterThan(0);
167+
168+
// Count total requests to ensure pathItems were resolved
169+
let requestCount = 0;
170+
171+
/**
172+
* Recursively count requests in collection items
173+
* @param {Array} items - Collection items to count
174+
* @returns {undefined}
175+
*/
176+
function countRequests(items) {
177+
items.forEach((item) => {
178+
if (item.request) { requestCount++; }
179+
if (item.item) { countRequests(item.item); }
180+
});
181+
}
182+
countRequests(collection.item);
183+
184+
expect(requestCount).to.be.greaterThan(0);
185+
done();
186+
}
187+
);
188+
}).catch(done);
189+
});
190+
44191
it('Should return bundled file as json - webhook object', async function () {
45192
let contentRootFile = fs.readFileSync(webhookItem31 + '/root.yaml', 'utf8'),
46193
user = fs.readFileSync(webhookItem31 + '/webhook.yaml', 'utf8'),

0 commit comments

Comments
 (0)