diff --git a/.default-eslintrc.yaml b/.default-eslintrc.yaml index 0109651..5f6fd3e 100644 --- a/.default-eslintrc.yaml +++ b/.default-eslintrc.yaml @@ -15,6 +15,7 @@ parserOptions: ecmaVersion: 2018 sourceType: module warnOnUnsupportedTypeScriptVersion : false + project: "./tsconfig.json" rules: "@typescript-eslint/no-this-alias" : 'off' "@typescript-eslint/interface-name-prefix": 'off' @@ -26,14 +27,10 @@ rules: "@typescript-eslint/no-unused-vars": 'off' "@typescript-eslint/no-parameter-properties": 'off' "@typescript-eslint/no-angle-bracket-type-assertion" : 'off' + "@typescript-eslint/no-use-before-define": "off" "require-atomic-updates" : 'off' - '@typescript-eslint/consistent-type-assertions' : - - error - - assertionStyle: 'angle-bracket' + '@typescript-eslint/no-floating-promises': error - "@typescript-eslint/array-type": - - error - - default: generic indent: - warn - 2 @@ -43,12 +40,6 @@ rules: - 2 no-undef: 'off' no-unused-vars: 'off' - linebreak-style: - - 'error' - - unix - quotes: - - error - - single semi: - error - always diff --git a/oai2-to-oai3/changelog.md b/oai2-to-oai3/changelog.md index b36e569..384b1d8 100644 --- a/oai2-to-oai3/changelog.md +++ b/oai2-to-oai3/changelog.md @@ -1,4 +1,7 @@ # Change Log - @azure-tools/oai2-to-oai3 +# 4.1.0 +- **fix**: Issue with cross-file referneces of body parameters [PR 131](https://github.com/Azure/perks/pull/131) + # 3/28/2019 - republishing to force change in package dependency chain. diff --git a/oai2-to-oai3/package.json b/oai2-to-oai3/package.json index 1fe651b..8d3bb4a 100644 --- a/oai2-to-oai3/package.json +++ b/oai2-to-oai3/package.json @@ -1,10 +1,10 @@ { "name": "@azure-tools/oai2-to-oai3", - "version": "4.0.0", + "version": "4.1.0", "patchOffset": 100, "description": "OpenAPI2 to OpenAPI3 conversion library that maintains souremaps for use with AutoRest", - "main": "./dist/main.js", - "typings": "./dist/main.d.ts", + "main": "./dist/src/index.js", + "typings": "./dist/src/index.d.ts", "engines": { "node": ">=10.12.0" }, @@ -57,4 +57,4 @@ "@azure-tools/openapi": "~3.0.0", "source-map": "0.5.6" } -} \ No newline at end of file +} diff --git a/oai2-to-oai3/main.ts b/oai2-to-oai3/src/converter.ts similarity index 80% rename from oai2-to-oai3/main.ts rename to oai2-to-oai3/src/converter.ts index d102785..af63f35 100644 --- a/oai2-to-oai3/main.ts +++ b/oai2-to-oai3/src/converter.ts @@ -1,5 +1,7 @@ -import { createGraphProxy, JsonPointer, Node, visit, FastStringify, parsePointer, get } from '@azure-tools/datastore'; +import { createGraphProxy, JsonPointer, Node, visit, get } from '@azure-tools/datastore'; import { Mapping } from 'source-map'; +import { cleanElementName, convertOai2RefToOai3, parseOai2Ref } from './refs-utils'; +import { ResolveReferenceFn } from './runner'; import { statusCodes } from './status-codes'; // NOTE: after testing references should be changed to OpenAPI 3.x.x references @@ -8,11 +10,14 @@ export class Oai2ToOai3 { public generated: any; public mappings = new Array(); - constructor(protected originalFilename: string, protected original: any) { + private resolveReference: ResolveReferenceFn; + + constructor(protected originalFilename: string, protected original: any, resolveReference?: ResolveReferenceFn) { this.generated = createGraphProxy(this.originalFilename, '', this.mappings); + this.resolveReference = resolveReference ?? (() => Promise.resolve(undefined)); } - convert() { + async convert() { // process servers if (this.original['x-ms-parameterized-host']) { const xMsPHost: any = this.original['x-ms-parameterized-host']; @@ -42,7 +47,7 @@ export class Oai2ToOai3 { if (!originalParameter['x-ms-parameter-location']) { originalParameter['x-ms-parameter-location'] = 'client'; } - originalParameter['x-ms-original'] = { $ref: rp.replace('#/parameters/', '#/components/parameters/') }; + originalParameter['x-ms-original'] = { $ref: await this.convertReferenceToOai3(rp)}; } else { originalParameter = xMsPHost.parameters[msp]; } @@ -91,13 +96,12 @@ export class Oai2ToOai3 { const server: any = {}; server.url = (s ? s + ':' : '') + '//' + this.original.host + (this.original.basePath ? this.original.basePath : '/'); - /* eslint-disable */ extractServerParameters(server); this.generated.servers.__push__({ value: server, pointer }); } } else if (this.original.basePath) { - let server: any = {}; + const server: any = {}; server.url = this.original.basePath; extractServerParameters(server); if (this.generated.servers === undefined) { @@ -136,7 +140,7 @@ export class Oai2ToOai3 { break; case 'info': this.generated.info = this.newObject(pointer); - this.visitInfo(children); + await this.visitInfo(children); break; case 'x-ms-paths': case 'paths': { @@ -145,7 +149,7 @@ export class Oai2ToOai3 { if (!this.generated[newKey]) { this.generated[newKey] = this.newObject(pointer); } - this.visitPaths(this.generated[newKey], children, globalConsumes, globalProduces); + await this.visitPaths(this.generated[newKey], children, globalConsumes, globalProduces); } break; case 'host': @@ -159,24 +163,24 @@ export class Oai2ToOai3 { // PENDING break; case 'definitions': - this.visitDefinitions(children); + await this.visitDefinitions(children); break; case 'parameters': - this.visitParameterDefinitions(children); + await this.visitParameterDefinitions(children); break; case 'responses': if (!this.generated.components) { this.generated.components = this.newObject(pointer); } this.generated.components.responses = this.newObject(pointer); - this.visitResponsesDefinitions(children, globalProduces); + await this.visitResponsesDefinitions(children, globalProduces); break; case 'securityDefinitions': if (!this.generated.components) { this.generated.components = this.newObject(pointer); } this.generated.components.securitySchemes = this.newObject(pointer); - this.visitSecurityDefinitions(children); + await this.visitSecurityDefinitions(children); break; // no changes to security from OA2 to OA3 case 'security': @@ -184,25 +188,23 @@ export class Oai2ToOai3 { break; case 'tags': this.generated.tags = this.newArray(pointer); - this.visitTags(children); + await this.visitTags(children); break; case 'externalDocs': this.visitExternalDocs(this.generated, key, value, pointer); break; default: // handle stuff liks x-* and things not recognized - this.visitExtensions(this.generated, key, value, pointer); + await this.visitExtensions(this.generated, key, value, pointer); break; } } - return this.generated; } - visitParameterDefinitions(parameters: Iterable) { + async visitParameterDefinitions(parameters: Iterable) { for (const { key, value, pointer, childIterator } of parameters) { if (value.in !== 'formData' && value.in !== 'body' && value.type !== 'file') { - if (this.generated.components === undefined) { this.generated.components = this.newObject(pointer); } @@ -211,20 +213,18 @@ export class Oai2ToOai3 { this.generated.components.parameters = this.newObject(pointer); } - const cleanParamName = key.replace(/\$|\[|\]/g, '_'); + const cleanParamName = cleanElementName(key); this.generated.components.parameters[cleanParamName] = this.newObject(pointer); - this.visitParameter(this.generated.components.parameters[cleanParamName], value, pointer, childIterator); - } + await this.visitParameter(this.generated.components.parameters[cleanParamName], value, pointer, childIterator); + } } } - visitParameter(parameterTarget: any, parameterValue: any, pointer: string, parameterItemMembers: () => Iterable) { - + async visitParameter(parameterTarget: any, parameterValue: any, pointer: string, parameterItemMembers: () => Iterable) { if (parameterValue.$ref !== undefined) { - const cleanReferenceValue = parameterValue.$ref.replace(/\$|\[|\]/g, '_'); - parameterTarget.$ref = { value: cleanReferenceValue.replace('#/parameters/', '#/components/parameters/'), pointer }; + parameterTarget.$ref = { value: await this.convertReferenceToOai3(parameterValue.$ref), pointer }; } else { const parameterUnchangedProperties = [ @@ -315,7 +315,7 @@ export class Oai2ToOai3 { if (parameterTarget.schema.items === undefined) { parameterTarget.schema.items = this.newObject(jsonPointer); } - this.visitSchema(parameterTarget.schema.items, parameterValue.items, childIterator); + await this.visitSchema(parameterTarget.schema.items, parameterValue.items, childIterator); } else if (schemaKeys.indexOf(key) !== -1) { parameterTarget.schema[key] = { value: parameterValue[key], pointer, recurse: true }; } @@ -330,14 +330,14 @@ export class Oai2ToOai3 { parameterTarget.schema.items = this.newObject(pointer); for (const { key, childIterator } of parameterItemMembers()) { if (key === 'items') { - this.visitSchema(parameterTarget.schema.items, parameterValue.items, childIterator); + await this.visitSchema(parameterTarget.schema.items, parameterValue.items, childIterator); } } } } } - visitInfo(info: Iterable) { + async visitInfo(info: Iterable) { for (const { value, key, pointer, children } of info) { switch (key) { case 'title': @@ -349,13 +349,13 @@ export class Oai2ToOai3 { this.generated.info[key] = { value, pointer }; break; default: - this.visitExtensions(this.generated.info, key, value, pointer); + await this.visitExtensions(this.generated.info, key, value, pointer); break; } } } - visitSecurityDefinitions(securityDefinitions: Iterable) { + async visitSecurityDefinitions(securityDefinitions: Iterable) { for (const { key: schemeName, value: v, pointer: jsonPointer, children: securityDefinitionsItemMembers } of securityDefinitions) { this.generated.components.securitySchemes[schemeName] = this.newObject(jsonPointer); const securityScheme = this.generated.components.securitySchemes[schemeName]; @@ -370,7 +370,7 @@ export class Oai2ToOai3 { securityScheme[key] = { value, pointer }; break; default: - this.visitExtensions(securityScheme, key, value, pointer); + await this.visitExtensions(securityScheme, key, value, pointer); break; } } @@ -386,7 +386,7 @@ export class Oai2ToOai3 { securityScheme.scheme = { value: 'basic', pointer }; break; default: - this.visitExtensions(securityScheme, key, value, pointer); + await this.visitExtensions(securityScheme, key, value, pointer); break; } } @@ -413,7 +413,6 @@ export class Oai2ToOai3 { securityScheme.flows[flowName] = this.newObject(jsonPointer); let authorizationUrl; let tokenUrl; - let scopes; if (v.authorizationUrl) { authorizationUrl = v.authorizationUrl.split('?')[0].trim() || '/'; @@ -425,7 +424,7 @@ export class Oai2ToOai3 { securityScheme.flows[flowName].tokenUrl = { value: tokenUrl, pointer: jsonPointer }; } - scopes = v.scopes || {}; + const scopes = v.scopes || {}; securityScheme.flows[flowName].scopes = { value: scopes, pointer: jsonPointer }; } break; @@ -433,7 +432,7 @@ export class Oai2ToOai3 { } } - visitDefinitions(definitions: Iterable) { + async visitDefinitions(definitions: Iterable) { for (const { key: schemaName, value: schemaValue, pointer: jsonPointer, childIterator: definitionsItemMembers } of definitions) { if (this.generated.components === undefined) { this.generated.components = this.newObject(jsonPointer); @@ -446,29 +445,36 @@ export class Oai2ToOai3 { const cleanSchemaName = schemaName.replace(/\[|\]/g, '_'); this.generated.components.schemas[cleanSchemaName] = this.newObject(jsonPointer); const schemaItem = this.generated.components.schemas[cleanSchemaName]; - this.visitSchema(schemaItem, schemaValue, definitionsItemMembers); + await this.visitSchema(schemaItem, schemaValue, definitionsItemMembers); } } - visitProperties(target: any, propertiesItemMembers: () => Iterable) { + async visitProperties(target: any, propertiesItemMembers: () => Iterable) { for (const { key, value, pointer, childIterator } of propertiesItemMembers()) { target[key] = this.newObject(pointer); - this.visitSchema(target[key], value, childIterator); + await this.visitSchema(target[key], value, childIterator); } } - visitResponsesDefinitions(responses: Iterable, globalProduces: Array) { + async visitResponsesDefinitions(responses: Iterable, globalProduces: Array) { for (const { key, pointer, value, childIterator } of responses) { this.generated.components.responses[key] = this.newObject(pointer); - this.visitResponse(this.generated.components.responses[key], value, key, childIterator, pointer, globalProduces); + await this.visitResponse( + this.generated.components.responses[key], + value, + key, + childIterator, + pointer, + globalProduces, + ); } } - visitSchema(target: any, schemaValue: any, schemaItemMemebers: () => Iterable) { + async visitSchema(target: any, schemaValue: any, schemaItemMemebers: () => Iterable) { for (const { key, value, pointer, childIterator } of schemaItemMemebers()) { switch (key) { case '$ref': - target[key] = { value: this.getNewSchemaReference(value), pointer }; + target.$ref = { value: await this.convertReferenceToOai3(value), pointer }; break; case 'additionalProperties': if (typeof (value) === 'boolean') { @@ -477,7 +483,7 @@ export class Oai2ToOai3 { } // false is assumed anyway in autorest. } else { target[key] = this.newObject(pointer); - this.visitSchema(target[key], value, childIterator); + await this.visitSchema(target[key], value, childIterator); } break; case 'required': @@ -503,15 +509,15 @@ export class Oai2ToOai3 { break; case 'allOf': target.allOf = this.newArray(pointer); - this.visitAllOf(target.allOf, childIterator); + await this.visitAllOf(target.allOf, childIterator); break; case 'items': target[key] = this.newObject(pointer); - this.visitSchema(target[key], value, childIterator); + await this.visitSchema(target[key], value, childIterator); break; case 'properties': target[key] = this.newObject(pointer); - this.visitProperties(target[key], childIterator); + await this.visitProperties(target[key], childIterator); break; case 'type': case 'format': @@ -543,16 +549,16 @@ export class Oai2ToOai3 { } break; default: - this.visitExtensions(target, key, value, pointer); + await this.visitExtensions(target, key, value, pointer); break; } } } - visitAllOf(target: any, allOfMembers: () => Iterable) { + async visitAllOf(target: any, allOfMembers: () => Iterable) { for (const { key: index, value, pointer, childIterator } of allOfMembers()) { target.__push__(this.newObject(pointer)); - this.visitSchema(target[index], value, childIterator); + await this.visitSchema(target[index], value, childIterator); } } @@ -576,13 +582,13 @@ export class Oai2ToOai3 { target[key] = { value, pointer, recurse: true }; } - visitTags(tags: Iterable) { + async visitTags(tags: Iterable) { for (const { key: index, pointer, children: tagItemMembers } of tags) { - this.visitTag(parseInt(index), pointer, tagItemMembers); + await this.visitTag(parseInt(index), pointer, tagItemMembers); } } - visitTag(index: number, jsonPointer: JsonPointer, tagItemMembers: Iterable) { + async visitTag(index: number, jsonPointer: JsonPointer, tagItemMembers: Iterable) { this.generated.tags.__push__(this.newObject(jsonPointer)); for (const { key, pointer, value } of tagItemMembers) { @@ -595,24 +601,20 @@ export class Oai2ToOai3 { this.visitExternalDocs(this.generated.tags[index], key, value, pointer); break; default: - this.visitExtensions(this.generated.tags[index], key, value, pointer); + await this.visitExtensions(this.generated.tags[index], key, value, pointer); break; } } } - // NOTE: For the previous converter external references are not - // converted, but internal references are converted. - // Decided that updating all references makes more sense. - getNewSchemaReference(oldReference: string) { - const cleanOldReference = oldReference.replace(/\$|\[|\]/g, '_'); - return cleanOldReference.replace('#/definitions/', '#/components/schemas/'); + private async convertReferenceToOai3(oldReference: string): Promise { + return convertOai2RefToOai3(oldReference); } - visitExtensions(target: any, key: string, value: any, pointer: string) { + async visitExtensions(target: any, key: string, value: any, pointer: string) { switch (key) { case 'x-ms-odata': - target[key] = { value: this.getNewSchemaReference(value), pointer }; + target[key] = { value: await this.convertReferenceToOai3(value), pointer }; break; default: target[key] = { value, pointer, recurse: true }; @@ -629,7 +631,7 @@ export class Oai2ToOai3 { } newObject(pointer: JsonPointer) { - return { value: createGraphProxy(this.originalFilename, pointer, this.mappings), pointer }; + return { value: createGraphProxy(this.originalFilename, pointer, this.mappings), pointer }; } visitUnspecified(nodes: Iterable) { @@ -638,13 +640,13 @@ export class Oai2ToOai3 { } } - visitPaths(target: any, paths: Iterable, globalConsumes: Array, globalProduces: Array) { + async visitPaths(target: any, paths: Iterable, globalConsumes: Array, globalProduces: Array) { for (const { key: uri, pointer, children: pathItemMembers } of paths) { - this.visitPath(target, uri, pointer, pathItemMembers, globalConsumes, globalProduces); + await this.visitPath(target, uri, pointer, pathItemMembers, globalConsumes, globalProduces); } } - visitPath(target: any, uri: string, jsonPointer: JsonPointer, pathItemMembers: Iterable, globalConsumes: Array, globalProduces: Array) { + async visitPath(target: any, uri: string, jsonPointer: JsonPointer, pathItemMembers: Iterable, globalConsumes: Array, globalProduces: Array) { target[uri] = this.newObject(jsonPointer); const pathItem = target[uri]; for (const { value, key, pointer, children: pathItemFieldMembers } of pathItemMembers) { @@ -663,24 +665,24 @@ export class Oai2ToOai3 { case 'head': case 'patch': case 'x-trace': - this.visitOperation(pathItem, key, pointer, pathItemFieldMembers, value, globalConsumes, globalProduces); + await this.visitOperation(pathItem, key, pointer, pathItemFieldMembers, value, globalConsumes, globalProduces); break; case 'parameters': pathItem.parameters = this.newArray(pointer); - this.visitPathParameters(pathItem.parameters, pathItemFieldMembers); + await this.visitPathParameters(pathItem.parameters, pathItemFieldMembers); break; } } } - visitPathParameters(target: any, parameters: Iterable) { + async visitPathParameters(target: any, parameters: Iterable) { for (const { key, value, pointer, childIterator } of parameters) { target.__push__(this.newObject(pointer)); - this.visitParameter(target[target.length - 1], value, pointer, childIterator); + await this.visitParameter(target[target.length - 1], value, pointer, childIterator); } } - visitOperation(pathItem: any, httpMethod: string, jsonPointer: JsonPointer, operationItemMembers: Iterable, operationValue: any, globalConsumes: Array, globalProduces: Array) { + async visitOperation(pathItem: any, httpMethod: string, jsonPointer: JsonPointer, operationItemMembers: Iterable, operationValue: any, globalConsumes: Array, globalProduces: Array) { // trace was not supported on OpenAPI 2.0, it was an extension httpMethod = (httpMethod !== 'x-trace') ? httpMethod : 'trace'; @@ -720,14 +722,14 @@ export class Oai2ToOai3 { // handled beforehand for parameters break; case 'parameters': - this.visitParameters(operation, operationFieldItemMembers, consumes, pointer); + await this.visitParameters(operation, operationFieldItemMembers, consumes, pointer); break; case 'produces': // handled beforehand for responses break; case 'responses': operation.responses = this.newObject(pointer); - this.visitResponses(operation.responses, operationFieldItemMembers, produces); + await this.visitResponses(operation.responses, operationFieldItemMembers, produces); break; case 'schemes': break; @@ -735,33 +737,55 @@ export class Oai2ToOai3 { operation.security = { value, pointer, recurse: true }; break; default: - this.visitExtensions(operation, key, value, pointer); + await this.visitExtensions(operation, key, value, pointer); break; } } } - visitParameters(targetOperation: any, parametersFieldItemMembers: any, consumes: any, pointer: string) { + async visitParameters(targetOperation: any, parametersFieldItemMembers: any, consumes: any, pointer: string) { const requestBodyTracker = { xmsname: undefined, name: undefined, description: undefined, index: -1, keepTrackingIndex: true, wasSpecialParameterFound: false, wasParamRequired: false }; + + for (let { pointer, value, childIterator } of parametersFieldItemMembers) { + if (value.$ref) { + const parsedRef = parseOai2Ref(value.$ref); + if(parsedRef === undefined) { + throw new Error(`Reference ${value.$ref} is not a valid ref(at ${pointer})`); + } + const parameterName = parsedRef.componentName; + if(parsedRef.basePath === "/parameters/") { + if (parsedRef.file == "" || parsedRef.file === this.originalFilename) { + const dereferencedParameter = get(this.original, parsedRef.path); + + if ( + dereferencedParameter.in === "body" || + dereferencedParameter.type === "file" || + dereferencedParameter.in === "formData" + ) { + childIterator = () => visit(dereferencedParameter, [parameterName]); + value = dereferencedParameter; + pointer = parsedRef.path; + } + } else { + const dereferencedParameter = await this.resolveReference(parsedRef.file, parsedRef.path); + if (!dereferencedParameter) { + throw new Error(`Cannot find reference ${value.$ref}`); + } - if (value.$ref && (value.$ref.match(/^#\/parameters\//g) || value.$ref.startsWith(`${this.originalFilename}#/parameters/`))) { - // local reference. it's possible to look it up. - const referenceParts = value.$ref.split('/'); - const paramName = referenceParts.pop(); - const componentType = referenceParts.pop(); - const referencePointer = `/${componentType}/${paramName}`; - const dereferencedParameter = get(this.original, referencePointer); - if (dereferencedParameter.in === 'body' || dereferencedParameter.type === 'file' || dereferencedParameter.in === 'formData') { - const parameterName = referencePointer.replace('/parameters/', ''); - const dereferencedParameters = get(this.original, '/parameters'); - for (const { key, childIterator: newChildIterator } of visit(dereferencedParameters)) { - if (key === parameterName) { - childIterator = newChildIterator; + if ( + dereferencedParameter.in === "body" || + dereferencedParameter.type === "file" || + dereferencedParameter.in === "formData" + ) { + childIterator = () => visit(dereferencedParameter, [parameterName]); + value = dereferencedParameter; + pointer = parsedRef.path; } } - value = dereferencedParameter; - pointer = referencePointer; + } else { + // TODO: Throw exception + console.error("### CAN'T RESOLVE $ref", value.$ref); } } @@ -797,7 +821,7 @@ export class Oai2ToOai3 { requestBodyTracker.index += 1; } - this.visitOperationParameter(targetOperation, value, pointer, childIterator, consumes); + await this.visitOperationParameter(targetOperation, value, pointer, childIterator, consumes); } if (targetOperation.requestBody !== undefined) { @@ -830,8 +854,7 @@ export class Oai2ToOai3 { } } - visitOperationParameter(targetOperation: any, parameterValue: any, pointer: string, parameterItemMembers: () => Iterable, consumes: Array) { - + async visitOperationParameter(targetOperation: any, parameterValue: any, pointer: string, parameterItemMembers: () => Iterable, consumes: Array) { if (parameterValue.in === 'formData' || parameterValue.in === 'body' || parameterValue.type === 'file') { if (targetOperation.requestBody === undefined) { @@ -876,7 +899,7 @@ export class Oai2ToOai3 { if (parameterValue.schema !== undefined) { for (const { key, value, childIterator } of parameterItemMembers()) { if (key === 'schema') { - this.visitSchema(targetOperation.requestBody.content[contentType].schema, value, childIterator); + await this.visitSchema(targetOperation.requestBody.content[contentType].schema, value, childIterator); } } } else { @@ -962,7 +985,7 @@ export class Oai2ToOai3 { consumesTempArray.push('application/json'); } - for (let mimetype of consumesTempArray) { + for (const mimetype of consumesTempArray) { if (targetOperation.requestBody.content[mimetype] === undefined) { targetOperation.requestBody.content[mimetype] = this.newObject(pointer); } @@ -974,7 +997,7 @@ export class Oai2ToOai3 { if (parameterValue.schema !== undefined) { for (const { key, value, childIterator } of parameterItemMembers()) { if (key === 'schema') { - this.visitSchema(targetOperation.requestBody.content[mimetype].schema, value, childIterator); + await this.visitSchema(targetOperation.requestBody.content[mimetype].schema, value, childIterator); } } } else { @@ -999,32 +1022,30 @@ export class Oai2ToOai3 { targetOperation.parameters.__push__(this.newObject(pointer)); const parameter = targetOperation.parameters[targetOperation.parameters.length - 1]; - this.visitParameter(parameter, parameterValue, pointer, parameterItemMembers); + await this.visitParameter(parameter, parameterValue, pointer, parameterItemMembers); } } - visitResponses(target: any, responsesItemMembers: Iterable, produces: Array) { + async visitResponses(target: any, responsesItemMembers: Iterable, produces: Array) { for (const { key, value, pointer, childIterator } of responsesItemMembers) { target[key] = this.newObject(pointer); if (value.$ref) { - let newReferenceValue = value.$ref.replace('#/responses/', '#/components/responses/'); - - target[key].$ref = { value: newReferenceValue, pointer }; + target[key].$ref = { value: await this.convertReferenceToOai3(value.$ref), pointer }; } else if (key.startsWith('x-')) { - this.visitExtensions(target[key], key, value, pointer); + await this.visitExtensions(target[key], key, value, pointer); } else { - this.visitResponse(target[key], value, key, childIterator, pointer, produces); + await this.visitResponse(target[key], value, key, childIterator, pointer, produces); } } } - visitResponse(responseTarget: any, responseValue: any, responseName: string, responsesFieldMembers: () => Iterable, jsonPointer: string, produces: Array) { + async visitResponse(responseTarget: any, responseValue: any, responseName: string, responsesFieldMembers: () => Iterable, jsonPointer: string, produces: Array) { // NOTE: The previous converter patches the description of the response because // every response should have a description. // So, to match previous behavior we do too. if (responseValue.description === undefined || responseValue.description === '') { - let sc = statusCodes.find((e) => { + const sc = statusCodes.find((e) => { return e.code === responseName; }); @@ -1035,16 +1056,16 @@ export class Oai2ToOai3 { if (responseValue.schema) { responseTarget.content = this.newObject(jsonPointer); - for (let mimetype of produces) { + for (const mimetype of produces) { responseTarget.content[mimetype] = this.newObject(jsonPointer); responseTarget.content[mimetype].schema = this.newObject(jsonPointer); for (const { key, value, childIterator } of responsesFieldMembers()) { if (key === 'schema') { - this.visitSchema(responseTarget.content[mimetype].schema, value, childIterator); + await this.visitSchema(responseTarget.content[mimetype].schema, value, childIterator); } } if (responseValue.examples && responseValue.examples[mimetype]) { - let example: any = {}; + const example: any = {}; example.value = responseValue.examples[mimetype]; responseTarget.content[mimetype].examples = this.newObject(jsonPointer); responseTarget.content[mimetype].examples.response = { value: example, pointer: jsonPointer }; @@ -1053,7 +1074,7 @@ export class Oai2ToOai3 { } // examples outside produces - for (let mimetype in responseValue.examples) { + for (const mimetype in responseValue.examples) { if (responseTarget.content === undefined) { responseTarget.content = this.newObject(jsonPointer); } @@ -1071,9 +1092,9 @@ export class Oai2ToOai3 { if (responseValue.headers) { responseTarget.headers = this.newObject(jsonPointer); - for (let h in responseValue.headers) { + for (const h in responseValue.headers) { responseTarget.headers[h] = this.newObject(jsonPointer); - this.visitHeader(responseTarget.headers[h], responseValue.headers[h], jsonPointer); + await this.visitHeader(responseTarget.headers[h], responseValue.headers[h], jsonPointer); } } @@ -1114,11 +1135,9 @@ export class Oai2ToOai3 { 'uniqueItems' ]; - visitHeader(targetHeader: any, headerValue: any, jsonPointer: string) { + async visitHeader(targetHeader: any, headerValue: any, jsonPointer: string) { if (headerValue.$ref) { - // GS01/CRITICAL-TO-DO-NELSON -- should that be /components/headers ???? - const newReferenceValue = `#/components/responses/${headerValue.schema.$ref.replace('#/responses/', '')}`; - targetHeader.$ref = { value: newReferenceValue, pointer: jsonPointer }; + targetHeader.$ref = { value: this.convertReferenceToOai3(headerValue.schema.$ref), pointer: jsonPointer }; } else { if (headerValue.type && headerValue.schema === undefined) { targetHeader.schema = this.newObject(jsonPointer); @@ -1130,7 +1149,7 @@ export class Oai2ToOai3 { for (const { key, childIterator } of visit(headerValue)) { if (key === 'schema') { - this.visitSchema(targetHeader.schema.items, headerValue.items, childIterator); + await this.visitSchema(targetHeader.schema.items, headerValue.items, childIterator); } else if (this.parameterTypeProperties.includes(key) || this.arrayProperties.includes(key)) { targetHeader.schema[key] = { value: headerValue[key], pointer: jsonPointer, recurse: true }; } else if (key.startsWith('x-') && targetHeader[key] === undefined) { diff --git a/oai2-to-oai3/src/index.ts b/oai2-to-oai3/src/index.ts new file mode 100644 index 0000000..1ed33ab --- /dev/null +++ b/oai2-to-oai3/src/index.ts @@ -0,0 +1,2 @@ +export * from "./runner"; +export * from "./converter"; diff --git a/oai2-to-oai3/src/refs-utils.ts b/oai2-to-oai3/src/refs-utils.ts new file mode 100644 index 0000000..a816175 --- /dev/null +++ b/oai2-to-oai3/src/refs-utils.ts @@ -0,0 +1,80 @@ +/** + * Clean a component name to use in OpenAPI 3.0. + * @param name OpenApi2.0 component name to clean. + */ +export const cleanElementName = (name: string) => name.replace(/\$|\[|\]/g, "_"); + +/** + * Convert a OpenAPI 2.0 $ref to its OpenAPI3.0 version. + * @param oai2Ref OpenAPI 2.0 reference. + * @param resolveReference Optional resolver for references pointing to a different file. + * @param currentFile Current file to use with `resolveReference` + */ +export const convertOai2RefToOai3 = async (oai2Ref: string): Promise => { + const [file, path] = oai2Ref.split("#"); + return `${file}#${convertOai2PathToOai3(path)}`; +}; + +const oai2PathMapping = { + "/definitions/": "/components/schemas/", + "/parameters/": "/components/parameters/", + "/responses/": "/components/responses/", +}; + +export const convertOai2PathToOai3 = (path: string) => { + const parsed = parseOai2Path(path); + if (parsed === undefined) { + throw new Error(`Cannot parse ref path ${path} it is not a supported ref pattern.`); + } + + return `${oai2PathMapping[parsed.basePath]}${cleanElementName(parsed.componentName)}`; +}; + +export interface Oai2ParsedPath { + basePath: keyof typeof oai2PathMapping; + componentName: string; +} + +/** + * Extract the component name and base path of a reference path. + * @example + * parseOai2Path("/parameters/Foo") -> {basePath: "/parameters/", componentName: "Foo"} + * parseOai2Path("/definitions/Foo") -> {basePath: "/definitions/", componentName: "Foo"} + * parseOai2Path("/unknown/Foo") -> undefined + */ +export const parseOai2Path = (path: string): Oai2ParsedPath | undefined => { + for (const oai2Path of Object.keys(oai2PathMapping)) { + if (path.startsWith(oai2Path)) { + return { + basePath: oai2Path as keyof typeof oai2PathMapping, + componentName: path.slice(oai2Path.length), + }; + } + } + return undefined; +}; + +export interface Oai2ParsedRef extends Oai2ParsedPath { + file: string; + path: string; +} + +/** + * Parse a OpenAPI2.0 json ref. + * @example + * parseOai2Path("#/parameters/Foo") -> {file: "", path: "/parameters/Foo", basePath: "/parameters/", componentName: "Foo"} + * parseOai2Path("bar.json#/definitions/Foo") -> {file: "bar.json", path: "/definitions/Foo", basePath: "/definitions/", componentName: "Foo"} + * parseOai2Path("other.json#/unknown/Foo") -> undefined + */ +export const parseOai2Ref = (oai2Ref: string): Oai2ParsedRef | undefined => { + const [file, path] = oai2Ref.split("#"); + const parsedPath = parseOai2Path(path); + if (parsedPath === undefined) { + return undefined; + } + return { + file, + path, + ...parsedPath, + }; +}; diff --git a/oai2-to-oai3/src/runner/index.ts b/oai2-to-oai3/src/runner/index.ts new file mode 100644 index 0000000..8c9692a --- /dev/null +++ b/oai2-to-oai3/src/runner/index.ts @@ -0,0 +1 @@ +export * from './oai2-to-oai3-runner'; \ No newline at end of file diff --git a/oai2-to-oai3/src/runner/oai2-to-oai3-runner.ts b/oai2-to-oai3/src/runner/oai2-to-oai3-runner.ts new file mode 100644 index 0000000..a1d0b1c --- /dev/null +++ b/oai2-to-oai3/src/runner/oai2-to-oai3-runner.ts @@ -0,0 +1,75 @@ +import { DataHandle, get } from "@azure-tools/datastore"; +import { Oai2ToOai3 } from "../converter"; +import { loadInputFiles } from "./utils"; + +export interface OaiToOai3FileInput { + name: string; + schema: any; // OAI2 type? +} + +export interface OaiToOai3FileOutput { + name: string; + result: any; // OAI2 type? +} + +export const convertOai2ToOai3Files = async (inputFiles: DataHandle[]): Promise => { + const files = await loadInputFiles(inputFiles); + const map = new Map(); + for (const file of files) { + map.set(file.name, file); + } + return convertOai2ToOai3(map); +}; + +export const convertOai2ToOai3 = async (inputs: Map): Promise => { + const resolvingFiles = new Set(); + const completedFiles = new Map(); + + const resolveReference: ResolveReferenceFn = async ( + targetfile: string, + refPath: string, + ): Promise => { + const file = inputs.get(targetfile); + if (file === undefined) { + throw new Error(`Ref file ${targetfile} doesn't exists.`); + } + + return get(file.schema, refPath); + }; + + const computeFile = async (input: OaiToOai3FileInput) => { + if (resolvingFiles.has(input.name)) { + // Todo better circular dep findings + throw new Error(`Circular dependency with file ${input.name}`); + } + resolvingFiles.add(input.name); + console.error("Resolving file", input.name); + + const result = await convertOai2ToOai3Schema(input, resolveReference); + completedFiles.set(input.name, { + result, + name: input.name, + }); + return result; + }; + + for (const input of inputs.values()) { + await computeFile(input); + } + return [...completedFiles.values()]; +}; + +/** + * Callback to resolve a reference. + */ +export type AddMappingFn = (oldRef: string, newRef: string, referencedEl: any) => void; +export type ResolveReferenceFn = (targetfile: string, reference: string) => Promise; + +export const convertOai2ToOai3Schema = async ( + { name, schema }: OaiToOai3FileInput, + resolveReference: ResolveReferenceFn, +): Promise => { + const converter = new Oai2ToOai3(name, schema, resolveReference); + await converter.convert(); + return converter.generated; +}; diff --git a/oai2-to-oai3/src/runner/utils.ts b/oai2-to-oai3/src/runner/utils.ts new file mode 100644 index 0000000..7f65188 --- /dev/null +++ b/oai2-to-oai3/src/runner/utils.ts @@ -0,0 +1,11 @@ +import { DataHandle } from '@azure-tools/datastore'; +import { OaiToOai3FileInput } from './oai2-to-oai3-runner'; + +export const loadInputFiles = async (inputFiles: DataHandle[]): Promise => { + const inputs: OaiToOai3FileInput[] = []; + for (const file of inputFiles) { + const schema = await file.ReadObject(); + inputs.push({ name: file.originalFullPath, schema }); + } + return inputs; +}; diff --git a/oai2-to-oai3/status-codes.ts b/oai2-to-oai3/src/status-codes.ts similarity index 100% rename from oai2-to-oai3/status-codes.ts rename to oai2-to-oai3/src/status-codes.ts diff --git a/oai2-to-oai3/test/test-conversion.ts b/oai2-to-oai3/test/test-conversion.ts index 56c1ad2..5173431 100644 --- a/oai2-to-oai3/test/test-conversion.ts +++ b/oai2-to-oai3/test/test-conversion.ts @@ -1,15 +1,14 @@ -import { suite, test, slow, timeout, skip, only } from 'mocha-typescript'; +import { suite, test } from 'mocha-typescript'; import * as assert from 'assert'; import * as aio from '@azure-tools/async-io'; import * as datastore from '@azure-tools/datastore'; -import { stringify, CancellationToken, FastStringify } from '@azure-tools/datastore'; -import { SourceMapGenerator } from 'source-map'; +import { FastStringify } from '@azure-tools/datastore'; /* eslint-disable @typescript-eslint/no-empty-function */ require('source-map-support').install(); -import { Oai2ToOai3 } from '../main'; +import { Oai2ToOai3 } from '../src/converter'; @suite class MyTests { @@ -38,7 +37,7 @@ import { Oai2ToOai3 } from '../main'; const convert = new Oai2ToOai3(swaggerUri, swag); // run the conversion - convert.convert(); + await convert.convert(); // const swaggerAsText = FastStringify(convert.generated); // console.log(swaggerAsText); @@ -72,7 +71,7 @@ import { Oai2ToOai3 } from '../main'; const convert = new Oai2ToOai3(swaggerUri, swag); // run the conversion - convert.convert(); + await convert.convert(); // const swaggerAsText = FastStringify(convert.generated); // console.log(swaggerAsText); @@ -106,7 +105,7 @@ import { Oai2ToOai3 } from '../main'; const convert = new Oai2ToOai3(swaggerUri, swag); // run the conversion - convert.convert(); + await convert.convert(); // const swaggerAsText = FastStringify(convert.generated); // console.log(swaggerAsText); @@ -140,7 +139,7 @@ import { Oai2ToOai3 } from '../main'; const convert = new Oai2ToOai3(swaggerUri, swag); // run the conversion - convert.convert(); + await convert.convert(); // const swaggerAsText = FastStringify(convert.generated); // console.log(swaggerAsText); @@ -174,7 +173,7 @@ import { Oai2ToOai3 } from '../main'; const convert = new Oai2ToOai3(swaggerUri, swag); // run the conversion - convert.convert(); + await convert.convert(); assert.deepStrictEqual(convert.generated, original, 'Should be the same'); } @@ -205,7 +204,7 @@ import { Oai2ToOai3 } from '../main'; const convert = new Oai2ToOai3(swaggerUri, swag); // run the conversion - convert.convert(); + await convert.convert(); assert.deepStrictEqual(convert.generated, original, 'Should be the same'); } @@ -236,7 +235,7 @@ import { Oai2ToOai3 } from '../main'; const convert = new Oai2ToOai3(swaggerUri, swag); // run the conversion - convert.convert(); + await convert.convert(); assert.deepStrictEqual(convert.generated, original, 'Should be the same'); } @@ -267,7 +266,7 @@ import { Oai2ToOai3 } from '../main'; const convert = new Oai2ToOai3(swaggerUri, swag); // run the conversion - convert.convert(); + await convert.convert(); assert.deepStrictEqual(convert.generated, original, 'Should be the same'); } @@ -298,7 +297,7 @@ import { Oai2ToOai3 } from '../main'; const convert = new Oai2ToOai3(swaggerUri, swag); // run the conversion - convert.convert(); + await convert.convert(); assert.deepStrictEqual(convert.generated, original, 'Should be the same'); } @@ -329,7 +328,7 @@ import { Oai2ToOai3 } from '../main'; const convert = new Oai2ToOai3(swaggerUri, swag); // run the conversion - convert.convert(); + await convert.convert(); assert.deepStrictEqual(convert.generated, original, 'Should be the same'); } @@ -361,7 +360,7 @@ import { Oai2ToOai3 } from '../main'; const convert = new Oai2ToOai3(swaggerUri, swag); // run the conversion - convert.convert(); + await convert.convert(); assert.deepStrictEqual(convert.generated, original, 'Should be the same'); } @@ -393,7 +392,7 @@ import { Oai2ToOai3 } from '../main'; const convert = new Oai2ToOai3(swaggerUri, swag); // run the conversion - convert.convert(); + await convert.convert(); assert.deepStrictEqual(convert.generated, original, 'Should be the same'); } @@ -425,7 +424,7 @@ import { Oai2ToOai3 } from '../main'; const convert = new Oai2ToOai3(swaggerUri, swag); // run the conversion - convert.convert(); + await convert.convert(); assert.deepStrictEqual(convert.generated, original, 'Should be the same'); } @@ -456,7 +455,7 @@ import { Oai2ToOai3 } from '../main'; const convert = new Oai2ToOai3(swaggerUri, swag); // run the conversion - convert.convert(); + await convert.convert(); assert.deepStrictEqual(convert.generated, original, 'Should be the same'); } @@ -486,7 +485,7 @@ import { Oai2ToOai3 } from '../main'; const convert = new Oai2ToOai3(swaggerUri, swag); // run the conversion - convert.convert(); + await convert.convert(); assert.deepStrictEqual(convert.generated, original, 'Should be the same'); } @@ -517,7 +516,7 @@ import { Oai2ToOai3 } from '../main'; const convert = new Oai2ToOai3(swaggerUri, swag); // run the conversion - convert.convert(); + await convert.convert(); assert.deepStrictEqual(convert.generated, original, 'Should be the same'); } @@ -548,7 +547,7 @@ import { Oai2ToOai3 } from '../main'; const convert = new Oai2ToOai3(swaggerUri, swag); // run the conversion - convert.convert(); + await convert.convert(); assert.deepStrictEqual(convert.generated, original, 'Should be the same'); } @@ -583,7 +582,7 @@ import { Oai2ToOai3 } from '../main'; const swag = swaggerdata.ReadObject(); const convert = new Oai2ToOai3(swaggerdata.key, swag); - const result = convert.convert(); + const result = await convert.convert(); const sink = ds.getDataSink(); const text = FastStringify(convert.generated); diff --git a/oai2-to-oai3/tsconfig.json b/oai2-to-oai3/tsconfig.json index 87d67f9..c81fb4c 100644 --- a/oai2-to-oai3/tsconfig.json +++ b/oai2-to-oai3/tsconfig.json @@ -3,15 +3,7 @@ "compilerOptions": { "rootDir": ".", "outDir": "dist", - "noImplicitAny": false, + "noImplicitAny": false }, - "include": [ - "." - ], - "exclude": [ - "dist", - "resources", - "node_modules", - "**/*.d.ts" - ] -} \ No newline at end of file + "include": ["src/**/*.ts", "test/**/*.ts"] +}