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 77ba2f54..f60fe244 100644 --- a/libV2/schemaUtils.js +++ b/libV2/schemaUtils.js @@ -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..d92fd249 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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..781bfa5b 100644 --- a/package.json +++ b/package.json @@ -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(); + } + ); + }); + +});