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: 0 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,6 @@
"no-unneeded-ternary": "error",
"no-whitespace-before-property": "error",
"object-curly-spacing": ["error", "always"],
"one-var": ["error", "always"],
"one-var-declaration-per-line": "error",
"operator-assignment": "error",
"operator-linebreak": ["error", "after"],
Expand Down
1 change: 0 additions & 1 deletion libV2/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable one-var */
const _ = require('lodash'),
{ Collection } = require('postman-collection/lib/collection/collection'),
GraphLib = require('graphlib'),
Expand Down
70 changes: 53 additions & 17 deletions libV2/schemaUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,21 @@ let QUERYPARAM = 'query',
* @returns {Object} Resolved schema
*/
resolveAllOfSchema = (context, schema, stack = 0, resolveFor = CONVERSION, seenRef = {}, currentPath = '') => {
/*
For TYPES_GENERATION, we do not want to merge the allOf schemas
instead we want to keep them separate so that we can generate types like:
allOf: [
{ $ref: '#/components/schemas/User' },
{
type: 'object',
properties: {
timestamp: { type: 'string', format: 'date-time' }
}
}
]
If we merge the schemas, we will loose the information that the schema was
a combination of multiple schemas
*/
if (resolveFor === TYPES_GENERATION) {
return {
allOf: _.map(schema.allOf, (schema) => {
Expand Down Expand Up @@ -553,7 +568,6 @@ let QUERYPARAM = 'query',

stack++;

// eslint-disable-next-line one-var
const compositeKeyword = schema.anyOf ? 'anyOf' : 'oneOf',
{ concreteUtils } = context;

Expand Down Expand Up @@ -628,7 +642,6 @@ let QUERYPARAM = 'query',
writeOnlyPropCache: context.writeOnlyPropCache
};

// eslint-disable-next-line one-var
const newReadPropCache = context.readOnlyPropCache,
newWritePropCache = context.writeOnlyPropCache;

Expand Down Expand Up @@ -1471,7 +1484,6 @@ let QUERYPARAM = 'query',
});
});

// eslint-disable-next-line one-var
let responseExample,
responseExampleData;

Expand Down Expand Up @@ -2114,14 +2126,18 @@ let QUERYPARAM = 'query',
param = resolveSchema(context, param);
}

if (_.has(param.schema, '$ref')) {
param.schema = resolveSchema(context, param.schema);
}

if (param.in !== QUERYPARAM || (!includeDeprecated && param.deprecated)) {
return;
}

const shouldResolveSchema = _.has(param, 'schema') &&
(_.has(param.schema, '$ref') || _.has(param.schema, 'anyOf') ||
_.has(param.schema, 'oneOf') || _.has(param.schema, 'allOf'));

if (shouldResolveSchema) {
param.schema = resolveSchema(context, param.schema);
}

let queryParamTypeInfo = {},
properties = {},
paramValue = resolveValueOfParameter(context, param);
Expand Down Expand Up @@ -2163,14 +2179,19 @@ let QUERYPARAM = 'query',
param = resolveSchema(context, param);
}

if (_.has(param.schema, '$ref')) {
param.schema = resolveSchema(context, param.schema);
}

if (param.in !== PATHPARAM) {
return;
}


const shouldResolveSchema = _.has(param, 'schema') &&
Copy link
Collaborator

Choose a reason for hiding this comment

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

you can make it concise with
const shouldResolveSchema = _.some(['$ref', 'anyOf', 'oneOf', 'allOf'], (key) => { return _.has(param, ['schema', key]); });

(_.has(param.schema, '$ref') || _.has(param.schema, 'anyOf') ||
_.has(param.schema, 'oneOf') || _.has(param.schema, 'allOf'));

if (shouldResolveSchema) {
param.schema = resolveSchema(context, param.schema);
}

let pathParamTypeInfo = {},
properties = {},
paramValue = resolveValueOfParameter(context, param);
Expand Down Expand Up @@ -2240,14 +2261,18 @@ let QUERYPARAM = 'query',
param = resolveSchema(context, param);
}

if (_.has(param.schema, '$ref')) {
param.schema = resolveSchema(context, param.schema);
}

if (param.in !== HEADER || (!includeDeprecated && param.deprecated)) {
return;
}

