diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8b8a8236..92e13e75 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,8 @@
## [Unreleased]
+## [v4.25.0] - 2025-01-15
+
## [v4.24.0] - 2024-08-13
### Added
@@ -647,7 +649,9 @@ Newer releases follow the [Keep a Changelog](https://keepachangelog.com/en/1.0.0
- Base release
-[Unreleased]: https://github.com/postmanlabs/openapi-to-postman/compare/v4.24.0...HEAD
+[Unreleased]: https://github.com/postmanlabs/openapi-to-postman/compare/v4.25.0...HEAD
+
+[v4.25.0]: https://github.com/postmanlabs/openapi-to-postman/compare/v4.24.0...v4.25.0
[v4.24.0]: https://github.com/postmanlabs/openapi-to-postman/compare/v4.23.1...v4.24.0
diff --git a/README.md b/README.md
index 12eec460..d9afabc5 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,7 @@
---
---
-## 💠Getting Started
+
💠Getting Started
To use the converter as a Node module, you need to have a copy of the NodeJS runtime. The easiest way to do this is through npm. If you have NodeJS installed you have npm installed as well.
@@ -48,7 +48,7 @@ $ npm i -g openapi-to-postmanv2
```
-## 📖 Command Line Interface
+📖 Command Line Interface
The converter can be used as a CLI tool as well. The following [command line options](#options) are available.
@@ -107,7 +107,7 @@ $ openapi2postmanv2 --test
```
-## 🛠Using the converter as a NodeJS module
+🛠Using the converter as a NodeJS module
In order to use the convert in your node application, you need to import the package using `require`.
@@ -219,7 +219,7 @@ The validate function is synchronous and returns a status object which conforms
- `reason` - Provides a reason for an unsuccessful validation of the specification
-## 🧠Conversion Schema
+🧠Conversion Schema
| *postman* | *openapi* | *related options* |
| --- | --- | :---: |
diff --git a/index.js b/index.js
index 98fc7ffa..a8738501 100644
--- a/index.js
+++ b/index.js
@@ -28,6 +28,17 @@ module.exports = {
return cb(new UserError(_.get(schema, 'validationResult.reason', DEFAULT_INVALID_ERROR)));
},
+ convertV2WithTypes: function(input, options, cb) {
+ const enableTypeFetching = true;
+ var schema = new SchemaPack(input, options, MODULE_VERSION.V2, enableTypeFetching);
+
+ if (schema.validated) {
+ return schema.convertV2(cb);
+ }
+
+ return cb(new UserError(_.get(schema, 'validationResult.reason', DEFAULT_INVALID_ERROR)));
+ },
+
validate: function (input) {
var schema = new SchemaPack(input);
return schema.validationResult;
diff --git a/lib/schemapack.js b/lib/schemapack.js
index e3bd8d17..551dd965 100644
--- a/lib/schemapack.js
+++ b/lib/schemapack.js
@@ -38,7 +38,7 @@ let path = require('path'),
pathBrowserify = require('path-browserify');
class SchemaPack {
- constructor (input, options = {}, moduleVersion = MODULE_VERSION.V1) {
+ constructor (input, options = {}, moduleVersion = MODULE_VERSION.V1, enableTypeFetching = false) {
if (input.type === schemaUtils.MULTI_FILE_API_TYPE_ALLOWED_VALUE &&
input.data && input.data[0] && input.data[0].path) {
input = schemaUtils.mapDetectRootFilesInputToFolderInput(input);
@@ -57,7 +57,7 @@ class SchemaPack {
actualStack: 0,
numberOfRequests: 0
};
-
+ this.enableTypeFetching = enableTypeFetching;
this.computedOptions = utils.mergeOptions(
// predefined options
_.keyBy(this.definedOptions, 'id'),
diff --git a/libV2/index.js b/libV2/index.js
index 1c3ce5af..8f552f89 100644
--- a/libV2/index.js
+++ b/libV2/index.js
@@ -32,7 +32,8 @@ module.exports = {
let preOrderTraversal = GraphLib.alg.preorder(collectionTree, 'root:collection');
- let collection = {};
+ let collection = {},
+ extractedTypesObject = {};
/**
* individually start generating the folder, request, collection
@@ -91,16 +92,19 @@ module.exports = {
// generate the request form the node
let request = {},
collectionVariables = [],
- requestObject = {};
+ requestObject = {},
+ requestTypesObject = {};
try {
- ({ request, collectionVariables } = resolvePostmanRequest(context,
+ ({ request, collectionVariables, requestTypesObject } = resolvePostmanRequest(context,
context.openapi.paths[node.meta.path],
node.meta.path,
node.meta.method
));
requestObject = generateRequestItemObject(request);
+ extractedTypesObject = Object.assign({}, extractedTypesObject, requestTypesObject);
+
}
catch (error) {
console.error(error);
@@ -217,7 +221,17 @@ module.exports = {
if (!_.isEmpty(collection.variable)) {
collection.variable = _.uniqBy(collection.variable, 'key');
}
-
+ if (context.enableTypeFetching) {
+ return cb(null, {
+ result: true,
+ output: [{
+ type: 'collection',
+ data: collection
+ }],
+ analytics: this.analytics || {},
+ extractedTypes: extractedTypesObject || {}
+ });
+ }
return cb(null, {
result: true,
output: [{
diff --git a/libV2/schemaUtils.js b/libV2/schemaUtils.js
index ea828681..f60fe244 100644
--- a/libV2/schemaUtils.js
+++ b/libV2/schemaUtils.js
@@ -469,7 +469,7 @@ let QUERYPARAM = 'query',
exampleKey = Object.keys(exampleObj)[0];
example = exampleObj[exampleKey];
- if (example.$ref) {
+ if (example && example.$ref) {
example = resolveExampleData(context, example);
}
@@ -736,6 +736,74 @@ let QUERYPARAM = 'query',
return schema;
},
+ /**
+ * Processes and resolves types from Nested JSON schema structure.
+ *
+ * @param {Object} resolvedSchema - The resolved JSON schema to process for type extraction.
+ * @returns {Object} The processed schema details.
+ */
+ processSchema = (resolvedSchema) => {
+ if (resolvedSchema.type === 'object' && resolvedSchema.properties) {
+ const schemaDetails = {
+ type: resolvedSchema.type,
+ properties: {},
+ required: []
+ },
+ requiredProperties = new Set(resolvedSchema.required || []);
+
+ for (let [propName, propValue] of Object.entries(resolvedSchema.properties)) {
+ if (!propValue.type) {
+ continue;
+ }
+ const propertyDetails = {
+ type: propValue.type,
+ deprecated: propValue.deprecated,
+ enum: propValue.enum || undefined,
+ minLength: propValue.minLength,
+ maxLength: propValue.maxLength,
+ minimum: propValue.minimum,
+ maximum: propValue.maximum,
+ pattern: propValue.pattern,
+ example: propValue.example,
+ description: propValue.description,
+ format: propValue.format
+ };
+
+ if (requiredProperties.has(propName)) {
+ schemaDetails.required.push(propName);
+ }
+ if (propValue.properties) {
+ let processedProperties = processSchema(propValue);
+ propertyDetails.properties = processedProperties.properties;
+ if (processedProperties.required) {
+ propertyDetails.required = processedProperties.required;
+ }
+ }
+ else if (propValue.type === 'array' && propValue.items) {
+ propertyDetails.items = processSchema(propValue.items);
+ }
+
+ schemaDetails.properties[propName] = propertyDetails;
+ }
+ if (schemaDetails.required && schemaDetails.required.length === 0) {
+ schemaDetails.required = undefined;
+ }
+ return schemaDetails;
+ }
+ else if (resolvedSchema.type === 'array' && resolvedSchema.items) {
+ const arrayDetails = {
+ type: resolvedSchema.type,
+ items: processSchema(resolvedSchema.items)
+ };
+ if (resolvedSchema.minItems !== undefined) { arrayDetails.minItems = resolvedSchema.minItems; }
+ if (resolvedSchema.maxItems !== undefined) { arrayDetails.maxItems = resolvedSchema.maxItems; }
+ return arrayDetails;
+ }
+ return {
+ type: resolvedSchema.type
+ };
+ },
+
/**
* Wrapper around _resolveSchema which resolves a given schema
*
@@ -1407,14 +1475,19 @@ let QUERYPARAM = 'query',
bodyKey = isExampleBody ? 'response' : 'request',
responseExamples,
example,
- examples;
+ examples,
+ resolvedSchemaTypes = [];
if (_.isEmpty(requestBodySchema)) {
return [{ [bodyKey]: bodyData }];
}
if (requestBodySchema.$ref) {
- requestBodySchema = resolveSchema(context, requestBodySchema, { isResponseSchema: isExampleBody });
+ requestBodySchema = resolveSchema(
+ context,
+ requestBodySchema,
+ { isResponseSchema: isExampleBody }
+ );
}
/**
@@ -1443,6 +1516,7 @@ let QUERYPARAM = 'query',
* b: 2
* }
*/
+
if (requestBodySchema.example !== undefined) {
const shouldResolveValueKey = _.has(requestBodySchema.example, 'value') &&
_.keys(requestBodySchema.example).length <= 1;
@@ -1463,7 +1537,10 @@ let QUERYPARAM = 'query',
examples = requestBodySchema.examples || _.get(requestBodySchema, 'schema.examples');
requestBodySchema = requestBodySchema.schema || requestBodySchema;
- requestBodySchema = resolveSchema(context, requestBodySchema, { isResponseSchema: isExampleBody });
+ requestBodySchema = resolveSchema(
+ context,
+ requestBodySchema,
+ { isResponseSchema: isExampleBody });
// If schema object has example defined, try to use that if no example is defiend at request body level
if (example === undefined && _.get(requestBodySchema, 'example') !== undefined) {
@@ -1488,7 +1565,10 @@ let QUERYPARAM = 'query',
requestBodySchema = requestBodySchema.schema || requestBodySchema;
if (requestBodySchema.$ref) {
- requestBodySchema = resolveSchema(context, requestBodySchema, { isResponseSchema: isExampleBody });
+ requestBodySchema = resolveSchema(
+ context,
+ requestBodySchema,
+ { isResponseSchema: isExampleBody });
}
if (isBodyTypeXML) {
@@ -1528,6 +1608,12 @@ let QUERYPARAM = 'query',
bodyData = '';
}
}
+
+ }
+
+ if (context.enableTypeFetching && requestBodySchema.type !== undefined) {
+ const requestBodySchemaTypes = processSchema(requestBodySchema);
+ resolvedSchemaTypes.push(requestBodySchemaTypes);
}
// Generate multiple examples when either request or response contains more than one example
@@ -1560,10 +1646,19 @@ let QUERYPARAM = 'query',
matchedRequestBodyExamples = requestBodyExamples;
}
- return generateExamples(context, responseExamples, matchedRequestBodyExamples, requestBodySchema, isBodyTypeXML);
+ const generatedBody = generateExamples(
+ context, responseExamples, matchedRequestBodyExamples, requestBodySchema, isBodyTypeXML);
+
+ return {
+ generatedBody,
+ resolvedSchemaType: resolvedSchemaTypes[0]
+ };
}
- return [{ [bodyKey]: bodyData }];
+ return {
+ generatedBody: [{ [bodyKey]: bodyData }],
+ resolvedSchemaType: resolvedSchemaTypes[0]
+ };
},
resolveUrlEncodedRequestBodyForPostmanRequest = (context, requestBodyContent) => {
@@ -1573,7 +1668,9 @@ let QUERYPARAM = 'query',
mode: 'urlencoded',
urlencoded: urlEncodedParams
},
- resolvedBody;
+ resolvedBody,
+ resolvedBodyResult,
+ resolvedSchemaTypeObject;
if (_.isEmpty(requestBodyContent)) {
return requestBodyData;
@@ -1583,7 +1680,14 @@ let QUERYPARAM = 'query',
requestBodyContent.schema = resolveSchema(context, requestBodyContent.schema);
}
- resolvedBody = resolveBodyData(context, requestBodyContent.schema)[0];
+ resolvedBodyResult = resolveBodyData(context, requestBodyContent.schema);
+ resolvedBody =
+ resolvedBodyResult &&
+ Array.isArray(resolvedBodyResult.generatedBody) &&
+ resolvedBodyResult.generatedBody[0];
+
+ resolvedSchemaTypeObject = resolvedBodyResult && resolvedBodyResult.resolvedSchemaType;
+
resolvedBody && (bodyData = resolvedBody.request);
const encoding = requestBodyContent.encoding || {};
@@ -1618,7 +1722,8 @@ let QUERYPARAM = 'query',
headers: [{
key: 'Content-Type',
value: URLENCODED
- }]
+ }],
+ resolvedSchemaTypeObject
};
},
@@ -1630,13 +1735,22 @@ let QUERYPARAM = 'query',
mode: 'formdata',
formdata: formDataParams
},
- resolvedBody;
+ resolvedBody,
+ resolvedBodyResult,
+ resolvedSchemaTypeObject;
if (_.isEmpty(requestBodyContent)) {
return requestBodyData;
}
- resolvedBody = resolveBodyData(context, requestBodyContent.schema)[0];
+ resolvedBodyResult = resolveBodyData(context, requestBodyContent.schema);
+ resolvedBody =
+ resolvedBodyResult &&
+ Array.isArray(resolvedBodyResult.generatedBody) &&
+ resolvedBodyResult.generatedBody[0];
+
+ resolvedSchemaTypeObject = resolvedBodyResult && resolvedBodyResult.resolvedSchemaType;
+
resolvedBody && (bodyData = resolvedBody.request);
encoding = _.get(requestBodyContent, 'encoding', {});
@@ -1694,7 +1808,8 @@ let QUERYPARAM = 'query',
headers: [{
key: 'Content-Type',
value: FORM_DATA
- }]
+ }],
+ resolvedSchemaTypeObject
};
},
@@ -1739,7 +1854,9 @@ let QUERYPARAM = 'query',
headerFamily,
dataToBeReturned = {},
{ concreteUtils } = context,
- resolvedBody;
+ resolvedBody,
+ resolvedBodyResult,
+ resolvedSchemaTypeObject;
headerFamily = getHeaderFamily(bodyType);
@@ -1750,7 +1867,14 @@ let QUERYPARAM = 'query',
}
// Handling for Raw mode data
else {
- resolvedBody = resolveBodyData(context, requestContent[bodyType], bodyType)[0];
+ resolvedBodyResult = resolveBodyData(context, requestContent[bodyType], bodyType);
+ resolvedBody =
+ resolvedBodyResult &&
+ Array.isArray(resolvedBodyResult.generatedBody) &&
+ resolvedBodyResult.generatedBody[0];
+
+ resolvedSchemaTypeObject = resolvedBodyResult && resolvedBodyResult.resolvedSchemaType;
+
resolvedBody && (bodyData = resolvedBody.request);
if ((bodyType === TEXT_XML || bodyType === APP_XML || headerFamily === HEADER_TYPE.XML)) {
@@ -1782,7 +1906,8 @@ let QUERYPARAM = 'query',
headers: [{
key: 'Content-Type',
value: bodyType
- }]
+ }],
+ resolvedSchemaTypeObject
};
},
@@ -1896,9 +2021,28 @@ let QUERYPARAM = 'query',
return reqParam;
},
+ createProperties = (param) => {
+ const { schema } = param;
+ return {
+ type: schema.type,
+ format: schema.format,
+ default: schema.default,
+ required: param.required || false,
+ deprecated: param.deprecated || false,
+ enum: schema.enum || undefined,
+ minLength: schema.minLength,
+ maxLength: schema.maxLength,
+ minimum: schema.minimum,
+ maximum: schema.maximum,
+ pattern: schema.pattern,
+ example: schema.example
+ };
+ },
+
resolveQueryParamsForPostmanRequest = (context, operationItem, method) => {
const params = resolvePathItemParams(context, operationItem[method].parameters, operationItem.parameters),
pmParams = [],
+ queryParamTypes = [],
{ includeDeprecated } = context.computedOptions;
_.forEach(params, (param) => {
@@ -1910,11 +2054,23 @@ 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;
}
- let paramValue = resolveValueOfParameter(context, param);
+ let queryParamTypeInfo = {},
+ properties = {},
+ paramValue = resolveValueOfParameter(context, param);
+
+ if (param && param.name && param.schema && param.schema.type) {
+ properties = createProperties(param);
+ queryParamTypeInfo = { keyName: param.name, properties };
+ queryParamTypes.push(queryParamTypeInfo);
+ }
if (typeof paramValue === 'number' || typeof paramValue === 'boolean') {
// the SDK will keep the number-ness,
@@ -1927,14 +2083,16 @@ let QUERYPARAM = 'query',
const deserialisedParams = serialiseParamsBasedOnStyle(context, param, paramValue);
pmParams.push(...deserialisedParams);
+
});
- return pmParams;
+ return { queryParamTypes, queryParams: pmParams };
},
resolvePathParamsForPostmanRequest = (context, operationItem, method) => {
const params = resolvePathItemParams(context, operationItem[method].parameters, operationItem.parameters),
- pmParams = [];
+ pmParams = [],
+ pathParamTypes = [];
_.forEach(params, (param) => {
if (!_.isObject(param)) {
@@ -1945,11 +2103,23 @@ let QUERYPARAM = 'query',
param = resolveSchema(context, param);
}
+ if (_.has(param.schema, '$ref')) {
+ param.schema = resolveSchema(context, param.schema);
+ }
+
if (param.in !== PATHPARAM) {
return;
}
- let paramValue = resolveValueOfParameter(context, param);
+ let pathParamTypeInfo = {},
+ properties = {},
+ paramValue = resolveValueOfParameter(context, param);
+
+ if (param && param.name && param.schema && param.schema.type) {
+ properties = createProperties(param);
+ pathParamTypeInfo = { keyName: param.name, properties };
+ pathParamTypes.push(pathParamTypeInfo);
+ }
if (typeof paramValue === 'number' || typeof paramValue === 'boolean') {
// the SDK will keep the number-ness,
@@ -1964,7 +2134,7 @@ let QUERYPARAM = 'query',
pmParams.push(...deserialisedParams);
});
- return pmParams;
+ return { pathParamTypes, pathParams: pmParams };
},
resolveNameForPostmanReqeust = (context, operationItem, requestUrl) => {
@@ -1998,6 +2168,7 @@ let QUERYPARAM = 'query',
resolveHeadersForPostmanRequest = (context, operationItem, method) => {
const params = resolvePathItemParams(context, operationItem[method].parameters, operationItem.parameters),
pmParams = [],
+ headerTypes = [],
{ keepImplicitHeaders, includeDeprecated } = context.computedOptions;
_.forEach(params, (param) => {
@@ -2009,6 +2180,10 @@ 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;
}
@@ -2017,7 +2192,15 @@ let QUERYPARAM = 'query',
return;
}
- let paramValue = resolveValueOfParameter(context, param);
+ let headerTypeInfo = {},
+ properties = {},
+ paramValue = resolveValueOfParameter(context, param);
+
+ if (param && param.name && param.schema && param.schema.type) {
+ properties = createProperties(param);
+ headerTypeInfo = { keyName: param.name, properties };
+ headerTypes.push(headerTypeInfo);
+ }
if (typeof paramValue === 'number' || typeof paramValue === 'boolean') {
// the SDK will keep the number-ness,
@@ -2032,7 +2215,7 @@ let QUERYPARAM = 'query',
pmParams.push(...deserialisedParams);
});
- return pmParams;
+ return { headerTypes, headers: pmParams };
},
/**
@@ -2053,7 +2236,9 @@ let QUERYPARAM = 'query',
acceptHeader,
emptyResponse = [{
body: undefined
- }];
+ }],
+ resolvedResponseBodyResult,
+ resolvedResponseBodyTypes;
if (_.isEmpty(responseBody)) {
return emptyResponse;
@@ -2072,7 +2257,10 @@ let QUERYPARAM = 'query',
bodyType = getRawBodyType(responseContent);
headerFamily = getHeaderFamily(bodyType);
- allBodyData = resolveBodyData(context, responseContent[bodyType], bodyType, true, code, requestBodyExamples);
+ resolvedResponseBodyResult = resolveBodyData(
+ context, responseContent[bodyType], bodyType, true, code, requestBodyExamples);
+ allBodyData = resolvedResponseBodyResult.generatedBody;
+ resolvedResponseBodyTypes = resolvedResponseBodyResult.resolvedSchemaType;
return _.map(allBodyData, (bodyData) => {
let requestBodyData = bodyData.request,
@@ -2111,14 +2299,16 @@ let QUERYPARAM = 'query',
}],
name: exampleName,
bodyType,
- acceptHeader
+ acceptHeader,
+ resolvedResponseBodyTypes: resolvedResponseBodyTypes
};
});
},
resolveResponseHeaders = (context, responseHeaders) => {
const headers = [],
- { includeDeprecated } = context.computedOptions;
+ { includeDeprecated } = context.computedOptions,
+ headerTypes = [];
if (_.has(responseHeaders, '$ref')) {
responseHeaders = resolveSchema(context, responseHeaders, { isResponseSchema: true });
@@ -2133,7 +2323,9 @@ let QUERYPARAM = 'query',
return;
}
- let headerValue = resolveValueOfParameter(context, value, { isResponseSchema: true });
+ let headerValue = resolveValueOfParameter(context, value, { isResponseSchema: true }),
+ headerTypeInfo = {},
+ properties = {};
if (typeof headerValue === 'number' || typeof headerValue === 'boolean') {
// the SDK will keep the number-ness,
@@ -2147,9 +2339,29 @@ let QUERYPARAM = 'query',
serialisedHeader = serialiseParamsBasedOnStyle(context, headerData, headerValue, { isResponseSchema: true });
headers.push(...serialisedHeader);
+
+ if (headerData && headerData.name && headerData.schema && headerData.schema.type) {
+ const { schema } = headerData;
+ properties = {
+ type: schema.type,
+ format: schema.format,
+ default: schema.default,
+ required: schema.required || false,
+ deprecated: schema.deprecated || false,
+ enum: schema.enum || undefined,
+ minLength: schema.minLength,
+ maxLength: schema.maxLength,
+ minimum: schema.minimum,
+ maximum: schema.maximum,
+ pattern: schema.pattern,
+ example: schema.example
+ };
+ headerTypeInfo = { keyName: headerData.name, properties };
+ headerTypes.push(headerTypeInfo);
+ }
});
- return headers;
+ return { resolvedHeaderTypes: headerTypes, headers };
},
getPreviewLangugaForResponseBody = (bodyType) => {
@@ -2240,7 +2452,9 @@ let QUERYPARAM = 'query',
requestContent,
rawBodyType,
headerFamily,
- isBodyTypeXML;
+ isBodyTypeXML,
+ resolvedExamplesObject = {},
+ responseTypes = {};
// store all request examples which will be used for creation of examples with correct request and response matching
if (typeof requestBody === 'object') {
@@ -2288,7 +2502,26 @@ let QUERYPARAM = 'query',
{ includeAuthInfoInExample } = context.computedOptions,
auth = request.auth,
resolvedExamples = resolveResponseBody(context, responseSchema, requestBodyExamples, code) || {},
- headers = resolveResponseHeaders(context, responseSchema.headers);
+ { resolvedHeaderTypes, headers } = resolveResponseHeaders(context, responseSchema.headers),
+ responseBodyHeaderObj;
+
+ /* since resolvedExamples is a list of objects, we are picking the head element everytime
+ as the types are generated per example and since we have response having same status code,
+ so their type would be also same */
+
+ resolvedExamplesObject = resolvedExamples[0] && resolvedExamples[0].resolvedResponseBodyTypes;
+
+ responseBodyHeaderObj =
+ {
+ body: JSON.stringify(resolvedExamplesObject, null, 2),
+ headers: JSON.stringify(resolvedHeaderTypes, null, 2)
+ };
+
+ // replace 'X' char in code with '0' | E.g. 5xx -> 500
+ code = code.replace(/X|x/g, '0');
+ code = code === 'default' ? 500 : _.toSafeInteger(code);
+
+ Object.assign(responseTypes, { [code]: responseBodyHeaderObj });
_.forOwn(resolvedExamples, (resolvedExample = {}) => {
let { body, contentHeader = [], bodyType, acceptHeader, name } = resolvedExample,
@@ -2350,8 +2583,11 @@ let QUERYPARAM = 'query',
responses.push(response);
});
});
-
- return { responses, acceptHeader: requestAcceptHeader };
+ return {
+ responses,
+ acceptHeader: requestAcceptHeader,
+ responseTypes: responseTypes
+ };
};
module.exports = {
@@ -2366,16 +2602,18 @@ module.exports = {
let url = resolveUrlForPostmanRequest(path),
baseUrlData = resolveBaseUrlForPostmanRequest(operationItem[method]),
requestName = resolveNameForPostmanReqeust(context, operationItem[method], url),
- queryParams = resolveQueryParamsForPostmanRequest(context, operationItem, method),
- headers = resolveHeadersForPostmanRequest(context, operationItem, method),
- pathParams = resolvePathParamsForPostmanRequest(context, operationItem, method),
+ { queryParamTypes, queryParams } = resolveQueryParamsForPostmanRequest(context, operationItem, method),
+ { headerTypes, headers } = resolveHeadersForPostmanRequest(context, operationItem, method),
+ { pathParamTypes, pathParams } = resolvePathParamsForPostmanRequest(context, operationItem, method),
{ pathVariables, collectionVariables } = filterCollectionAndPathVariables(url, pathParams),
requestBody = resolveRequestBodyForPostmanRequest(context, operationItem[method]),
+ requestBodyTypes = requestBody && requestBody.resolvedSchemaTypeObject,
request,
securitySchema = _.get(operationItem, [method, 'security']),
authHelper = generateAuthForCollectionFromOpenAPI(context.openapi, securitySchema),
- { alwaysInheritAuthentication } = context.computedOptions;
-
+ { alwaysInheritAuthentication } = context.computedOptions,
+ requestIdentifier,
+ requestTypesObject = {};
headers.push(..._.get(requestBody, 'headers', []));
pathVariables.push(...baseUrlData.pathVariables);
collectionVariables.push(...baseUrlData.collectionVariables);
@@ -2396,7 +2634,22 @@ module.exports = {
auth: alwaysInheritAuthentication ? undefined : authHelper
};
- const { responses, acceptHeader } = resolveResponseForPostmanRequest(context, operationItem[method], request);
+ const requestTypes = {
+ body: JSON.stringify(requestBodyTypes, null, 2),
+ headers: JSON.stringify(headerTypes, null, 2),
+ pathParam: JSON.stringify(pathParamTypes, null, 2),
+ queryParam: JSON.stringify(queryParamTypes, null, 2)
+ },
+
+ {
+ responses,
+ acceptHeader,
+ responseTypes
+ } = resolveResponseForPostmanRequest(context, operationItem[method], request);
+
+ requestIdentifier = method + path;
+ Object.assign(requestTypesObject,
+ { [requestIdentifier]: { request: requestTypes, response: responseTypes } });
// add accept header if found and not present already
if (!_.isEmpty(acceptHeader)) {
@@ -2410,7 +2663,8 @@ module.exports = {
responses
})
},
- collectionVariables
+ collectionVariables,
+ requestTypesObject
};
},
diff --git a/libV2/utils.js b/libV2/utils.js
index 9d4df9a8..33375b77 100644
--- a/libV2/utils.js
+++ b/libV2/utils.js
@@ -28,10 +28,6 @@ const _ = require('lodash'),
originalRequest.header = _.get(response, 'originalRequest.headers', []);
originalRequest.body = requestItem.request.body;
- // replace 'X' char with '0'
- response.code = response.code.replace(/X|x/g, '0');
- response.code = response.code === 'default' ? 500 : _.toSafeInteger(response.code);
-
let sdkResponse = new Response({
name: response.name,
code: response.code,
diff --git a/package-lock.json b/package-lock.json
index aa8f5528..f5c215ab 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "openapi-to-postmanv2",
- "version": "4.24.0",
+ "version": "4.25.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "openapi-to-postmanv2",
- "version": "4.24.0",
+ "version": "4.25.0",
"license": "Apache-2.0",
"dependencies": {
"ajv": "8.11.0",
@@ -5368,6 +5368,7 @@
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
diff --git a/package.json b/package.json
index 7283bc0e..36e1900b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "openapi-to-postmanv2",
- "version": "4.24.0",
+ "version": "4.25.0",
"description": "Convert a given OpenAPI specification to Postman Collection v2.0",
"homepage": "https://github.com/postmanlabs/openapi-to-postman",
"bugs": "https://github.com/postmanlabs/openapi-to-postman/issues",
@@ -121,6 +121,7 @@
"ajv-formats": "2.1.1",
"async": "3.2.4",
"commander": "2.20.3",
+ "graphlib": "2.1.8",
"js-yaml": "4.1.0",
"json-pointer": "0.6.2",
"json-schema-merge-allof": "0.8.1",
@@ -128,7 +129,6 @@
"neotraverse": "0.6.15",
"oas-resolver-browser": "2.5.6",
"object-hash": "3.0.0",
- "graphlib": "2.1.8",
"path-browserify": "1.0.1",
"postman-collection": "^4.4.0",
"swagger2openapi": "7.0.8",
diff --git a/test/unit/convertV2.test.js b/test/unit/convertV2.test.js
index 985e3499..98038d10 100644
--- a/test/unit/convertV2.test.js
+++ b/test/unit/convertV2.test.js
@@ -205,9 +205,9 @@ describe('The convert v2 Function', function() {
tooManyRefs, function(done) {
var openapi = fs.readFileSync(tooManyRefs, 'utf8');
Converter.convertV2({ type: 'string', data: openapi }, { schemaFaker: true }, (err, conversionResult) => {
-
expect(err).to.be.null;
expect(conversionResult.result).to.equal(true);
+ expect(conversionResult).to.not.have.property('extractedTypes');
expect(conversionResult.output.length).to.equal(1);
expect(conversionResult.output[0].type).to.equal('collection');
expect(conversionResult.output[0].data).to.have.property('info');
diff --git a/test/unit/convertV2WithTypes.test.js b/test/unit/convertV2WithTypes.test.js
new file mode 100644
index 00000000..a95c946a
--- /dev/null
+++ b/test/unit/convertV2WithTypes.test.js
@@ -0,0 +1,292 @@
+/* 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 */
+
+
+const expect = require('chai').expect,
+ Converter = require('../../index.js'),
+ fs = require('fs'),
+ path = require('path'),
+ VALID_OPENAPI_PATH = '../data/valid_openapi',
+ Ajv = require('ajv'),
+ testSpec = path.join(__dirname, VALID_OPENAPI_PATH + '/test.json'),
+ testSpec1 = path.join(__dirname, VALID_OPENAPI_PATH + '/test1.json'),
+ readOnlyNestedSpec =
+ path.join(__dirname, VALID_OPENAPI_PATH, '/readOnlyNested.json'),
+ ajv = new Ajv({ allErrors: true, strict: false }),
+ transformSchema = (schema) => {
+ const properties = schema.properties,
+ rest = Object.keys(schema)
+ .filter((key) => { return key !== 'properties'; })
+ .reduce((acc, key) => {
+ acc[key] = schema[key];
+ return acc;
+ }, {}),
+
+ transformedProperties = Object.entries(properties).reduce(
+ (acc, [key, value]) => {
+ acc[key] = {
+ type: value.type,
+ deprecated: value.deprecated || false,
+ enum: value.enum !== null ? value.enum : undefined,
+ minLength: value.minLength !== null ? value.minLength : undefined,
+ maxLength: value.maxLength !== null ? value.maxLength : undefined,
+ minimum: value.minimum !== null ? value.minimum : undefined,
+ maximum: value.maximum !== null ? value.maximum : undefined,
+ pattern: value.pattern !== null ? value.pattern : undefined,
+ format: value.format !== null ? value.format : undefined
+ };
+ return acc;
+ },
+ {}
+ ),
+
+
+ transformedObject = Object.assign({}, rest, { properties: transformedProperties });
+
+ return transformedObject;
+ };
+
+
+describe('convertV2WithTypes should generate collection conforming to collection schema', function() {
+
+ it('Should generate collection conforming to schema for and fail if not valid ' +
+ testSpec, function(done) {
+ var openapi = fs.readFileSync(testSpec, 'utf8');
+ Converter.convertV2WithTypes({ type: 'string', data: openapi }, { schemaFaker: true }, (err, conversionResult) => {
+ expect(err).to.be.null;
+ expect(conversionResult.result).to.equal(true);
+ expect(conversionResult.output.length).to.equal(1);
+ expect(conversionResult.output[0].type).to.equal('collection');
+ expect(conversionResult.output[0].data).to.have.property('info');
+ expect(conversionResult.output[0].data).to.have.property('item');
+ done();
+ });
+ });
+
+ it('should validate parameters of the collection', function (done) {
+ const openapi = fs.readFileSync(testSpec1, 'utf8'),
+ options = { schemaFaker: true, exampleParametersResolution: 'schema' };
+
+ Converter.convertV2WithTypes({ type: 'string', data: openapi }, options, (err, conversionResult) => {
+ expect(err).to.be.null;
+ expect(conversionResult.output).to.be.an('array').that.is.not.empty;
+
+ const firstFolder = conversionResult.output[0].data.item[0];
+ expect(firstFolder).to.have.property('name', 'pets');
+
+ const listAllPets = firstFolder.item[0];
+ expect(listAllPets).to.have.property('name', 'List all pets');
+ expect(listAllPets.request.method).to.equal('GET');
+
+ const createPet = firstFolder.item[1];
+ expect(createPet).to.have.property('name', '/pets');
+ expect(createPet.request.method).to.equal('POST');
+ expect(createPet.request.body.mode).to.equal('raw');
+ expect(createPet.request.body.raw).to.include('request body comes here');
+
+ const queryParams = listAllPets.request.url.query;
+ expect(queryParams).to.be.an('array').that.has.length(3);
+ expect(queryParams[0]).to.have.property('key', 'limit');
+ expect(queryParams[0]).to.have.property('value', '');
+
+ const headers = listAllPets.request.header;
+ expect(headers).to.be.an('array').that.is.not.empty;
+ expect(headers[0]).to.have.property('key', 'variable');
+ expect(headers[0]).to.have.property('value', ',');
+
+ const response = listAllPets.response[0];
+ expect(response).to.have.property('status', 'OK');
+ expect(response).to.have.property('code', 200);
+ expect(response.body).to.include('"id": ""');
+
+ done();
+ }
+ );
+ });
+
+ it('Should generate collection conforming to schema for and fail if not valid ' +
+ testSpec1, function(done) {
+ Converter.convertV2WithTypes(
+ { type: 'file', data: testSpec1 }, { requestNameSource: 'url' }, (err, conversionResult) => {
+ expect(err).to.be.null;
+ expect(conversionResult.result).to.equal(true);
+ expect(conversionResult.output.length).to.equal(1);
+ expect(conversionResult.output[0].type).to.equal('collection');
+ expect(conversionResult.output[0].data).to.have.property('info');
+ expect(conversionResult.output[0].data).to.have.property('item');
+
+ done();
+ });
+ });
+});
+
+
+describe('convertV2WithTypes', function() {
+ it('should contain extracted types' + testSpec1, function () {
+ Converter.convertV2WithTypes(
+ { type: 'file', data: testSpec1 }, { requestNameSource: 'url' }, (err, conversionResult) => {
+ expect(err).to.be.null;
+ expect(conversionResult.result).to.equal(true);
+ expect(conversionResult.extractedTypes).to.not.be.undefined;
+ expect(Object.keys(conversionResult.extractedTypes).length).to.not.equal(0);
+ }
+ );
+ });
+
+ it('should validate the generated type object' + testSpec1, function() {
+ const example = {
+ code: 200,
+ message: 'Success'
+ };
+ Converter.convertV2WithTypes(
+ { type: 'file', data: testSpec1 }, { requestNameSource: 'url' }, (err, conversionResult) => {
+
+ expect(err).to.be.null;
+ expect(conversionResult.extractedTypes).to.be.an('object').that.is.not.empty;
+ for (const [path, element] of Object.entries(conversionResult.extractedTypes)) {
+ expect(element).to.be.an('object').that.includes.keys('request');
+ expect(element).to.be.an('object').that.includes.keys('response');
+ expect(path).to.be.a('string');
+
+ const { response } = element;
+ expect(response).to.be.an('object').that.is.not.empty;
+ const [key, value] = Object.entries(response)[1];
+ expect(key).to.be.a('string');
+
+ const schema = JSON.parse(value.body),
+ transformedSchema = transformSchema(schema),
+ validate = ajv.compile(transformedSchema),
+ valid = validate(example);
+
+ expect(value).to.have.property('body').that.is.a('string');
+ expect(valid, `Validation failed for key: ${key} with errors: ${JSON.stringify(validate.errors)}`).to.be.true;
+ }
+ });
+ });
+
+ it('should resolve nested array and object schema types correctly in extractedTypes', function(done) {
+ const example = {
+ name: 'Buddy',
+ pet: {
+ id: 123,
+ name: 'Charlie',
+ address: {
+ addressCode: {
+ code: 'A123'
+ },
+ city: 'New York'
+ }
+ }
+ },
+ openapi = fs.readFileSync(readOnlyNestedSpec, 'utf8'),
+ options = { schemaFaker: true, exampleParametersResolution: 'schema' };
+
+ Converter.convertV2WithTypes({ type: 'string', data: openapi }, options, (err, conversionResult) => {
+ expect(err).to.be.null;
+ expect(conversionResult.extractedTypes).to.be.an('object').that.is.not.empty;
+
+ const element = Object.values(conversionResult.extractedTypes)[0];
+ const { response } = element;
+ const [key, value] = Object.entries(response)[0];
+ expect(value).to.have.property('body').that.is.a('string');
+
+ const schema = JSON.parse(value.body),
+ transformedSchema = transformSchema(schema),
+ validate = ajv.compile(transformedSchema),
+ valid = validate(example);
+ expect(valid, `Validation failed for key: ${key} with errors: ${JSON.stringify(validate.errors)}`).to.be.true;
+ done();
+ }
+ );
+ });
+
+ it('should resolve extractedTypes into correct schema structure', function(done) {
+ const expectedExtractedTypes = {
+ 'get/pets': {
+ 'request': {
+ 'headers': '[\n {\n "keyName": "variable",\n "properties": {\n "type": "array",\n "required": false,\n "deprecated": false\n }\n }\n]',
+ 'pathParam': '[]',
+ 'queryParam': '[\n {\n "keyName": "limit",\n "properties": {\n "type": "string",\n "default": "",\n "required": false,\n "deprecated": false\n }\n },\n {\n "keyName": "variable2",\n "properties": {\n "type": "array",\n "required": false,\n "deprecated": false\n }\n },\n {\n "keyName": "variable3",\n "properties": {\n "type": "array",\n "required": false,\n "deprecated": false\n }\n }\n]'
+ },
+ 'response': {
+ '200': {
+ 'body': '{\n "type": "array",\n "items": {\n "type": "object",\n "properties": {\n "id": {\n "type": "integer",\n "format": "int64"\n },\n "name": {\n "type": "string"\n },\n "tag": {\n "type": "string"\n }\n },\n "required": [\n "id",\n "name"\n ]\n }\n}',
+ 'headers': '[\n {\n "keyName": "x-next",\n "properties": {\n "type": "string",\n "default": "",\n "required": false,\n "deprecated": false\n }\n }\n]'
+ },
+ '500': {
+ 'body': '{\n "type": "object",\n "properties": {\n "code": {\n "type": "integer"\n },\n "message": {\n "type": "string"\n }\n },\n "required": [\n "code",\n "message"\n ]\n}',
+ 'headers': '[]'
+ }
+ }
+ },
+ 'post/pets': {
+ 'request': {
+ 'headers': '[]',
+ 'pathParam': '[]',
+ 'queryParam': '[\n {\n "keyName": "limit",\n "properties": {\n "type": "string",\n "default": "",\n "required": false,\n "deprecated": false\n }\n },\n {\n "keyName": "variable3",\n "properties": {\n "type": "array",\n "required": false,\n "deprecated": false\n }\n }\n]'
+ },
+ 'response': {
+ '201': {
+ 'headers': '[]'
+ },
+ '500': {
+ 'body': '{\n "type": "object",\n "properties": {\n "code": {\n "type": "integer"\n },\n "message": {\n "type": "string"\n }\n },\n "required": [\n "code",\n "message"\n ]\n}',
+ 'headers': '[]'
+ }
+ }
+ },
+ 'get/pet/{petId}': {
+ 'request': {
+ 'headers': '[]',
+ 'pathParam': '[\n {\n "keyName": "petId",\n "properties": {\n "type": "string",\n "default": "",\n "required": true,\n "deprecated": false\n }\n }\n]',
+ 'queryParam': '[]'
+ },
+ 'response': {
+ '200': {
+ 'body': '{\n "type": "array",\n "items": {\n "type": "object",\n "properties": {\n "id": {\n "type": "integer",\n "format": "int64"\n },\n "name": {\n "type": "string"\n },\n "tag": {\n "type": "string"\n }\n },\n "required": [\n "id",\n "name"\n ]\n }\n}',
+ 'headers': '[]'
+ },
+ '500': {
+ 'body': '{\n "type": "object",\n "properties": {\n "code": {\n "type": "integer"\n },\n "message": {\n "type": "string"\n }\n },\n "required": [\n "code",\n "message"\n ]\n}',
+ 'headers': '[]'
+ }
+ }
+ },
+ 'post/pet/{petId}': {
+ 'request': {
+ 'headers': '[]',
+ 'pathParam': '[\n {\n "keyName": "petId",\n "properties": {\n "type": "string",\n "default": "",\n "required": true,\n "deprecated": false\n }\n }\n]',
+ 'queryParam': '[]'
+ },
+ 'response': {
+ '200': {
+ 'body': '{\n "type": "array",\n "items": {\n "type": "object",\n "properties": {\n "id": {\n "type": "integer",\n "format": "int64"\n },\n "name": {\n "type": "string"\n },\n "tag": {\n "type": "string"\n }\n },\n "required": [\n "id",\n "name"\n ]\n }\n}',
+ 'headers': '[]'
+ },
+ '500': {
+ 'body': '{\n "type": "object",\n "properties": {\n "code": {\n "type": "integer"\n },\n "message": {\n "type": "string"\n }\n },\n "required": [\n "code",\n "message"\n ]\n}',
+ 'headers': '[]'
+ }
+ }
+ }
+ },
+ openapi = fs.readFileSync(testSpec1, 'utf8'),
+ options = { schemaFaker: true, exampleParametersResolution: 'schema' };
+
+ Converter.convertV2WithTypes({ type: 'string', data: openapi }, options, (err, conversionResult) => {
+ expect(err).to.be.null;
+ expect(conversionResult.extractedTypes).to.be.an('object').that.is.not.empty;
+
+ const extractedTypes = conversionResult.extractedTypes;
+ expect(JSON.parse(JSON.stringify(extractedTypes))).to.deep.equal(
+ JSON.parse(JSON.stringify(expectedExtractedTypes)));
+ done();
+ }
+ );
+ });
+
+});