const shouldResolveSchema = _.has(param, 'schema') &&
(_.has(param.schema, '$ref') || _.has(param.schema, 'anyOf') ||
_.has(param.schema, 'oneOf') || _.has(param.schema, 'allOf'));

if (shouldResolveSchema) {
param.schema = resolveSchema(context, param.schema);
}

if (!keepImplicitHeaders && _.includes(IMPLICIT_HEADERS, _.toLower(_.get(param, 'name')))) {
return;
}
Expand Down Expand Up @@ -2400,8 +2425,19 @@ let QUERYPARAM = 'query',

headers.push(...serialisedHeader);

if (headerData && headerData.name && headerData.schema && headerData.schema.type) {
const { schema } = headerData;
if (headerData && headerData.name && headerData.schema) {
let { schema } = headerData;
const shouldResolveSchema = _.has(schema, '$ref') || _.has(schema, 'anyOf') ||
_.has(schema, 'oneOf') || _.has(schema, 'allOf');

if (shouldResolveSchema) {
schema = resolveSchema(context, schema);
}

if (!schema.type) {
return;
}

properties = {
type: schema.type,
format: schema.format,
Expand Down
215 changes: 214 additions & 1 deletion test/unit/convertV2WithTypes.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* eslint-disable max-len */
// Disabling max Length for better visibility of the expectedExtractedTypes

/* eslint-disable one-var */
/* Disabling as we want the checks to run in order of their declaration as declaring everything as once
even though initial declarations fails with test won't do any good */

Expand Down Expand Up @@ -764,6 +763,220 @@ describe('convertV2WithTypes', function() {
done();
});
});

it('should extract only first option from composite query parameters, path parameters, request headers, and response headers', function(done) {
const openApiWithCompositeParams = {
openapi: '3.0.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {
'/test/{pathParam}': {
get: {
parameters: [
// Query parameters with composite schemas
{
name: 'status',
in: 'query',
schema: {
oneOf: [
{ type: 'string', enum: ['active', 'inactive'] },
{ type: 'integer', minimum: 1, maximum: 10 }
]
}
},
{
name: 'category',
in: 'query',
schema: {
anyOf: [
{ type: 'string' },
{ type: 'number' }
]
}
},
{
name: 'priority',
in: 'query',
schema: {
allOf: [
{ type: 'string' },
{ minLength: 3 }
]
}
},
// Path parameter with composite schema
{
name: 'pathParam',
in: 'path',
required: true,
schema: {
oneOf: [
{ type: 'string', pattern: '^[a-z]+$' },
{ type: 'integer', minimum: 100 }
]
}
},
// Header parameters with composite schemas
{
name: 'X-Custom-Header',
in: 'header',
schema: {
anyOf: [
{ type: 'string', format: 'uuid' },
{ type: 'string', enum: ['default', 'custom'] }
]
}
},
{
name: 'X-Version',
in: 'header',
schema: {
allOf: [
{ type: 'string' },
{ pattern: '^v\\d+\\.\\d+$' }
]
}
}
],
responses: {
'200': {
description: 'Success',
headers: {
'X-Rate-Limit': {
description: 'Rate limit header with composite schema',
schema: {
oneOf: [
{ type: 'integer', minimum: 1, maximum: 1000 },
{ type: 'string', enum: ['unlimited', 'blocked'] }
]
}
},
'X-Response-Type': {
description: 'Response type header with composite schema',
schema: {
anyOf: [
{ type: 'string', format: 'uri' },
{ type: 'string', pattern: '^[A-Z_]+$' }
]
}
},
'X-Content-Version': {
description: 'Content version header with composite schema',
schema: {
allOf: [
{ type: 'string' },
{ pattern: '^v\\d+\\.\\d+\\.\\d+$' },
{ minLength: 5 }
]
}
}
},
content: {
'application/json': {
schema: {
anyOf: [
{ type: 'string' },
{ type: 'object', properties: { message: { type: 'string' } } }
]
}
}
}
}
}
}
}
}
};

Converter.convertV2WithTypes({ type: 'json', data: openApiWithCompositeParams }, {}, (err, conversionResult) => {
expect(err).to.be.null;
expect(conversionResult.extractedTypes).to.be.an('object').that.is.not.empty;

const extractedTypes = conversionResult.extractedTypes['get/test/{pathParam}'];

// Verify query parameters extract only first option
const queryParams = JSON.parse(extractedTypes.request.queryParam);
expect(queryParams).to.be.an('array').with.length(3);

// Check oneOf query parameter - should extract first option (string with enum)
expect(queryParams[0]).to.have.property('keyName', 'status');
expect(queryParams[0].properties).to.have.property('type', 'string');
expect(queryParams[0].properties).to.have.property('enum');
expect(queryParams[0].properties.enum).to.deep.equal(['active', 'inactive']);
expect(queryParams[0].properties).to.not.have.property('oneOf');

// Check anyOf query parameter - should extract first option (string)
expect(queryParams[1]).to.have.property('keyName', 'category');
expect(queryParams[1].properties).to.have.property('type', 'string');
expect(queryParams[1].properties).to.not.have.property('anyOf');

// Check allOf query parameter - should merge constraints (string with minLength)
expect(queryParams[2]).to.have.property('keyName', 'priority');
expect(queryParams[2].properties).to.have.property('type', 'string');
expect(queryParams[2].properties).to.have.property('minLength', 3);
expect(queryParams[2].properties).to.not.have.property('allOf');

// Verify path parameters extract only first option
const pathParams = JSON.parse(extractedTypes.request.pathParam);
expect(pathParams).to.be.an('array').with.length(1);

// Check oneOf path parameter - should extract first option (string with pattern)
expect(pathParams[0]).to.have.property('keyName', 'pathParam');
expect(pathParams[0].properties).to.have.property('type', 'string');
expect(pathParams[0].properties).to.have.property('pattern', '^[a-z]+$');
expect(pathParams[0].properties).to.not.have.property('oneOf');

// Verify headers extract only first option
const headers = JSON.parse(extractedTypes.request.headers);
expect(headers).to.be.an('array').with.length(2);

// Check anyOf header - should extract first option (string with format)
expect(headers[0]).to.have.property('keyName', 'X-Custom-Header');
expect(headers[0].properties).to.have.property('type', 'string');
expect(headers[0].properties).to.have.property('format', 'uuid');
expect(headers[0].properties).to.not.have.property('anyOf');

// Check allOf header - should merge constraints (string with pattern)
expect(headers[1]).to.have.property('keyName', 'X-Version');
expect(headers[1].properties).to.have.property('type', 'string');
expect(headers[1].properties).to.have.property('pattern', '^v\\d+\\.\\d+$');
expect(headers[1].properties).to.not.have.property('allOf');

// Verify response body preserves full composite schema
const responseBody = conversionResult.extractedTypes['get/test/{pathParam}'].response['200'].body;
const parsedResponseBody = JSON.parse(responseBody);

expect(parsedResponseBody).to.have.property('anyOf');
expect(parsedResponseBody.anyOf).to.be.an('array').with.length(2);
expect(parsedResponseBody.anyOf[0]).to.have.property('type', 'string');
expect(parsedResponseBody.anyOf[1]).to.have.property('type', 'object');

// Verify response headers extract only first option
const responseHeaders = JSON.parse(extractedTypes.response['200'].headers);
expect(responseHeaders).to.be.an('array').with.length(3);

// Check oneOf response header - should extract first option (integer with constraints)
expect(responseHeaders[0]).to.have.property('keyName', 'X-Rate-Limit');
expect(responseHeaders[0].properties).to.have.property('type', 'integer');
expect(responseHeaders[0].properties).to.have.property('minimum', 1);
expect(responseHeaders[0].properties).to.have.property('maximum', 1000);
expect(responseHeaders[0].properties).to.not.have.property('oneOf');

// Check anyOf response header - should extract first option (string with format)
expect(responseHeaders[1]).to.have.property('keyName', 'X-Response-Type');
expect(responseHeaders[1].properties).to.have.property('type', 'string');
expect(responseHeaders[1].properties).to.have.property('format', 'uri');
expect(responseHeaders[1].properties).to.not.have.property('anyOf');

// Check allOf response header - should merge constraints (string with pattern and minLength)
expect(responseHeaders[2]).to.have.property('keyName', 'X-Content-Version');
expect(responseHeaders[2].properties).to.have.property('type', 'string');
expect(responseHeaders[2].properties).to.have.property('pattern', '^v\\d+\\.\\d+\\.\\d+$');
expect(responseHeaders[2].properties).to.have.property('minLength', 5);
expect(responseHeaders[2].properties).to.not.have.property('allOf');

done();
});
});
});

});
Loading