From 1d4ed64ca0ae974b8d0c843a2212d2edf743f5af Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Fri, 22 Nov 2019 15:31:00 -0800 Subject: [PATCH 01/10] Generate operation specs --- .vscode/launch.json | 61 +++++--- src/operationTransforms.ts | 136 ++++++++++++++++++ test/unit/operationTransforms.spec.ts | 192 ++++++++++++++++++++++++++ 3 files changed, 366 insertions(+), 23 deletions(-) create mode 100644 src/operationTransforms.ts create mode 100644 test/unit/operationTransforms.spec.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 16e656cbfa..e61bf8206d 100755 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,24 +1,39 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "attach", - "name": "Attach", - "port": 9229 - }, - { - "type": "node", - "request": "launch", - "name": "Launch Program", - "program": "${workspaceFolder}\\dist\\src\\main.js", - "preLaunchTask": "tsc: build - tsconfig.json", - "outFiles": [ - "${workspaceFolder}/dist/**/*.js" - ] - } - ] -} \ No newline at end of file + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "attach", + "name": "Attach", + "port": 9229 + }, + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "program": "${workspaceFolder}\\dist\\src\\main.js", + "preLaunchTask": "tsc: build - tsconfig.json", + "outFiles": ["${workspaceFolder}/dist/**/*.js"] + }, + { + "type": "node", + "request": "launch", + "name": "UnitTests", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", + "args": [ + "-r", + "ts-node/register", + "--timeout", + "999999", + "--colors", + "${workspaceFolder}/test/unit/**/*.spec.ts" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "protocol": "inspector" + } + ] +} diff --git a/src/operationTransforms.ts b/src/operationTransforms.ts new file mode 100644 index 0000000000..757611d12d --- /dev/null +++ b/src/operationTransforms.ts @@ -0,0 +1,136 @@ +import { + OperationSpec, + Serializer, + HttpMethods, + OperationResponse +} from "@azure/core-http"; +import { + Operation, + Request, + SchemaResponse, + Response, + Schema, + SchemaType, + ChoiceSchema +} from "@azure-tools/codemodel"; +import { getLanguageMetadata } from "./transforms"; + +export function transformOperationSpec(operation: Operation): OperationSpec { + // Extract protocol information + const httpInfo = extractHttpDetails(operation.request); + const serializer = new Serializer(); + + return { + ...httpInfo, + responses: extractResponses(operation.responses, operation.exceptions), + serializer + }; +} + +export function extractHttpDetails({ protocol }: Request) { + if (!protocol.http) { + throw new Error("operation doesn't contain a definition for HTTP protocol"); + } + const { path, method } = protocol.http; + + return { + path, + httpMethod: method.toUpperCase() as HttpMethods + }; +} + +export type OperationResponses = { [responseCode: string]: OperationResponse }; +export function extractResponses( + responses?: Array, + exceptions: Array = [] +): OperationResponses { + if (!responses || !responses.length) { + return {}; + } + const schemaResponses = extractSchemaResponses(responses); + const defaultResponse = extractSchemaResponses(exceptions); + + return { + ...schemaResponses, + ...defaultResponse + }; +} + +export interface SpecType { + name: any; + allowedValues?: any[]; +} + +export function getSpecType(responseSchema: Schema): any { + let typeName: string; + let allowedValues: any[] | undefined = undefined; + switch (responseSchema.type) { + case SchemaType.ByteArray: + typeName = "Base64Url"; + break; + case SchemaType.String: + case SchemaType.Constant: + typeName = "String"; + break; + case SchemaType.Choice: + const choiceSchema = responseSchema as ChoiceSchema; + typeName = "Enum"; + allowedValues = choiceSchema.choices.map(choice => choice.value); + break; + case SchemaType.Object: + const name = getLanguageMetadata(responseSchema.language).name; + return `Mappers.${name}`; + default: + throw new Error(`Unsupported Spec Type ${responseSchema.type}`); + } + + let result = { + name: typeName + }; + + return !!allowedValues ? { ...result, allowedValues } : result; +} + +export function extractBasicResponses( + responses: Array +) { + return (responses as any[]).filter((r: SchemaResponse) => !r.schema); +} + +export function extractSchemaResponses( + responses: Array +) { + return responses.reduce( + (result: OperationResponses, response: SchemaResponse | Response) => { + if (!response.protocol.http) { + return result; + } + + const statusCodes = response.protocol.http.statusCodes; + + if (!statusCodes || !statusCodes.length) { + return result; + } + + const statusCode = statusCodes[0]; + result[statusCode] = {}; + const schemaResponse = response as any; + if (schemaResponse.schema) { + result[statusCode] = { + bodyMapper: getBodyMapperFromSchema(schemaResponse.schema) + }; + } + return result; + }, + {} + ); +} + +function getBodyMapperFromSchema(responseSchema: Schema) { + const responseType = getSpecType(responseSchema); + return !responseType.name + ? responseType + : { + type: responseType + }; +} diff --git a/test/unit/operationTransforms.spec.ts b/test/unit/operationTransforms.spec.ts new file mode 100644 index 0000000000..30ba5f514c --- /dev/null +++ b/test/unit/operationTransforms.spec.ts @@ -0,0 +1,192 @@ +import * as assert from "assert"; +import { + transformOperationSpec, + getSpecType +} from "../../src/operationTransforms"; +import { + Operation, + SchemaResponse, + SchemaType, + Schema, + ChoiceValue, + ChoiceSchema, + ObjectSchema +} from "@azure-tools/codemodel"; +import { KnownMediaType } from "@azure-tools/codegen"; +import { OperationSpec } from "@azure/core-http"; + +const choice = new ChoiceSchema("mockChoice", "", { + choices: [ + new ChoiceValue("red", "", "red color"), + new ChoiceValue("green-color", "", "green-color"), + new ChoiceValue("blue_color", "", "blue_color") + ] +}); + +const constantSchema = new Schema( + "paths·string-null·get·responses·200·content·application-json·schema", + "", + SchemaType.Constant +); + +const objectSchema = new ObjectSchema("RefColorConstant", "", {}); +const errorSchema = new ObjectSchema("ErrorModel", "", {}); + +describe("OperationTransforms", () => { + describe("getSpecType", () => { + it("should return string when type is constant or string", () => { + assert.strictEqual( + getSpecType({ type: SchemaType.Constant } as any).name, + "String", + "constant" + ); + assert.strictEqual( + getSpecType({ type: SchemaType.String } as any).name, + "String", + "string" + ); + }); + it("should return Base64Url when type is ByteArray", () => { + assert.strictEqual( + getSpecType({ type: SchemaType.ByteArray } as any).name, + "Base64Url" + ); + }); + it("should return the right Enum type for Choice", () => { + const choiceType = getSpecType(choice); + assert.strictEqual(choiceType.name, "Enum"); + assert.deepEqual(choiceType.allowedValues, [ + "red color", + "green-color", + "blue_color" + ]); + }); + it("should return a reference to a mapper when type is object", () => { + const objectType = getSpecType(objectSchema); + assert.strictEqual(objectType, `Mappers.RefColorConstant`); + }); + }); + + describe("transformOperationSpec", () => { + describe("Simple get operation", () => { + const operationPath = "/string/null"; + const getErrorResponseSchema = () => { + const schema = errorSchema; + const response = new SchemaResponse(schema); + + response.protocol = { + http: { + knownMediaType: KnownMediaType.Json, + statusCodes: ["default"], + mediaTypes: "application/json" + } as any + }; + + return response; + }; + const get200ResponseSchema = (schema: Schema) => { + const response = new SchemaResponse(schema); + + response.protocol = { + http: { + knownMediaType: KnownMediaType.Json, + statusCodes: [200], + mediaTypes: "application/json" + } as any + }; + + return response; + }; + + const getOperation = (responseSchema?: SchemaResponse) => { + return new Operation("", "", { + request: { + parameters: [], + protocol: { + http: { + path: operationPath, + method: "get", + url: "{$host}" + } + } + }, + responses: [ + responseSchema || { protocol: { http: { statusCodes: ["200"] } } } + ], + exceptions: [getErrorResponseSchema()], + language: { + default: { + name: "getNull", + description: "Get null string value value" + } + } + }); + }; + + const checkHttpMethodAndPath = (operationSpec: OperationSpec) => { + assert.strictEqual( + operationSpec.httpMethod, + "GET", + `expected HTTPMethod to be GET, actual ${operationSpec.httpMethod}` + ); + + assert.strictEqual( + operationSpec.path, + operationPath, + `expected PATH to be ${operationPath}, actual ${operationSpec.path}` + ); + }; + + it("should create an operation spec with correct http details", () => { + const okResponseSchema = get200ResponseSchema(constantSchema); + const mockOperation = getOperation(okResponseSchema); + const operationSpec = transformOperationSpec(mockOperation); + checkHttpMethodAndPath(operationSpec); + }); + + it("should create an operation spec with correct responses from a basic response", () => { + const mockOperation = getOperation(); + const operationSpec = transformOperationSpec(mockOperation); + checkHttpMethodAndPath(operationSpec); + assert.deepEqual(operationSpec.responses[200], {}); + }); + + it("should create an operation spec with correct responses spec and cosntant schema response", () => { + const okResponseSchema = get200ResponseSchema(constantSchema); + const mockOperation = getOperation(okResponseSchema); + const operationSpec = transformOperationSpec(mockOperation); + checkHttpMethodAndPath(operationSpec); + const okResponse = operationSpec.responses[200]; + // assert.strictEqual( + // okResponse.bodyMapper!.serializedName, + // "parsedResponse" + // ); + assert.deepEqual(okResponse.bodyMapper!.type, { name: "String" }); + assert.deepEqual( + operationSpec.responses.default.bodyMapper, + "Mappers.ErrorModel" + ); + }); + + it("should create an operation spec with correct responses spec and choice schema response", () => { + const okResponseSchema = get200ResponseSchema(choice); + const mockOperation = getOperation(okResponseSchema); + const operationSpec = transformOperationSpec(mockOperation); + checkHttpMethodAndPath(operationSpec); + const okResponse = operationSpec.responses[200]; + // assert.strictEqual( + // okResponse.bodyMapper!.serializedName, + // "parsedResponse" + // ); + assert.deepEqual(okResponse.bodyMapper!.type, { + name: "Enum", + allowedValues: ["red color", "green-color", "blue_color"] + }); + assert.deepEqual( + operationSpec.responses.default.bodyMapper, + "Mappers.ErrorModel" + ); + }); + }); + }); +}); From b9c10ccaf7d14b4bcaa4bc8909b2795b41f81b13 Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Mon, 25 Nov 2019 12:40:14 -0800 Subject: [PATCH 02/10] Generate opration files --- package.json | 3 +- src/generators/operationGenerator.ts | 158 +++++++++++++++++++++++++++ src/operationTransforms.ts | 3 +- src/typescriptGenerator.ts | 2 + src/utils/nameUtils.ts | 7 +- 5 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 src/generators/operationGenerator.ts diff --git a/package.json b/package.json index 2081cc898b..7d0f5211c7 100755 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "test": "npm run unit-test & npm run integration-test", "unit-test": "mocha -r ts-node/register './test/unit/**/*spec.ts'", "integration-test": "ts-node test/utils/start-server.ts & mocha -r ts-node/register './test/integration/**/*spec.ts' & stop-autorest-testserver", - "debug": "node --inspect-brk ./dist/src/main.js" + "debug": "node --inspect-brk ./dist/src/main.js", + "generate-bodystring": "npm run build && autorest-beta --typescript --use=/Users/joheredi/Projects/Azure/autorest.typescript.v3 --title=BodyString --input-file=node_modules/@autorest/test-server/__files/swagger/body-string.json --package-name=bodyString --package-version=1.0.0-preview1" }, "dependencies": { "@autorest/autorest": "^3.0.6122", diff --git a/src/generators/operationGenerator.ts b/src/generators/operationGenerator.ts new file mode 100644 index 0000000000..c443a81ad5 --- /dev/null +++ b/src/generators/operationGenerator.ts @@ -0,0 +1,158 @@ +import { CodeModel, OperationGroup, Language } from "@azure-tools/codemodel"; +import { Project, SourceFile, VariableDeclarationKind, Scope } from "ts-morph"; +import { getLanguageMetadata } from "../transforms"; +import { normalizeName, NameType } from "../utils/nameUtils"; +import { ClientDetails } from "../models/clientDetails"; +import { transformOperationSpec } from "../operationTransforms"; +import { OperationSpec } from "@azure/core-http"; + +export function generateOperations( + codeModel: CodeModel, + clientDetails: ClientDetails, + project: Project +) { + codeModel.operationGroups.forEach(operationGroup => + generateOperation(operationGroup, clientDetails, project) + ); +} + +function generateOperation( + operationGroup: OperationGroup, + clientDetails: ClientDetails, + project: Project +) { + const namingMetadata = getLanguageMetadata(operationGroup.language); + const name = normalizeName(namingMetadata.name, NameType.File); + + const operationGroupFile = project.createSourceFile( + `src/operations/${name}.ts`, + undefined, + { overwrite: true } + ); + + addImports(operationGroupFile, clientDetails); + addClass(operationGroupFile, namingMetadata, clientDetails); + addOperationSchemas(operationGroup, operationGroupFile); +} + +function addOperationSchemas(operationGroup: OperationGroup, file: SourceFile) { + file.addVariableStatement({ + declarationKind: VariableDeclarationKind.Const, + declarations: [ + { + name: "serializer", + initializer: "new coreHttp.Serializer(Mappers);" + } + ] + }); + + operationGroup.operations.forEach(operation => { + const metadata = getLanguageMetadata(operation.language); + const operationName = normalizeName(metadata.name, NameType.Property); + const operationSpec = transformOperationSpec(operation); + file.addVariableStatement({ + declarationKind: VariableDeclarationKind.Const, + declarations: [ + { + name: operationName, + type: "coreHttp.OperationSpec", + initializer: buildSpec(operationSpec) + } + ] + }); + }); +} + +function buildSpec(spec: OperationSpec) { + const responses = buildResponses(spec); + return `{ + path: "${spec.path}", + httpMethod: "${spec.httpMethod}", + responses: {${responses.join(", ")}}, + serializer + }`; +} + +function buildResponses({ responses }: OperationSpec) { + const x = "hola"; + const responseCodes = Object.keys(responses); + let parsedResponses: string[] = []; + responseCodes.forEach(code => { + if ( + responses[code] && + responses[code].bodyMapper && + (responses[code].bodyMapper as any).indexOf && + (responses[code].bodyMapper as any).indexOf("Mappers") > -1 + ) { + // Complex + parsedResponses.push(`${code}: { + bodyMapper: ${responses[code].bodyMapper} + }`); + } else { + // Simple + parsedResponses.push(`${code}: ${JSON.stringify(responses[code])}`); + } + }); + + return parsedResponses; +} + +function addClass( + operationGroupFile: SourceFile, + namingMetadata: Language, + clientDetails: ClientDetails +) { + const className = normalizeName(namingMetadata.name, NameType.Class); + const operationGroupClass = operationGroupFile.addClass({ + name: className, + docs: [`Class representing a ${className}.`], + isExported: true + }); + operationGroupClass.addProperty({ + name: "client", + isReadonly: true, + scope: Scope.Private, + type: clientDetails.className + }); + const constructorDefinition = operationGroupClass.addConstructor({ + docs: [ + { + description: `Initialize a new instance of the class ${className} class.` + } + ], + parameters: [ + { + name: "client", + hasQuestionToken: false, + type: clientDetails.className + } + ] + }); + + constructorDefinition.addStatements(["this.client = client"]); +} + +function addImports( + operationGroupFile: SourceFile, + { className, sourceFileName }: ClientDetails +) { + operationGroupFile.addImportDeclaration({ + namespaceImport: "coreHttp", + moduleSpecifier: "@azure/core-http" + }); + + operationGroupFile.addImportDeclaration({ + namespaceImport: "Models", + moduleSpecifier: "../models" + }); + + operationGroupFile.addImportDeclaration({ + namespaceImport: "Mappers", + moduleSpecifier: "../models/mappers" + }); + + operationGroupFile.addImportDeclaration({ + namedImports: [className], + moduleSpecifier: `../${sourceFileName}` + }); +} diff --git a/src/operationTransforms.ts b/src/operationTransforms.ts index 757611d12d..4d499dc2dc 100644 --- a/src/operationTransforms.ts +++ b/src/operationTransforms.ts @@ -14,6 +14,7 @@ import { ChoiceSchema } from "@azure-tools/codemodel"; import { getLanguageMetadata } from "./transforms"; +import { normalizeName, NameType } from "./utils/nameUtils"; export function transformOperationSpec(operation: Operation): OperationSpec { // Extract protocol information @@ -79,7 +80,7 @@ export function getSpecType(responseSchema: Schema): any { break; case SchemaType.Object: const name = getLanguageMetadata(responseSchema.language).name; - return `Mappers.${name}`; + return `Mappers.${normalizeName(name, NameType.Class)}`; default: throw new Error(`Unsupported Spec Type ${responseSchema.type}`); } diff --git a/src/typescriptGenerator.ts b/src/typescriptGenerator.ts index 129a8c092e..ce09b8a4a3 100755 --- a/src/typescriptGenerator.ts +++ b/src/typescriptGenerator.ts @@ -17,6 +17,7 @@ import { generateLicenseFile } from "./generators/static/licenseFileGenerator"; import { generateReadmeFile } from "./generators/static/readmeFileGenerator"; import { generateTsConfig } from "./generators/static/tsConfigFileGenerator"; import { generateRollupConfig } from "./generators/static/rollupConfigFileGenerator"; +import { generateOperations } from "./generators/operationGenerator"; const prettierTypeScriptOptions: prettier.Options = { parser: "typescript", @@ -77,6 +78,7 @@ export class TypescriptGenerator { generateClientContext(clientDetails, packageDetails, project); generateModels(this.codeModel, project); generateMappers(this.codeModel, project); + generateOperations(this.codeModel, clientDetails, project); // TODO: Get this from the "license-header" setting: // await this.host.GetValue("license-header"); diff --git a/src/utils/nameUtils.ts b/src/utils/nameUtils.ts index 08dd1509b4..43d2a434da 100644 --- a/src/utils/nameUtils.ts +++ b/src/utils/nameUtils.ts @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +const ReservedModelNames = ["Error"]; export enum CasingConvention { Pascal, @@ -21,7 +22,11 @@ export function normalizeName(name: string, nameType: NameType): string { const normalizedParts = (otherParts || []) .map(part => toCasing(part, CasingConvention.Pascal)) .join(""); - return `${normalizedFirstPart}${normalizedParts}`; + + const normalized = `${normalizedFirstPart}${normalizedParts}`; + return ReservedModelNames.indexOf(normalized) > -1 + ? `${normalized}Model` + : normalized; } export function getModelsName(title: string): string { From 9d8984ae6044724cd3c8c3a520523dff9b8837f5 Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Mon, 25 Nov 2019 16:01:49 -0800 Subject: [PATCH 03/10] Wire client and operations to pass first 2 basic tests --- src/generators/clientContextFileGenerator.ts | 2 +- src/generators/clientFileGenerator.ts | 59 ++++--- src/generators/operationGenerator.ts | 161 +++++++++++++++++-- src/generators/operationGroupsGenerator.ts | 30 ---- src/models/operationDetails.ts | 57 +++++++ src/operationTransforms.ts | 114 ++++++++++++- src/transforms.ts | 10 +- src/typescriptGenerator.ts | 2 +- test/integration/bodyString.spec.ts | 33 ++++ test/unit/operationTransforms.spec.ts | 25 +-- 10 files changed, 416 insertions(+), 77 deletions(-) delete mode 100644 src/generators/operationGroupsGenerator.ts create mode 100644 src/models/operationDetails.ts create mode 100644 test/integration/bodyString.spec.ts diff --git a/src/generators/clientContextFileGenerator.ts b/src/generators/clientContextFileGenerator.ts index 8c71ca7f30..05fc3dac4d 100644 --- a/src/generators/clientContextFileGenerator.ts +++ b/src/generators/clientContextFileGenerator.ts @@ -56,7 +56,7 @@ export function generateClientContext( { name: "options", hasQuestionToken: true, - type: `Models.${clientDetails.className}Options` + type: "any" // TODO: Use the correct type from models `Models.${clientDetails.className}Options` } ] }); diff --git a/src/generators/clientFileGenerator.ts b/src/generators/clientFileGenerator.ts index 0d3d9e7697..dd924e8067 100644 --- a/src/generators/clientFileGenerator.ts +++ b/src/generators/clientFileGenerator.ts @@ -1,15 +1,25 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { Project } from "ts-morph"; +import { Project, PropertyDeclarationStructure } from "ts-morph"; import { ClientDetails } from "../models/clientDetails"; -import { getModelsName, getMappersName } from "../utils/nameUtils"; +import { + getModelsName, + getMappersName, + normalizeName, + NameType +} from "../utils/nameUtils"; +import { CodeModel } from "@azure-tools/codemodel"; +import { transformOperationGroup } from "../operationTransforms"; -export function generateClient(clientDetails: ClientDetails, project: Project) { +export function generateClient( + codeModel: CodeModel, + clientDetails: ClientDetails, + project: Project +) { const modelsName = getModelsName(clientDetails.className); const mappersName = getMappersName(clientDetails.className); const clientContextClassName = `${clientDetails.className}Context`; - const clientContextFileName = `${clientDetails.sourceFileName}Context`; const clientFile = project.createSourceFile( `src/${clientDetails.sourceFileName}.ts`, @@ -44,15 +54,25 @@ export function generateClient(clientDetails: ClientDetails, project: Project) { extends: clientContextClassName }); - clientClass.addProperties([ - // TODO: Generate these based on operation groups list - // { - // name: "string", - // type: "operations.String", - // leadingTrivia: writer => writer.write("// Operation groups") - // }, - // { name: "enumModel", type: "operations.EnumModel" } - ]); + const operations = codeModel.operationGroups.map(og => { + const operationGoupDetails = transformOperationGroup(og); + return { + name: normalizeName(operationGoupDetails.name, NameType.Property), + typeName: `operations.${normalizeName( + operationGoupDetails.name, + NameType.Class + )}` + }; + }); + + clientClass.addProperties( + operations.map(op => { + return { + name: op.name, + type: op.typeName + } as PropertyDeclarationStructure; + }) + ); const clientConstructor = clientClass.addConstructor({ docs: [ @@ -65,20 +85,21 @@ export function generateClient(clientDetails: ClientDetails, project: Project) { { name: "options", hasQuestionToken: true, - type: `Models.${clientDetails.className}Options` + type: "any" // TODO Use the right type `Models.${clientDetails.className}Options` } ] }); clientConstructor.addStatements([ - "super(options);" - // TODO: Generate these based on operation groups list - // "this.string = new operations.String(this);", - // "this.enumModel = new operations.EnumModel(this);" + "super(options);", + ...operations.map( + ({ name, typeName }) => `this.${name} = new ${typeName}(this)` + ) ]); clientFile.addExportDeclaration({ - leadingTrivia: writer => writer.write("// Operation Specifications\n\n"), + leadingTrivia: (writer: any) => + writer.write("// Operation Specifications\n\n"), namedExports: [ { name: clientDetails.className }, { name: clientContextClassName }, diff --git a/src/generators/operationGenerator.ts b/src/generators/operationGenerator.ts index c443a81ad5..4954dc7093 100644 --- a/src/generators/operationGenerator.ts +++ b/src/generators/operationGenerator.ts @@ -1,28 +1,65 @@ -import { CodeModel, OperationGroup, Language } from "@azure-tools/codemodel"; -import { Project, SourceFile, VariableDeclarationKind, Scope } from "ts-morph"; +import { + CodeModel, + OperationGroup, + Language, + ParameterLocation +} from "@azure-tools/codemodel"; +import { + Project, + SourceFile, + VariableDeclarationKind, + Scope, + ClassDeclaration, + ParameterDeclarationStructure, + StructureKind, + OptionalKind, + MethodDeclarationOverloadStructure, + ExportDeclarationStructure +} from "ts-morph"; import { getLanguageMetadata } from "../transforms"; import { normalizeName, NameType } from "../utils/nameUtils"; import { ClientDetails } from "../models/clientDetails"; -import { transformOperationSpec } from "../operationTransforms"; +import { + transformOperationSpec, + transformOperationGroup +} from "../operationTransforms"; import { OperationSpec } from "@azure/core-http"; +import { OperationGroupDetails } from "../models/operationDetails"; export function generateOperations( codeModel: CodeModel, clientDetails: ClientDetails, project: Project ) { - codeModel.operationGroups.forEach(operationGroup => - generateOperation(operationGroup, clientDetails, project) + let fileNames: string[] = []; + codeModel.operationGroups.forEach(operationGroup => { + const operationDetails = transformOperationGroup(operationGroup); + fileNames.push(normalizeName(operationDetails.name, NameType.File)); + generateOperation(operationDetails, operationGroup, clientDetails, project); + }); + + const operationIndexFile = project.createSourceFile( + "src/operations/index.ts", + undefined, + { overwrite: true } + ); + + operationIndexFile.addExportDeclarations( + fileNames.map(fileName => { + return { + moduleSpecifier: `./${fileName}` + } as ExportDeclarationStructure; + }) ); } function generateOperation( + operationGroupDetails: OperationGroupDetails, operationGroup: OperationGroup, clientDetails: ClientDetails, project: Project ) { - const namingMetadata = getLanguageMetadata(operationGroup.language); - const name = normalizeName(namingMetadata.name, NameType.File); + const name = normalizeName(operationGroupDetails.name, NameType.File); const operationGroupFile = project.createSourceFile( `src/operations/${name}.ts`, @@ -31,7 +68,7 @@ function generateOperation( ); addImports(operationGroupFile, clientDetails); - addClass(operationGroupFile, namingMetadata, clientDetails); + addClass(operationGroupFile, operationGroupDetails, clientDetails); addOperationSchemas(operationGroup, operationGroupFile); } @@ -54,7 +91,7 @@ function addOperationSchemas(operationGroup: OperationGroup, file: SourceFile) { declarationKind: VariableDeclarationKind.Const, declarations: [ { - name: operationName, + name: `${operationName}OperationSpec`, type: "coreHttp.OperationSpec", initializer: buildSpec(operationSpec) } @@ -65,16 +102,32 @@ function addOperationSchemas(operationGroup: OperationGroup, file: SourceFile) { function buildSpec(spec: OperationSpec) { const responses = buildResponses(spec); + const requestBody = buildRequestBody(spec); return `{ path: "${spec.path}", httpMethod: "${spec.httpMethod}", responses: {${responses.join(", ")}}, + ${requestBody} serializer }`; } +function buildRequestBody({ requestBody }: OperationSpec) { + if (!requestBody) { + return ""; + } + + const mapper = !requestBody.mapper.type + ? requestBody.mapper + : JSON.stringify(requestBody.mapper); + + return `requestBody: { + parameterPath: "${requestBody.parameterPath}", + mapper: ${mapper} + },`; +} + function buildResponses({ responses }: OperationSpec) { - const x = "hola"; const responseCodes = Object.keys(responses); let parsedResponses: string[] = []; responseCodes.forEach(code => { @@ -99,10 +152,10 @@ function buildResponses({ responses }: OperationSpec) { function addClass( operationGroupFile: SourceFile, - namingMetadata: Language, + operationGroupDetails: OperationGroupDetails, clientDetails: ClientDetails ) { - const className = normalizeName(namingMetadata.name, NameType.Class); + const className = normalizeName(operationGroupDetails.name, NameType.Class); const operationGroupClass = operationGroupFile.addClass({ name: className, docs: [`Class representing a ${className}.`], @@ -130,6 +183,90 @@ function addClass( }); constructorDefinition.addStatements(["this.client = client"]); + addOperations(operationGroupDetails, operationGroupClass); +} + +function addOperations( + operationGroupDetails: OperationGroupDetails, + operationGroupClass: ClassDeclaration +) { + const primitiveTypes = ["string", "boolean", "number", "any", "Int8Array"]; + operationGroupDetails.operations.forEach(operation => { + const parameters = operation.request.parameters || []; + const params = parameters + .filter(param => param.location === ParameterLocation.Body) + .map(param => { + const typeName = param.modelType || "any"; + const type = + primitiveTypes.indexOf(typeName) > -1 + ? typeName + : `Models.${typeName}`; + return { + name: param.name, + type, + hasQuestionToken: !param.required + }; + }); + const operationMethod = operationGroupClass.addMethod({ + name: normalizeName(operation.name, NameType.Property), + docs: [operation.description], + parameters: [ + ...params, + getOptionsParameter(true), + getCallbackParameter(true) + ], + returnType: "Promise" // TODO: Add correct return type + }); + + const sendParams = params.map(p => p.name).join(","); + operationMethod.addStatements( + `return this.client.sendOperationRequest({${sendParams}${ + !!sendParams ? "," : "" + } options}, ${operation.name}OperationSpec, callback)` + ); + + operationMethod.addOverloads([ + // Overload with optional options + { + parameters: [...params, getOptionsParameter(true)], + returnType: "Promise" // TODO: Add correct return type + }, + // Overload with required callback + { + parameters: [...params, getCallbackParameter(false)], + returnType: "void" + }, + // Overload with required options and callback + { + parameters: [ + ...params, + getOptionsParameter(false), + getCallbackParameter(false) + ], + returnType: "void" + } + ]); + }); +} + +function getOptionsParameter( + isOptional = false +): OptionalKind { + return { + name: "options", + type: "coreHttp.RequestOptionsBase", + hasQuestionToken: isOptional + }; +} + +function getCallbackParameter( + isOptional = false +): OptionalKind { + return { + name: "callback", + type: "coreHttp.ServiceCallback", // TODO get the real type for callback + hasQuestionToken: isOptional + }; } function addImports( diff --git a/src/generators/operationGroupsGenerator.ts b/src/generators/operationGroupsGenerator.ts deleted file mode 100644 index c7756b5a47..0000000000 --- a/src/generators/operationGroupsGenerator.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { Generator } from "./generator"; -import { CodeModel } from '@azure-tools/codemodel'; -import { Host } from '@azure-tools/autorest-extension-base'; -import * as constants from '../utils/constants'; -import * as fs from 'fs'; - -export class OperationGroupsGenerator implements Generator { - templateName: string; - private codeModel:CodeModel; - private host:Host; - - constructor(codeModel: CodeModel, host: Host) { - this.codeModel = codeModel; - this.host = host; - this.templateName = 'operation_groups_template.ejs'; - } - - getTemplate(): string { - return fs.readFileSync(`${constants.TEMPLATE_LOCATION}/${this.templateName}`, { - encoding: 'utf8' - }); - } - - public async process(): Promise { - throw new Error("Method not implemented."); - } -} diff --git a/src/models/operationDetails.ts b/src/models/operationDetails.ts new file mode 100644 index 0000000000..1c9a2e4f41 --- /dev/null +++ b/src/models/operationDetails.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ParameterLocation, HttpMethod } from "@azure-tools/codemodel"; +import { KnownMediaType } from "@azure-tools/codegen"; +import { Mapper } from "@azure/core-http"; + +/** + * Details of an operation request parameter, transformed from Request. + */ +export interface OperationRequestParameterDetails { + name: string; + description: string; + required?: boolean; + modelType?: string; // Could be a primitive or actual model type + mapper: Mapper | string; + location: ParameterLocation; +} + +/** + * Details of an operation request, transformed from Request. + */ +export interface OperationRequestDetails { + path: string; + method: HttpMethod; + mediaType?: KnownMediaType; + parameters?: OperationRequestParameterDetails[]; +} + +/** + * Details of an operation response, transformed from Response or SchemaResponse. + */ +export interface OperationResponseDetails { + statusCodes: string[]; // Can be a status code number or "default" + modelType?: string; // Could be a primitive or actual model type + mediaType?: KnownMediaType; +} + +/** + * Details of an operation, transformed from Operation. + */ +export interface OperationDetails { + name: string; + description: string; + apiVersions: string[]; + request: OperationRequestDetails; + responses: OperationResponseDetails[]; +} + +/** + * Details of an operation group, transformed from OperationGroup. + */ +export interface OperationGroupDetails { + key: string; + name: string; + operations: OperationDetails[]; +} diff --git a/src/operationTransforms.ts b/src/operationTransforms.ts index 4d499dc2dc..c97bd1898d 100644 --- a/src/operationTransforms.ts +++ b/src/operationTransforms.ts @@ -11,19 +11,31 @@ import { Response, Schema, SchemaType, - ChoiceSchema + ChoiceSchema, + OperationGroup, + ParameterLocation, + Parameter } from "@azure-tools/codemodel"; -import { getLanguageMetadata } from "./transforms"; +import { getLanguageMetadata, getTypeForSchema } from "./transforms"; import { normalizeName, NameType } from "./utils/nameUtils"; +import { + OperationGroupDetails, + OperationDetails, + OperationResponseDetails, + OperationRequestDetails, + OperationRequestParameterDetails +} from "./models/operationDetails"; export function transformOperationSpec(operation: Operation): OperationSpec { // Extract protocol information const httpInfo = extractHttpDetails(operation.request); const serializer = new Serializer(); + const operationDetails = transformOperation(operation); return { ...httpInfo, responses: extractResponses(operation.responses, operation.exceptions), + requestBody: extractRequest(operationDetails) as any, serializer }; } @@ -35,7 +47,7 @@ export function extractHttpDetails({ protocol }: Request) { const { path, method } = protocol.http; return { - path, + path: path.replace("{$host}/", ""), httpMethod: method.toUpperCase() as HttpMethods }; } @@ -57,6 +69,21 @@ export function extractResponses( }; } +export function extractRequest(operationDetails: OperationDetails) { + const parameters = (operationDetails.request.parameters || []).filter( + p => p.location === ParameterLocation.Body + ); + + if (parameters.length < 1) { + return undefined; + } + + return { + parameterPath: parameters.map(p => p.name)[0], + mapper: parameters[0].mapper + }; +} + export interface SpecType { name: any; allowedValues?: any[]; @@ -135,3 +162,84 @@ function getBodyMapperFromSchema(responseSchema: Schema) { type: responseType }; } + +/** + * Operation Details + */ + +export function transformOperationRequestParameter( + parameter: Parameter +): OperationRequestParameterDetails { + const metadata = getLanguageMetadata(parameter.language); + return { + name: metadata.name, + description: metadata.description, + modelType: getTypeForSchema(parameter.schema).typeName, + required: parameter.required, + location: parameter.protocol.http + ? parameter.protocol.http.in + : ParameterLocation.Body, + mapper: getBodyMapperFromSchema(parameter.schema) + }; +} + +export function transformOperationRequest( + request: Request +): OperationRequestDetails { + if (request.protocol.http) { + return { + path: request.protocol.http.path.replace("{$host}/", ""), + method: request.protocol.http.method, + parameters: request.parameters + ? request.parameters.map(transformOperationRequestParameter) + : undefined + }; + } else { + throw new Error("Operation does not specify HTTP request details."); + } +} + +export function transformOperationResponse( + response: Response +): OperationResponseDetails { + let modelType: string | undefined = undefined; + if ((response as any).schema) { + const schemaResponse = response as SchemaResponse; + modelType = getTypeForSchema(schemaResponse.schema).typeName; + } + + if (response.protocol.http) { + return { + statusCodes: response.protocol.http.statusCodes, + mediaType: response.protocol.http.knownMediaType, + modelType + }; + } else { + throw new Error("Operation does not specify HTTP response details."); + } +} + +export function transformOperation(operation: Operation): OperationDetails { + const metadata = getLanguageMetadata(operation.language); + operation.responses; + return { + name: normalizeName(metadata.name, NameType.Property), + apiVersions: operation.apiVersions + ? operation.apiVersions.map(v => v.version) + : [], + description: metadata.description, + request: transformOperationRequest(operation.request), + responses: [] + }; +} + +export function transformOperationGroup( + operationGroup: OperationGroup +): OperationGroupDetails { + const metadata = getLanguageMetadata(operationGroup.language); + return { + name: normalizeName(metadata.name, NameType.Property), + key: operationGroup.$key, + operations: operationGroup.operations.map(transformOperation) + }; +} diff --git a/src/transforms.ts b/src/transforms.ts index 66182385eb..6517167036 100644 --- a/src/transforms.ts +++ b/src/transforms.ts @@ -14,7 +14,6 @@ import { SchemaType, Property, ChoiceSchema, - ChoiceValue, ValueSchema, ConstantSchema } from "@azure-tools/codemodel"; @@ -75,6 +74,15 @@ export function getTypeForSchema(schema: Schema): PropertyTypeDetails { false ); break; + case SchemaType.ByteArray: + typeName = "Int8Array"; + break; + case SchemaType.Choice: + case SchemaType.SealedChoice: + case SchemaType.Object: + const { name } = getLanguageMetadata(schema.language); + typeName = name; + break; default: throw new Error(`Unsupported schema type: ${schema.type}`); } diff --git a/src/typescriptGenerator.ts b/src/typescriptGenerator.ts index ce09b8a4a3..21e6e76bf4 100755 --- a/src/typescriptGenerator.ts +++ b/src/typescriptGenerator.ts @@ -74,7 +74,7 @@ export class TypescriptGenerator { generateRollupConfig(clientDetails, packageDetails, project); } - generateClient(clientDetails, project); + generateClient(this.codeModel, clientDetails, project); generateClientContext(clientDetails, packageDetails, project); generateModels(this.codeModel, project); generateMappers(this.codeModel, project); diff --git a/test/integration/bodyString.spec.ts b/test/integration/bodyString.spec.ts new file mode 100644 index 0000000000..415c171949 --- /dev/null +++ b/test/integration/bodyString.spec.ts @@ -0,0 +1,33 @@ +import { equal, fail, ok } from "assert"; +import { BodyString as BodyStringClient } from "../../generated/src/bodyString"; + +describe("Integration tests for BodyString", () => { + describe("getMbcs", () => { + it("should receive an UTF8 string in the response body", async () => { + const client = new BodyStringClient(); + try { + const result = await client.string.getMbcs(); + equal( + result.body, + "啊齄丂狛狜隣郎隣兀﨩ˊ〞〡¦℡㈱‐ー﹡﹢﹫、〓ⅰⅹ⒈€㈠㈩ⅠⅫ! ̄ぁんァヶΑ︴АЯаяāɡㄅㄩ─╋︵﹄︻︱︳︴ⅰⅹɑɡ〇〾⿻⺁䜣€" + ); + } catch (error) { + fail(error); + } + }).timeout(5000); + }); + + describe("putMbcs", () => { + it("should send an UTF8 string in the request body", async () => { + const client = new BodyStringClient(); + const utf8String = + "啊齄丂狛狜隣郎隣兀﨩ˊ〞〡¦℡㈱‐ー﹡﹢﹫、〓ⅰⅹ⒈€㈠㈩ⅠⅫ! ̄ぁんァヶΑ︴АЯаяāɡㄅㄩ─╋︵﹄︻︱︳︴ⅰⅹɑɡ〇〾⿻⺁䜣€"; + try { + await client.string.putMbcs(utf8String); + ok(true, "Operation executed with no errors"); + } catch (error) { + fail(error); + } + }).timeout(5000); + }); +}); diff --git a/test/unit/operationTransforms.spec.ts b/test/unit/operationTransforms.spec.ts index 30ba5f514c..f252c75f2f 100644 --- a/test/unit/operationTransforms.spec.ts +++ b/test/unit/operationTransforms.spec.ts @@ -1,7 +1,8 @@ import * as assert from "assert"; import { transformOperationSpec, - getSpecType + getSpecType, + extractRequest } from "../../src/operationTransforms"; import { Operation, @@ -10,7 +11,8 @@ import { Schema, ChoiceValue, ChoiceSchema, - ObjectSchema + ObjectSchema, + ParameterLocation } from "@azure-tools/codemodel"; import { KnownMediaType } from "@azure-tools/codegen"; import { OperationSpec } from "@azure/core-http"; @@ -157,10 +159,6 @@ describe("OperationTransforms", () => { const operationSpec = transformOperationSpec(mockOperation); checkHttpMethodAndPath(operationSpec); const okResponse = operationSpec.responses[200]; - // assert.strictEqual( - // okResponse.bodyMapper!.serializedName, - // "parsedResponse" - // ); assert.deepEqual(okResponse.bodyMapper!.type, { name: "String" }); assert.deepEqual( operationSpec.responses.default.bodyMapper, @@ -174,10 +172,6 @@ describe("OperationTransforms", () => { const operationSpec = transformOperationSpec(mockOperation); checkHttpMethodAndPath(operationSpec); const okResponse = operationSpec.responses[200]; - // assert.strictEqual( - // okResponse.bodyMapper!.serializedName, - // "parsedResponse" - // ); assert.deepEqual(okResponse.bodyMapper!.type, { name: "Enum", allowedValues: ["red color", "green-color", "blue_color"] @@ -189,4 +183,15 @@ describe("OperationTransforms", () => { }); }); }); + + describe("extractRequest", () => { + it("should extract request with expected parameterPath", () => { + const request = extractRequest({ + request: { + parameters: [{ location: ParameterLocation.Body, name: "stringBody" }] + } + } as any); + assert.deepEqual(request!.parameterPath, "stringBody"); + }); + }); }); From 7d357c9e26c178bdf2a2cb76c51758621ff7f226 Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Mon, 25 Nov 2019 17:43:57 -0800 Subject: [PATCH 04/10] Refactor to use OperationDetails instead of raw Operation --- package.json | 4 +- src/generators/operationGenerator.ts | 20 +++-- src/models/clientDetails.ts | 2 + src/models/modelDetails.ts | 9 +++ src/models/operationDetails.ts | 1 + src/operationTransforms.ts | 77 ++++++++++-------- src/transforms.ts | 104 +++--------------------- src/typescriptGenerator.ts | 2 +- src/utils/languageHelpers.ts | 5 ++ src/utils/nameUtils.ts | 8 +- src/utils/schemaHelpers.ts | 47 +++++++++++ src/utils/valueHelpers.ts | 19 +++++ test/unit/operationTransforms.spec.ts | 26 ++++-- test/unit/transforms.spec.ts | 112 +------------------------- test/unit/utils/schemaHelpers.spec.ts | 79 ++++++++++++++++++ test/unit/utils/valueHelpers.spec.ts | 50 ++++++++++++ 16 files changed, 301 insertions(+), 264 deletions(-) create mode 100644 src/utils/languageHelpers.ts create mode 100644 src/utils/schemaHelpers.ts create mode 100644 src/utils/valueHelpers.ts create mode 100644 test/unit/utils/schemaHelpers.spec.ts create mode 100644 test/unit/utils/valueHelpers.spec.ts diff --git a/package.json b/package.json index 7d0f5211c7..7e2eb1d6c4 100755 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "unit-test": "mocha -r ts-node/register './test/unit/**/*spec.ts'", "integration-test": "ts-node test/utils/start-server.ts & mocha -r ts-node/register './test/integration/**/*spec.ts' & stop-autorest-testserver", "debug": "node --inspect-brk ./dist/src/main.js", - "generate-bodystring": "npm run build && autorest-beta --typescript --use=/Users/joheredi/Projects/Azure/autorest.typescript.v3 --title=BodyString --input-file=node_modules/@autorest/test-server/__files/swagger/body-string.json --package-name=bodyString --package-version=1.0.0-preview1" + "generate-bodystring": "npm run build && autorest-beta --typescript --use=/Users/joheredi/Projects/Azure/autorest.typescript.v3 --title=BodyString --input-file=node_modules/@autorest/test-server/__files/swagger/body-string.json --package-name=bodyString --package-version=1.0.0-preview1", + "generate-bodycomplex": "npm run build && autorest-beta --typescript --use=/Users/joheredi/Projects/Azure/autorest.typescript.v3 --title=BodyComplexClient --input-file=node_modules/@autorest/test-server/__files/swagger/body-complex.json --package-name=bodyString --package-version=1.0.0-preview1" + }, "dependencies": { "@autorest/autorest": "^3.0.6122", diff --git a/src/generators/operationGenerator.ts b/src/generators/operationGenerator.ts index 4954dc7093..3224a5edd3 100644 --- a/src/generators/operationGenerator.ts +++ b/src/generators/operationGenerator.ts @@ -16,7 +16,6 @@ import { MethodDeclarationOverloadStructure, ExportDeclarationStructure } from "ts-morph"; -import { getLanguageMetadata } from "../transforms"; import { normalizeName, NameType } from "../utils/nameUtils"; import { ClientDetails } from "../models/clientDetails"; import { @@ -27,15 +26,13 @@ import { OperationSpec } from "@azure/core-http"; import { OperationGroupDetails } from "../models/operationDetails"; export function generateOperations( - codeModel: CodeModel, clientDetails: ClientDetails, project: Project ) { let fileNames: string[] = []; - codeModel.operationGroups.forEach(operationGroup => { - const operationDetails = transformOperationGroup(operationGroup); + clientDetails.operationGroups.forEach(operationDetails => { fileNames.push(normalizeName(operationDetails.name, NameType.File)); - generateOperation(operationDetails, operationGroup, clientDetails, project); + generateOperation(operationDetails, clientDetails, project); }); const operationIndexFile = project.createSourceFile( @@ -55,7 +52,6 @@ export function generateOperations( function generateOperation( operationGroupDetails: OperationGroupDetails, - operationGroup: OperationGroup, clientDetails: ClientDetails, project: Project ) { @@ -69,10 +65,13 @@ function generateOperation( addImports(operationGroupFile, clientDetails); addClass(operationGroupFile, operationGroupDetails, clientDetails); - addOperationSchemas(operationGroup, operationGroupFile); + addOperationSpecs(operationGroupDetails, operationGroupFile); } -function addOperationSchemas(operationGroup: OperationGroup, file: SourceFile) { +function addOperationSpecs( + operationGroupDetails: OperationGroupDetails, + file: SourceFile +) { file.addVariableStatement({ declarationKind: VariableDeclarationKind.Const, declarations: [ @@ -83,9 +82,8 @@ function addOperationSchemas(operationGroup: OperationGroup, file: SourceFile) { ] }); - operationGroup.operations.forEach(operation => { - const metadata = getLanguageMetadata(operation.language); - const operationName = normalizeName(metadata.name, NameType.Property); + operationGroupDetails.operations.forEach(operation => { + const operationName = normalizeName(operation.name, NameType.Property); const operationSpec = transformOperationSpec(operation); file.addVariableStatement({ declarationKind: VariableDeclarationKind.Const, diff --git a/src/models/clientDetails.ts b/src/models/clientDetails.ts index 8f3f9ed4fe..3c7d2f69da 100644 --- a/src/models/clientDetails.ts +++ b/src/models/clientDetails.ts @@ -3,6 +3,7 @@ import { ModelDetails } from "./modelDetails"; import { UnionDetails } from "./unionDetails"; +import { OperationGroupDetails } from "./operationDetails"; export interface ClientDetails { name: string; @@ -11,4 +12,5 @@ export interface ClientDetails { sourceFileName: string; models: ModelDetails[]; unions: UnionDetails[]; + operationGroups: OperationGroupDetails[]; } diff --git a/src/models/modelDetails.ts b/src/models/modelDetails.ts index 37d12e84cc..5db020a61d 100644 --- a/src/models/modelDetails.ts +++ b/src/models/modelDetails.ts @@ -15,6 +15,15 @@ export interface PropertyDetails { isConstant: boolean; } +/** + * Details of a property's type + */ +export interface PropertyTypeDetails { + typeName: string; + isConstant: boolean; + defaultValue?: string; +} + /** * Details of a model, transformed from ObjectSchema. */ diff --git a/src/models/operationDetails.ts b/src/models/operationDetails.ts index 1c9a2e4f41..8de3a0a889 100644 --- a/src/models/operationDetails.ts +++ b/src/models/operationDetails.ts @@ -34,6 +34,7 @@ export interface OperationResponseDetails { statusCodes: string[]; // Can be a status code number or "default" modelType?: string; // Could be a primitive or actual model type mediaType?: KnownMediaType; + bodyMapper?: Mapper | string; } /** diff --git a/src/operationTransforms.ts b/src/operationTransforms.ts index c97bd1898d..4387e6aed4 100644 --- a/src/operationTransforms.ts +++ b/src/operationTransforms.ts @@ -2,7 +2,8 @@ import { OperationSpec, Serializer, HttpMethods, - OperationResponse + OperationResponse, + Mapper } from "@azure/core-http"; import { Operation, @@ -16,7 +17,6 @@ import { ParameterLocation, Parameter } from "@azure-tools/codemodel"; -import { getLanguageMetadata, getTypeForSchema } from "./transforms"; import { normalizeName, NameType } from "./utils/nameUtils"; import { OperationGroupDetails, @@ -25,27 +25,24 @@ import { OperationRequestDetails, OperationRequestParameterDetails } from "./models/operationDetails"; +import { getLanguageMetadata } from "./utils/languageHelpers"; +import { getTypeForSchema } from "./utils/schemaHelpers"; -export function transformOperationSpec(operation: Operation): OperationSpec { +export function transformOperationSpec( + operationDetails: OperationDetails +): OperationSpec { // Extract protocol information - const httpInfo = extractHttpDetails(operation.request); + const httpInfo = extractHttpDetails(operationDetails.request); const serializer = new Serializer(); - const operationDetails = transformOperation(operation); - return { ...httpInfo, - responses: extractResponses(operation.responses, operation.exceptions), + responses: extractResponses(operationDetails.responses), requestBody: extractRequest(operationDetails) as any, serializer }; } -export function extractHttpDetails({ protocol }: Request) { - if (!protocol.http) { - throw new Error("operation doesn't contain a definition for HTTP protocol"); - } - const { path, method } = protocol.http; - +export function extractHttpDetails({ path, method }: OperationRequestDetails) { return { path: path.replace("{$host}/", ""), httpMethod: method.toUpperCase() as HttpMethods @@ -54,19 +51,14 @@ export function extractHttpDetails({ protocol }: Request) { export type OperationResponses = { [responseCode: string]: OperationResponse }; export function extractResponses( - responses?: Array, - exceptions: Array = [] + responses?: OperationResponseDetails[] ): OperationResponses { if (!responses || !responses.length) { return {}; } const schemaResponses = extractSchemaResponses(responses); - const defaultResponse = extractSchemaResponses(exceptions); - return { - ...schemaResponses, - ...defaultResponse - }; + return schemaResponses; } export function extractRequest(operationDetails: OperationDetails) { @@ -125,16 +117,10 @@ export function extractBasicResponses( return (responses as any[]).filter((r: SchemaResponse) => !r.schema); } -export function extractSchemaResponses( - responses: Array -) { +export function extractSchemaResponses(responses: OperationResponseDetails[]) { return responses.reduce( - (result: OperationResponses, response: SchemaResponse | Response) => { - if (!response.protocol.http) { - return result; - } - - const statusCodes = response.protocol.http.statusCodes; + (result: OperationResponses, response: OperationResponseDetails) => { + const statusCodes = response.statusCodes; if (!statusCodes || !statusCodes.length) { return result; @@ -142,10 +128,9 @@ export function extractSchemaResponses( const statusCode = statusCodes[0]; result[statusCode] = {}; - const schemaResponse = response as any; - if (schemaResponse.schema) { + if (response.bodyMapper) { result[statusCode] = { - bodyMapper: getBodyMapperFromSchema(schemaResponse.schema) + bodyMapper: response.bodyMapper as any }; } return result; @@ -203,16 +188,19 @@ export function transformOperationResponse( response: Response ): OperationResponseDetails { let modelType: string | undefined = undefined; + let bodyMapper: Mapper | string | undefined = undefined; if ((response as any).schema) { const schemaResponse = response as SchemaResponse; modelType = getTypeForSchema(schemaResponse.schema).typeName; + bodyMapper = getBodyMapperFromSchema(schemaResponse.schema); } if (response.protocol.http) { return { statusCodes: response.protocol.http.statusCodes, mediaType: response.protocol.http.knownMediaType, - modelType + modelType, + bodyMapper }; } else { throw new Error("Operation does not specify HTTP response details."); @@ -221,7 +209,6 @@ export function transformOperationResponse( export function transformOperation(operation: Operation): OperationDetails { const metadata = getLanguageMetadata(operation.language); - operation.responses; return { name: normalizeName(metadata.name, NameType.Property), apiVersions: operation.apiVersions @@ -229,10 +216,30 @@ export function transformOperation(operation: Operation): OperationDetails { : [], description: metadata.description, request: transformOperationRequest(operation.request), - responses: [] + responses: mergeResponsesAndExceptions(operation) }; } +function mergeResponsesAndExceptions(operation: Operation) { + let responses: OperationResponseDetails[] = []; + + if (operation.responses) { + responses = [ + ...(responses || []), + ...operation.responses.map(transformOperationResponse) + ]; + } + + if (operation.exceptions) { + responses = [ + ...(responses || []), + ...operation.exceptions.map(transformOperationResponse) + ]; + } + + return responses; +} + export function transformOperationGroup( operationGroup: OperationGroup ): OperationGroupDetails { diff --git a/src/transforms.ts b/src/transforms.ts index 6517167036..da4d6161fc 100644 --- a/src/transforms.ts +++ b/src/transforms.ts @@ -8,91 +8,14 @@ import { ModelDetails, PropertyDetails } from "./models/modelDetails"; import { CodeModel, ObjectSchema, - Languages, - Language, - Schema, - SchemaType, Property, - ChoiceSchema, - ValueSchema, - ConstantSchema + ChoiceSchema } from "@azure-tools/codemodel"; -import { normalizeName, NameType } from "./utils/nameUtils"; - -// An array of model names that are "reserved", or shouldn't be used -// verbatim. Currently any reserved model name will have "Model" -// appended to it in the generated code. -const ReservedModelNames = ["Error"]; - -export function getLanguageMetadata(languages: Languages): Language { - return languages.typescript || languages.javascript || languages.default; -} - -export interface PropertyTypeDetails { - typeName: string; - isConstant: boolean; - defaultValue?: string; -} - -export function getStringForValue( - value: any, - valueType: ValueSchema, - quotedStrings = true -): string { - switch (valueType.type) { - case SchemaType.String: - return quotedStrings ? `"${value}"` : `${value}`; - case SchemaType.Number: - case SchemaType.Integer: - return value.toString(); - case SchemaType.Boolean: - return value.toString(); - default: - throw new Error(`Unexpected value type: ${valueType.type}`); - } -} - -export function getTypeForSchema(schema: Schema): PropertyTypeDetails { - let typeName: string = ""; - let defaultValue: string | undefined = undefined; - - switch (schema.type) { - case SchemaType.String: - typeName = "string"; - break; - case SchemaType.Number: - case SchemaType.Integer: - typeName = "number"; - break; - case SchemaType.Constant: - const constantSchema = schema as ConstantSchema; - const constantType = getTypeForSchema(constantSchema.valueType); - typeName = constantType.typeName; - defaultValue = getStringForValue( - constantSchema.value.value, - constantSchema.valueType, - false - ); - break; - case SchemaType.ByteArray: - typeName = "Int8Array"; - break; - case SchemaType.Choice: - case SchemaType.SealedChoice: - case SchemaType.Object: - const { name } = getLanguageMetadata(schema.language); - typeName = name; - break; - default: - throw new Error(`Unsupported schema type: ${schema.type}`); - } - - return { - typeName, - isConstant: schema.type === SchemaType.Constant, - defaultValue - }; -} +import { normalizeName, NameType, guardReservedNames } from "./utils/nameUtils"; +import { getTypeForSchema } from "./utils/schemaHelpers"; +import { getStringForValue } from "./utils/valueHelpers"; +import { getLanguageMetadata } from "./utils/languageHelpers"; +import { transformOperationGroup } from "./operationTransforms"; export function transformProperty(property: Property): PropertyDetails { const metadata = getLanguageMetadata(property.language); @@ -116,10 +39,7 @@ export function transformProperty(property: Property): PropertyDetails { export function transformChoice(choice: ChoiceSchema): UnionDetails { const metadata = getLanguageMetadata(choice.language); - let name = - ReservedModelNames.indexOf(metadata.name) > -1 - ? `${metadata.name}Model` - : metadata.name; + let name = guardReservedNames(metadata.name); return { name, @@ -133,12 +53,7 @@ export function transformChoice(choice: ChoiceSchema): UnionDetails { export function transformObject(obj: ObjectSchema): ModelDetails { const metadata = getLanguageMetadata(obj.language); - let name = normalizeName( - ReservedModelNames.indexOf(metadata.name) > -1 - ? `${metadata.name}Model` - : metadata.name, - NameType.Class - ); + let name = normalizeName(guardReservedNames(metadata.name), NameType.Class); return { name, @@ -162,6 +77,7 @@ export function transformCodeModel(codeModel: CodeModel): ClientDetails { : [], unions: codeModel.schemas.choices ? codeModel.schemas.choices.map(transformChoice) - : [] + : [], + operationGroups: codeModel.operationGroups.map(transformOperationGroup) }; } diff --git a/src/typescriptGenerator.ts b/src/typescriptGenerator.ts index 21e6e76bf4..6f20b004ee 100755 --- a/src/typescriptGenerator.ts +++ b/src/typescriptGenerator.ts @@ -78,7 +78,7 @@ export class TypescriptGenerator { generateClientContext(clientDetails, packageDetails, project); generateModels(this.codeModel, project); generateMappers(this.codeModel, project); - generateOperations(this.codeModel, clientDetails, project); + generateOperations(clientDetails, project); // TODO: Get this from the "license-header" setting: // await this.host.GetValue("license-header"); diff --git a/src/utils/languageHelpers.ts b/src/utils/languageHelpers.ts new file mode 100644 index 0000000000..9058cf9dbd --- /dev/null +++ b/src/utils/languageHelpers.ts @@ -0,0 +1,5 @@ +import { Languages, Language } from "@azure-tools/codemodel"; + +export function getLanguageMetadata(languages: Languages): Language { + return languages.typescript || languages.javascript || languages.default; +} diff --git a/src/utils/nameUtils.ts b/src/utils/nameUtils.ts index 43d2a434da..b49a1fadd7 100644 --- a/src/utils/nameUtils.ts +++ b/src/utils/nameUtils.ts @@ -13,6 +13,10 @@ export enum NameType { File } +export function guardReservedNames(name: string): string { + return ReservedModelNames.indexOf(name) > -1 ? `${name}Model` : name; +} + export function normalizeName(name: string, nameType: NameType): string { const casingConvention = getCasingConvention(nameType); @@ -24,9 +28,7 @@ export function normalizeName(name: string, nameType: NameType): string { .join(""); const normalized = `${normalizedFirstPart}${normalizedParts}`; - return ReservedModelNames.indexOf(normalized) > -1 - ? `${normalized}Model` - : normalized; + return guardReservedNames(normalized); } export function getModelsName(title: string): string { diff --git a/src/utils/schemaHelpers.ts b/src/utils/schemaHelpers.ts new file mode 100644 index 0000000000..f820258565 --- /dev/null +++ b/src/utils/schemaHelpers.ts @@ -0,0 +1,47 @@ +import { PropertyTypeDetails } from "../models/modelDetails"; + +import { Schema, SchemaType, ConstantSchema } from "@azure-tools/codemodel"; +import { getStringForValue } from "./valueHelpers"; +import { getLanguageMetadata } from "./languageHelpers"; + +export function getTypeForSchema(schema: Schema): PropertyTypeDetails { + let typeName: string = ""; + let defaultValue: string | undefined = undefined; + + switch (schema.type) { + case SchemaType.String: + typeName = "string"; + break; + case SchemaType.Number: + case SchemaType.Integer: + typeName = "number"; + break; + case SchemaType.Constant: + const constantSchema = schema as ConstantSchema; + const constantType = getTypeForSchema(constantSchema.valueType); + typeName = constantType.typeName; + defaultValue = getStringForValue( + constantSchema.value.value, + constantSchema.valueType, + false + ); + break; + case SchemaType.ByteArray: + typeName = "Int8Array"; + break; + case SchemaType.Choice: + case SchemaType.SealedChoice: + case SchemaType.Object: + const { name } = getLanguageMetadata(schema.language); + typeName = name; + break; + default: + throw new Error(`Unsupported schema type: ${schema.type}`); + } + + return { + typeName, + isConstant: schema.type === SchemaType.Constant, + defaultValue + }; +} diff --git a/src/utils/valueHelpers.ts b/src/utils/valueHelpers.ts new file mode 100644 index 0000000000..e79283cace --- /dev/null +++ b/src/utils/valueHelpers.ts @@ -0,0 +1,19 @@ +import { ValueSchema, SchemaType } from "@azure-tools/codemodel"; + +export function getStringForValue( + value: any, + valueType: ValueSchema, + quotedStrings = true +): string { + switch (valueType.type) { + case SchemaType.String: + return quotedStrings ? `"${value}"` : `${value}`; + case SchemaType.Number: + case SchemaType.Integer: + return value.toString(); + case SchemaType.Boolean: + return value.toString(); + default: + throw new Error(`Unexpected value type: ${valueType.type}`); + } +} diff --git a/test/unit/operationTransforms.spec.ts b/test/unit/operationTransforms.spec.ts index f252c75f2f..b0934c3e0a 100644 --- a/test/unit/operationTransforms.spec.ts +++ b/test/unit/operationTransforms.spec.ts @@ -2,7 +2,8 @@ import * as assert from "assert"; import { transformOperationSpec, getSpecType, - extractRequest + extractRequest, + transformOperation } from "../../src/operationTransforms"; import { Operation, @@ -12,7 +13,9 @@ import { ChoiceValue, ChoiceSchema, ObjectSchema, - ParameterLocation + ParameterLocation, + ConstantSchema, + ConstantValue } from "@azure-tools/codemodel"; import { KnownMediaType } from "@azure-tools/codegen"; import { OperationSpec } from "@azure/core-http"; @@ -25,10 +28,13 @@ const choice = new ChoiceSchema("mockChoice", "", { ] }); -const constantSchema = new Schema( +const constantSchema = new ConstantSchema( "paths·string-null·get·responses·200·content·application-json·schema", "", - SchemaType.Constant + { + value: new ConstantValue("Some constant value"), + valueType: { type: SchemaType.String } + } ); const objectSchema = new ObjectSchema("RefColorConstant", "", {}); @@ -142,13 +148,15 @@ describe("OperationTransforms", () => { it("should create an operation spec with correct http details", () => { const okResponseSchema = get200ResponseSchema(constantSchema); const mockOperation = getOperation(okResponseSchema); - const operationSpec = transformOperationSpec(mockOperation); + const operationDetails = transformOperation(mockOperation); + const operationSpec = transformOperationSpec(operationDetails); checkHttpMethodAndPath(operationSpec); }); it("should create an operation spec with correct responses from a basic response", () => { const mockOperation = getOperation(); - const operationSpec = transformOperationSpec(mockOperation); + const operationDetails = transformOperation(mockOperation); + const operationSpec = transformOperationSpec(operationDetails); checkHttpMethodAndPath(operationSpec); assert.deepEqual(operationSpec.responses[200], {}); }); @@ -156,7 +164,8 @@ describe("OperationTransforms", () => { it("should create an operation spec with correct responses spec and cosntant schema response", () => { const okResponseSchema = get200ResponseSchema(constantSchema); const mockOperation = getOperation(okResponseSchema); - const operationSpec = transformOperationSpec(mockOperation); + const operationDetails = transformOperation(mockOperation); + const operationSpec = transformOperationSpec(operationDetails); checkHttpMethodAndPath(operationSpec); const okResponse = operationSpec.responses[200]; assert.deepEqual(okResponse.bodyMapper!.type, { name: "String" }); @@ -169,7 +178,8 @@ describe("OperationTransforms", () => { it("should create an operation spec with correct responses spec and choice schema response", () => { const okResponseSchema = get200ResponseSchema(choice); const mockOperation = getOperation(okResponseSchema); - const operationSpec = transformOperationSpec(mockOperation); + const operationDetails = transformOperation(mockOperation); + const operationSpec = transformOperationSpec(operationDetails); checkHttpMethodAndPath(operationSpec); const okResponse = operationSpec.responses[200]; assert.deepEqual(okResponse.bodyMapper!.type, { diff --git a/test/unit/transforms.spec.ts b/test/unit/transforms.spec.ts index 523f9c5b6b..c1260de8c8 100644 --- a/test/unit/transforms.spec.ts +++ b/test/unit/transforms.spec.ts @@ -3,9 +3,7 @@ import * as assert from "assert"; import { transformObject, transformProperty, - transformChoice, - getTypeForSchema, - getStringForValue + transformChoice } from "../../src/transforms"; import { @@ -57,74 +55,6 @@ const fakeCodeModel: CodeModel = new CodeModel("FakeModel", false, { }); describe("Transforms", () => { - describe("Schema to PropertyTypeDetails", () => { - it("converts StringSchema to string", () => { - const typeDetails = getTypeForSchema( - new StringSchema("StringType", "This is a string.") - ); - - assert.deepEqual(typeDetails, { - typeName: "string", - isConstant: false, - defaultValue: undefined - }); - }); - - it("converts NumberSchema of Number type to number", () => { - let typeDetails = getTypeForSchema( - new NumberSchema( - "NumberType", - "This is a number.", - SchemaType.Integer, - 32 - ) - ); - - assert.deepEqual(typeDetails, { - typeName: "number", - isConstant: false, - defaultValue: undefined - }); - }); - - it("converts NumberSchema of Integer type to number", () => { - let typeDetails = getTypeForSchema( - new NumberSchema( - "NumberType", - "This is a number.", - SchemaType.Number, - 32 - ) - ); - - assert.deepEqual(typeDetails, { - typeName: "number", - isConstant: false, - defaultValue: undefined - }); - }); - - it("converts ConstantSchema to the underlying type", () => { - let typeDetails = getTypeForSchema( - new ConstantSchema("ConstantNumber", "This is a constant number", { - value: new ConstantValue(311), - valueType: new NumberSchema( - "NumberType", - "This is a number.", - SchemaType.Number, - 32 - ) - }) - ); - - assert.deepEqual(typeDetails, { - typeName: "number", - isConstant: true, - defaultValue: 311 - }); - }); - }); - describe("Property to PropertyDetails", () => { it("retains basic details", () => { const property = transformProperty( @@ -173,44 +103,4 @@ describe("Transforms", () => { assert.deepEqual(colorUnion.values, [`"red"`, `"green"`, `"blue"`]); }); }); - - describe("Value to string", () => { - it("converts a string value to a quoted string", () => { - assert.strictEqual( - getStringForValue( - "red", - new StringSchema("ColorString", "A color string.") - ), - `"red"` - ); - }); - - it("converts a string value to a non-quoted string", () => { - assert.strictEqual( - getStringForValue( - "red", - new StringSchema("ColorString", "A color string."), - false - ), - "red" - ); - }); - - it("converts a numeric value to a plain string", () => { - assert.strictEqual( - getStringForValue( - 1, - new NumberSchema("Número", "El número.", SchemaType.Number, 32) - ), - `1` - ); - }); - - it("converts a boolean value to a plain string", () => { - assert.strictEqual( - getStringForValue(true, new BooleanSchema("Truth", "The truth.")), - `true` - ); - }); - }); }); diff --git a/test/unit/utils/schemaHelpers.spec.ts b/test/unit/utils/schemaHelpers.spec.ts new file mode 100644 index 0000000000..41b6cf211f --- /dev/null +++ b/test/unit/utils/schemaHelpers.spec.ts @@ -0,0 +1,79 @@ +import * as assert from "assert"; +import { + StringSchema, + NumberSchema, + SchemaType, + ConstantSchema, + ConstantValue +} from "@azure-tools/codemodel"; +import { getTypeForSchema } from "../../../src/utils/schemaHelpers"; + +describe("SchemaHelpers", () => { + describe("getTypeForSchema", () => { + it("converts StringSchema to string", () => { + const typeDetails = getTypeForSchema( + new StringSchema("StringType", "This is a string.") + ); + + assert.deepEqual(typeDetails, { + typeName: "string", + isConstant: false, + defaultValue: undefined + }); + }); + + it("converts NumberSchema of Number type to number", () => { + let typeDetails = getTypeForSchema( + new NumberSchema( + "NumberType", + "This is a number.", + SchemaType.Integer, + 32 + ) + ); + + assert.deepEqual(typeDetails, { + typeName: "number", + isConstant: false, + defaultValue: undefined + }); + }); + + it("converts NumberSchema of Integer type to number", () => { + let typeDetails = getTypeForSchema( + new NumberSchema( + "NumberType", + "This is a number.", + SchemaType.Number, + 32 + ) + ); + + assert.deepEqual(typeDetails, { + typeName: "number", + isConstant: false, + defaultValue: undefined + }); + }); + + it("converts ConstantSchema to the underlying type", () => { + let typeDetails = getTypeForSchema( + new ConstantSchema("ConstantNumber", "This is a constant number", { + value: new ConstantValue(311), + valueType: new NumberSchema( + "NumberType", + "This is a number.", + SchemaType.Number, + 32 + ) + }) + ); + + assert.deepEqual(typeDetails, { + typeName: "number", + isConstant: true, + defaultValue: 311 + }); + }); + }); +}); diff --git a/test/unit/utils/valueHelpers.spec.ts b/test/unit/utils/valueHelpers.spec.ts new file mode 100644 index 0000000000..69b8d18221 --- /dev/null +++ b/test/unit/utils/valueHelpers.spec.ts @@ -0,0 +1,50 @@ +import { getStringForValue } from "../../../src/utils/valueHelpers"; +import { + BooleanSchema, + SchemaType, + NumberSchema, + StringSchema +} from "@azure-tools/codemodel"; +import * as assert from "assert"; + +describe("ValueHelpers", () => { + describe("getStringForValue", () => { + it("converts a string value to a quoted string", () => { + assert.strictEqual( + getStringForValue( + "red", + new StringSchema("ColorString", "A color string.") + ), + `"red"` + ); + }); + + it("converts a string value to a non-quoted string", () => { + assert.strictEqual( + getStringForValue( + "red", + new StringSchema("ColorString", "A color string."), + false + ), + "red" + ); + }); + + it("converts a numeric value to a plain string", () => { + assert.strictEqual( + getStringForValue( + 1, + new NumberSchema("Número", "El número.", SchemaType.Number, 32) + ), + `1` + ); + }); + + it("converts a boolean value to a plain string", () => { + assert.strictEqual( + getStringForValue(true, new BooleanSchema("Truth", "The truth.")), + `true` + ); + }); + }); +}); From 36e6000688bab74793ecec16a82d6dd62254303c Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Tue, 26 Nov 2019 13:12:23 -0800 Subject: [PATCH 05/10] Improve generated JSDoc and cleanup anys --- src/generators/operationGenerator.ts | 236 ++++++++++++++++---------- src/models/operationDetails.ts | 23 +++ src/operationTransforms.ts | 107 ++++++------ test/unit/operationTransforms.spec.ts | 21 ++- 4 files changed, 232 insertions(+), 155 deletions(-) diff --git a/src/generators/operationGenerator.ts b/src/generators/operationGenerator.ts index 3224a5edd3..eda0e4d130 100644 --- a/src/generators/operationGenerator.ts +++ b/src/generators/operationGenerator.ts @@ -1,9 +1,4 @@ -import { - CodeModel, - OperationGroup, - Language, - ParameterLocation -} from "@azure-tools/codemodel"; +import { ParameterLocation } from "@azure-tools/codemodel"; import { Project, SourceFile, @@ -11,24 +6,31 @@ import { Scope, ClassDeclaration, ParameterDeclarationStructure, - StructureKind, OptionalKind, - MethodDeclarationOverloadStructure, ExportDeclarationStructure } from "ts-morph"; import { normalizeName, NameType } from "../utils/nameUtils"; import { ClientDetails } from "../models/clientDetails"; +import { transformOperationSpec } from "../operationTransforms"; +import { Mapper } from "@azure/core-http"; import { - transformOperationSpec, - transformOperationGroup -} from "../operationTransforms"; -import { OperationSpec } from "@azure/core-http"; -import { OperationGroupDetails } from "../models/operationDetails"; + OperationGroupDetails, + OperationSpecDetails +} from "../models/operationDetails"; +/** + * Function that writes the code for all the operations. + * It will generate one file per operation group and each file contains: + * - A class definition for the operation group + * - Methods and overrides for each operation + * - OperationSpecs for each operation + * @param clientDetails client details + * @param project project for code generation + */ export function generateOperations( clientDetails: ClientDetails, project: Project -) { +): void { let fileNames: string[] = []; clientDetails.operationGroups.forEach(operationDetails => { fileNames.push(normalizeName(operationDetails.name, NameType.File)); @@ -50,11 +52,14 @@ export function generateOperations( ); } +/** + * This function creates a file for a given Operation Group + */ function generateOperation( operationGroupDetails: OperationGroupDetails, clientDetails: ClientDetails, project: Project -) { +): void { const name = normalizeName(operationGroupDetails.name, NameType.File); const operationGroupFile = project.createSourceFile( @@ -68,37 +73,11 @@ function generateOperation( addOperationSpecs(operationGroupDetails, operationGroupFile); } -function addOperationSpecs( - operationGroupDetails: OperationGroupDetails, - file: SourceFile -) { - file.addVariableStatement({ - declarationKind: VariableDeclarationKind.Const, - declarations: [ - { - name: "serializer", - initializer: "new coreHttp.Serializer(Mappers);" - } - ] - }); - - operationGroupDetails.operations.forEach(operation => { - const operationName = normalizeName(operation.name, NameType.Property); - const operationSpec = transformOperationSpec(operation); - file.addVariableStatement({ - declarationKind: VariableDeclarationKind.Const, - declarations: [ - { - name: `${operationName}OperationSpec`, - type: "coreHttp.OperationSpec", - initializer: buildSpec(operationSpec) - } - ] - }); - }); -} - -function buildSpec(spec: OperationSpec) { +/** + * Generates a string representation of an Operation spec + * the output is to be inserted in the Operation group file + */ +function buildSpec(spec: OperationSpecDetails): string { const responses = buildResponses(spec); const requestBody = buildRequestBody(spec); return `{ @@ -110,12 +89,18 @@ function buildSpec(spec: OperationSpec) { }`; } -function buildRequestBody({ requestBody }: OperationSpec) { +/** + * This function transforms the requestBody of OperationSpecDetails into its string representation + * to insert in generated files + */ +function buildRequestBody({ requestBody }: OperationSpecDetails): string { if (!requestBody) { return ""; } - const mapper = !requestBody.mapper.type + // If requestBody mapper is a string it is just a reference to an existing mapper in + // the generated mappers file, so just use the string, otherwise stringify the actual mapper + const mapper = !(requestBody.mapper as Mapper).type ? requestBody.mapper : JSON.stringify(requestBody.mapper); @@ -125,29 +110,53 @@ function buildRequestBody({ requestBody }: OperationSpec) { },`; } -function buildResponses({ responses }: OperationSpec) { +/** + * This function transforms the responses of OperationSpecDetails into their string representation + * to insert in generated files + */ +function buildResponses({ responses }: OperationSpecDetails): string[] { const responseCodes = Object.keys(responses); let parsedResponses: string[] = []; responseCodes.forEach(code => { + // Check whether we have an actual mapper or a string reference if ( responses[code] && responses[code].bodyMapper && - (responses[code].bodyMapper as any).indexOf && - (responses[code].bodyMapper as any).indexOf("Mappers") > -1 + (responses[code].bodyMapper as Mapper).type ) { - // Complex + parsedResponses.push(`${code}: ${JSON.stringify(responses[code])}`); + } else { + // Mapper is a refference to an existing mapper in the Mappers file parsedResponses.push(`${code}: { - bodyMapper: ${responses[code].bodyMapper} + bodyMapper: ${responses[code].bodyMapper} }`); - } else { - // Simple - parsedResponses.push(`${code}: ${JSON.stringify(responses[code])}`); } }); return parsedResponses; } +function getOptionsParameter(isOptional = false): ParameterWithDescription { + return { + name: "options", + type: "coreHttp.RequestOptionsBase", + hasQuestionToken: isOptional, + description: "The options parameters." + }; +} + +function getCallbackParameter(isOptional = false): ParameterWithDescription { + return { + name: "callback", + type: "coreHttp.ServiceCallback", // TODO get the real type for callback + hasQuestionToken: isOptional, + description: "The callback." + }; +} + +/** + * Adds an Operation group class to the generated file + */ function addClass( operationGroupFile: SourceFile, operationGroupDetails: OperationGroupDetails, @@ -184,6 +193,14 @@ function addClass( addOperations(operationGroupDetails, operationGroupClass); } +type ParameterWithDescription = OptionalKind< + ParameterDeclarationStructure & { description: string } +>; + +/** + * Add all the required operations whith their overloads, + * extracted from OperationGroupDetails, to the generated file + */ function addOperations( operationGroupDetails: OperationGroupDetails, operationGroupClass: ClassDeclaration @@ -193,7 +210,7 @@ function addOperations( const parameters = operation.request.parameters || []; const params = parameters .filter(param => param.location === ParameterLocation.Body) - .map(param => { + .map(param => { const typeName = param.modelType || "any"; const type = primitiveTypes.indexOf(typeName) > -1 @@ -201,18 +218,28 @@ function addOperations( : `Models.${typeName}`; return { name: param.name, + description: param.description, type, hasQuestionToken: !param.required }; }); + + const allParams = [ + ...params, + getOptionsParameter(true), + getCallbackParameter(true) + ]; + + const optionalOptionsParams = [...params, getOptionsParameter(true)]; + const requiredCallbackParams = [...params, getCallbackParameter(false)]; + const retuiredOptionsAndCallbackParams = [ + ...params, + getOptionsParameter(false), + getCallbackParameter(false) + ]; const operationMethod = operationGroupClass.addMethod({ name: normalizeName(operation.name, NameType.Property), - docs: [operation.description], - parameters: [ - ...params, - getOptionsParameter(true), - getCallbackParameter(true) - ], + parameters: allParams, returnType: "Promise" // TODO: Add correct return type }); @@ -224,49 +251,80 @@ function addOperations( ); operationMethod.addOverloads([ - // Overload with optional options { - parameters: [...params, getOptionsParameter(true)], + parameters: optionalOptionsParams, + docs: [ + generateOperationJSDoc(optionalOptionsParams, operation.description) + ], returnType: "Promise" // TODO: Add correct return type }, - // Overload with required callback { - parameters: [...params, getCallbackParameter(false)], + parameters: requiredCallbackParams, + docs: [generateOperationJSDoc(requiredCallbackParams)], returnType: "void" }, - // Overload with required options and callback { - parameters: [ - ...params, - getOptionsParameter(false), - getCallbackParameter(false) - ], + parameters: retuiredOptionsAndCallbackParams, + docs: [generateOperationJSDoc(retuiredOptionsAndCallbackParams)], returnType: "void" } ]); }); } -function getOptionsParameter( - isOptional = false -): OptionalKind { - return { - name: "options", - type: "coreHttp.RequestOptionsBase", - hasQuestionToken: isOptional - }; +function generateOperationJSDoc( + params: ParameterWithDescription[] = [], + description: string = "" +): string { + const paramJSDoc = + !params || !params.length + ? "" + : params + .map(param => { + return `@param ${param.name} ${param.description}`; + }) + .join("\n"); + + return `${description ? description + "\n" : description}${paramJSDoc}`; } -function getCallbackParameter( - isOptional = false -): OptionalKind { - return { - name: "callback", - type: "coreHttp.ServiceCallback", // TODO get the real type for callback - hasQuestionToken: isOptional - }; +/** + * Generates and inserts into the file the operation specs + * for a given operation group + */ +function addOperationSpecs( + operationGroupDetails: OperationGroupDetails, + file: SourceFile +): void { + file.addVariableStatement({ + declarationKind: VariableDeclarationKind.Const, + declarations: [ + { + name: "serializer", + initializer: "new coreHttp.Serializer(Mappers);" + } + ] + }); + + operationGroupDetails.operations.forEach(operation => { + const operationName = normalizeName(operation.name, NameType.Property); + const operationSpec = transformOperationSpec(operation); + file.addVariableStatement({ + declarationKind: VariableDeclarationKind.Const, + declarations: [ + { + name: `${operationName}OperationSpec`, + type: "coreHttp.OperationSpec", + initializer: buildSpec(operationSpec) + } + ] + }); + }); } +/** + * Adds required imports at the top of the file + */ function addImports( operationGroupFile: SourceFile, { className, sourceFileName }: ClientDetails diff --git a/src/models/operationDetails.ts b/src/models/operationDetails.ts index 8de3a0a889..35fd83e634 100644 --- a/src/models/operationDetails.ts +++ b/src/models/operationDetails.ts @@ -48,6 +48,16 @@ export interface OperationDetails { responses: OperationResponseDetails[]; } +/** + * Details of an operation spec, transformed from OperationSpec. + */ +export interface OperationSpecDetails { + path: string; + httpMethod: string; + responses: OperationSpecResponses; + requestBody?: OperationSpecRequest; +} + /** * Details of an operation group, transformed from OperationGroup. */ @@ -56,3 +66,16 @@ export interface OperationGroupDetails { name: string; operations: OperationDetails[]; } + +export interface OperationSpecResponse { + bodyMapper?: Mapper | string; +} + +export type OperationSpecResponses = { + [responseCode: string]: OperationSpecResponse; +}; + +export type OperationSpecRequest = { + parameterPath: string; + mapper: Mapper | string; +}; diff --git a/src/operationTransforms.ts b/src/operationTransforms.ts index 4387e6aed4..3a7e2b7b15 100644 --- a/src/operationTransforms.ts +++ b/src/operationTransforms.ts @@ -1,10 +1,4 @@ -import { - OperationSpec, - Serializer, - HttpMethods, - OperationResponse, - Mapper -} from "@azure/core-http"; +import { HttpMethods, Mapper, MapperType } from "@azure/core-http"; import { Operation, Request, @@ -23,22 +17,23 @@ import { OperationDetails, OperationResponseDetails, OperationRequestDetails, - OperationRequestParameterDetails + OperationRequestParameterDetails, + OperationSpecDetails, + OperationSpecResponses, + OperationSpecRequest } from "./models/operationDetails"; import { getLanguageMetadata } from "./utils/languageHelpers"; import { getTypeForSchema } from "./utils/schemaHelpers"; export function transformOperationSpec( operationDetails: OperationDetails -): OperationSpec { +): OperationSpecDetails { // Extract protocol information const httpInfo = extractHttpDetails(operationDetails.request); - const serializer = new Serializer(); return { ...httpInfo, - responses: extractResponses(operationDetails.responses), - requestBody: extractRequest(operationDetails) as any, - serializer + responses: extractSpecResponses(operationDetails.responses), + requestBody: extractSpecRequest(operationDetails) }; } @@ -49,11 +44,11 @@ export function extractHttpDetails({ path, method }: OperationRequestDetails) { }; } -export type OperationResponses = { [responseCode: string]: OperationResponse }; -export function extractResponses( +export function extractSpecResponses( responses?: OperationResponseDetails[] -): OperationResponses { +): OperationSpecResponses { if (!responses || !responses.length) { + // Should we throw an error here? return {}; } const schemaResponses = extractSchemaResponses(responses); @@ -61,7 +56,9 @@ export function extractResponses( return schemaResponses; } -export function extractRequest(operationDetails: OperationDetails) { +export function extractSpecRequest( + operationDetails: OperationDetails +): OperationSpecRequest | undefined { const parameters = (operationDetails.request.parameters || []).filter( p => p.location === ParameterLocation.Body ); @@ -77,13 +74,15 @@ export function extractRequest(operationDetails: OperationDetails) { } export interface SpecType { - name: any; - allowedValues?: any[]; + name: string; + allowedValues?: string[]; + reference?: string; } -export function getSpecType(responseSchema: Schema): any { - let typeName: string; +export function getSpecType(responseSchema: Schema): SpecType { + let typeName: string = ""; let allowedValues: any[] | undefined = undefined; + let reference: string | undefined = undefined; switch (responseSchema.type) { case SchemaType.ByteArray: typeName = "Base64Url"; @@ -99,27 +98,23 @@ export function getSpecType(responseSchema: Schema): any { break; case SchemaType.Object: const name = getLanguageMetadata(responseSchema.language).name; - return `Mappers.${normalizeName(name, NameType.Class)}`; + reference = `Mappers.${normalizeName(name, NameType.Class)}`; + break; default: throw new Error(`Unsupported Spec Type ${responseSchema.type}`); } let result = { - name: typeName + name: typeName as any, + reference }; return !!allowedValues ? { ...result, allowedValues } : result; } -export function extractBasicResponses( - responses: Array -) { - return (responses as any[]).filter((r: SchemaResponse) => !r.schema); -} - export function extractSchemaResponses(responses: OperationResponseDetails[]) { return responses.reduce( - (result: OperationResponses, response: OperationResponseDetails) => { + (result: OperationSpecResponses, response: OperationResponseDetails) => { const statusCodes = response.statusCodes; if (!statusCodes || !statusCodes.length) { @@ -130,7 +125,7 @@ export function extractSchemaResponses(responses: OperationResponseDetails[]) { result[statusCode] = {}; if (response.bodyMapper) { result[statusCode] = { - bodyMapper: response.bodyMapper as any + bodyMapper: response.bodyMapper }; } return result; @@ -139,19 +134,6 @@ export function extractSchemaResponses(responses: OperationResponseDetails[]) { ); } -function getBodyMapperFromSchema(responseSchema: Schema) { - const responseType = getSpecType(responseSchema); - return !responseType.name - ? responseType - : { - type: responseType - }; -} - -/** - * Operation Details - */ - export function transformOperationRequestParameter( parameter: Parameter ): OperationRequestParameterDetails { @@ -185,11 +167,12 @@ export function transformOperationRequest( } export function transformOperationResponse( - response: Response + response: Response | SchemaResponse ): OperationResponseDetails { let modelType: string | undefined = undefined; let bodyMapper: Mapper | string | undefined = undefined; - if ((response as any).schema) { + + if ((response as SchemaResponse).schema) { const schemaResponse = response as SchemaResponse; modelType = getTypeForSchema(schemaResponse.schema).typeName; bodyMapper = getBodyMapperFromSchema(schemaResponse.schema); @@ -220,6 +203,27 @@ export function transformOperation(operation: Operation): OperationDetails { }; } +export function transformOperationGroup( + operationGroup: OperationGroup +): OperationGroupDetails { + const metadata = getLanguageMetadata(operationGroup.language); + return { + name: normalizeName(metadata.name, NameType.Property), + key: operationGroup.$key, + operations: operationGroup.operations.map(transformOperation) + }; +} + +function getBodyMapperFromSchema(responseSchema: Schema): Mapper | string { + const responseType = getSpecType(responseSchema); + const { reference, ...type } = responseType; + return ( + reference || { + type: type as MapperType + } + ); +} + function mergeResponsesAndExceptions(operation: Operation) { let responses: OperationResponseDetails[] = []; @@ -239,14 +243,3 @@ function mergeResponsesAndExceptions(operation: Operation) { return responses; } - -export function transformOperationGroup( - operationGroup: OperationGroup -): OperationGroupDetails { - const metadata = getLanguageMetadata(operationGroup.language); - return { - name: normalizeName(metadata.name, NameType.Property), - key: operationGroup.$key, - operations: operationGroup.operations.map(transformOperation) - }; -} diff --git a/test/unit/operationTransforms.spec.ts b/test/unit/operationTransforms.spec.ts index b0934c3e0a..5f4a9098b0 100644 --- a/test/unit/operationTransforms.spec.ts +++ b/test/unit/operationTransforms.spec.ts @@ -2,8 +2,8 @@ import * as assert from "assert"; import { transformOperationSpec, getSpecType, - extractRequest, - transformOperation + transformOperation, + extractSpecRequest } from "../../src/operationTransforms"; import { Operation, @@ -18,7 +18,8 @@ import { ConstantValue } from "@azure-tools/codemodel"; import { KnownMediaType } from "@azure-tools/codegen"; -import { OperationSpec } from "@azure/core-http"; +import { Mapper } from "@azure/core-http"; +import { OperationSpecDetails } from "../../src/models/operationDetails"; const choice = new ChoiceSchema("mockChoice", "", { choices: [ @@ -71,7 +72,7 @@ describe("OperationTransforms", () => { }); it("should return a reference to a mapper when type is object", () => { const objectType = getSpecType(objectSchema); - assert.strictEqual(objectType, `Mappers.RefColorConstant`); + assert.strictEqual(objectType.reference, `Mappers.RefColorConstant`); }); }); @@ -131,7 +132,7 @@ describe("OperationTransforms", () => { }); }; - const checkHttpMethodAndPath = (operationSpec: OperationSpec) => { + const checkHttpMethodAndPath = (operationSpec: OperationSpecDetails) => { assert.strictEqual( operationSpec.httpMethod, "GET", @@ -168,7 +169,9 @@ describe("OperationTransforms", () => { const operationSpec = transformOperationSpec(operationDetails); checkHttpMethodAndPath(operationSpec); const okResponse = operationSpec.responses[200]; - assert.deepEqual(okResponse.bodyMapper!.type, { name: "String" }); + assert.deepEqual((okResponse.bodyMapper as Mapper).type, { + name: "String" + }); assert.deepEqual( operationSpec.responses.default.bodyMapper, "Mappers.ErrorModel" @@ -182,7 +185,7 @@ describe("OperationTransforms", () => { const operationSpec = transformOperationSpec(operationDetails); checkHttpMethodAndPath(operationSpec); const okResponse = operationSpec.responses[200]; - assert.deepEqual(okResponse.bodyMapper!.type, { + assert.deepEqual((okResponse.bodyMapper as Mapper).type, { name: "Enum", allowedValues: ["red color", "green-color", "blue_color"] }); @@ -194,9 +197,9 @@ describe("OperationTransforms", () => { }); }); - describe("extractRequest", () => { + describe("extractSpecRequest", () => { it("should extract request with expected parameterPath", () => { - const request = extractRequest({ + const request = extractSpecRequest({ request: { parameters: [{ location: ParameterLocation.Body, name: "stringBody" }] } From 44df0e4ef760984e62cf74dec1bba6fa76c0c771 Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Tue, 26 Nov 2019 13:20:06 -0800 Subject: [PATCH 06/10] Add missing JSDoc --- src/generators/operationGenerator.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/generators/operationGenerator.ts b/src/generators/operationGenerator.ts index eda0e4d130..4259e7e6f2 100644 --- a/src/generators/operationGenerator.ts +++ b/src/generators/operationGenerator.ts @@ -177,7 +177,7 @@ function addClass( const constructorDefinition = operationGroupClass.addConstructor({ docs: [ { - description: `Initialize a new instance of the class ${className} class.` + description: `Initialize a new instance of the class ${className} class. \n@param client Reference to the service client` } ], parameters: [ @@ -296,6 +296,7 @@ function addOperationSpecs( operationGroupDetails: OperationGroupDetails, file: SourceFile ): void { + file.addStatements("// Operation Specifications"); file.addVariableStatement({ declarationKind: VariableDeclarationKind.Const, declarations: [ From dec185289db924790c24771130540950b612aece Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Tue, 26 Nov 2019 13:27:25 -0800 Subject: [PATCH 07/10] Generate bodyString as part of integration-test script --- package.json | 6 +++--- test/integration/bodyString.spec.ts | 2 +- test/utils/start-server.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 7e2eb1d6c4..756a3468ee 100755 --- a/package.json +++ b/package.json @@ -6,10 +6,10 @@ "start": "node ./dist/src/main.js", "test": "npm run unit-test & npm run integration-test", "unit-test": "mocha -r ts-node/register './test/unit/**/*spec.ts'", - "integration-test": "ts-node test/utils/start-server.ts & mocha -r ts-node/register './test/integration/**/*spec.ts' & stop-autorest-testserver", + "integration-test": "ts-node test/utils/start-server.ts & npm run generate-bodystring & mocha -r ts-node/register './test/integration/**/*spec.ts' & stop-autorest-testserver", "debug": "node --inspect-brk ./dist/src/main.js", - "generate-bodystring": "npm run build && autorest-beta --typescript --use=/Users/joheredi/Projects/Azure/autorest.typescript.v3 --title=BodyString --input-file=node_modules/@autorest/test-server/__files/swagger/body-string.json --package-name=bodyString --package-version=1.0.0-preview1", - "generate-bodycomplex": "npm run build && autorest-beta --typescript --use=/Users/joheredi/Projects/Azure/autorest.typescript.v3 --title=BodyComplexClient --input-file=node_modules/@autorest/test-server/__files/swagger/body-complex.json --package-name=bodyString --package-version=1.0.0-preview1" + "generate-bodystring": "npm run build && autorest-beta --typescript --output-folder=./generated/bodyString --use=/Users/joheredi/Projects/Azure/autorest.typescript.v3 --title=BodyStringClient --input-file=node_modules/@autorest/test-server/__files/swagger/body-string.json --package-name=bodyString --package-version=1.0.0-preview1", + "generate-bodycomplex": "npm run build && autorest-beta --typescript --output-folder=./generated/bodyComplex --use=/Users/joheredi/Projects/Azure/autorest.typescript.v3 --title=BodyComplexClient --input-file=node_modules/@autorest/test-server/__files/swagger/body-complex.json --package-name=bodyString --package-version=1.0.0-preview1" }, "dependencies": { diff --git a/test/integration/bodyString.spec.ts b/test/integration/bodyString.spec.ts index 415c171949..39d7655b78 100644 --- a/test/integration/bodyString.spec.ts +++ b/test/integration/bodyString.spec.ts @@ -1,5 +1,5 @@ import { equal, fail, ok } from "assert"; -import { BodyString as BodyStringClient } from "../../generated/src/bodyString"; +import { BodyStringClient } from "../../generated/bodyString/src/bodyStringClient"; describe("Integration tests for BodyString", () => { describe("getMbcs", () => { diff --git a/test/utils/start-server.ts b/test/utils/start-server.ts index a484c15e24..358415667d 100644 --- a/test/utils/start-server.ts +++ b/test/utils/start-server.ts @@ -5,7 +5,7 @@ type OutputType = "silent" | "dots"; /** * Function that starts the test server with retries */ -const startTestServer = () => retry(startServer, 3); +const startTestServer = () => retry(startServer, 10); /** * Function that starts the tests server and verifies it is ready to receive requests From eba4b55517fa2800f16c98983850c9a7b4e74345 Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Wed, 27 Nov 2019 10:24:32 -0800 Subject: [PATCH 08/10] Address PR comments --- package.json | 4 +-- src/generators/clientFileGenerator.ts | 11 ++------ src/generators/mappersGenerator.ts | 2 +- src/generators/modelsGenerator.ts | 2 +- src/generators/operationGenerator.ts | 8 +++--- src/{ => transforms}/mapperTransforms.ts | 0 src/{ => transforms}/operationTransforms.ts | 28 ++++++++++--------- src/{ => transforms}/transforms.ts | 18 +++++++----- src/typescriptGenerator.ts | 2 +- .../{ => transforms}/mapperTransforms.spec.ts | 2 +- .../operationTransforms.spec.ts | 4 +-- test/unit/{ => transforms}/transforms.spec.ts | 2 +- 12 files changed, 42 insertions(+), 41 deletions(-) rename src/{ => transforms}/mapperTransforms.ts (100%) rename src/{ => transforms}/operationTransforms.ts (91%) rename src/{ => transforms}/transforms.ts (83%) rename test/unit/{ => transforms}/mapperTransforms.spec.ts (98%) rename test/unit/{ => transforms}/operationTransforms.spec.ts (98%) rename test/unit/{ => transforms}/transforms.spec.ts (98%) diff --git a/package.json b/package.json index 756a3468ee..92521b1d70 100755 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ "unit-test": "mocha -r ts-node/register './test/unit/**/*spec.ts'", "integration-test": "ts-node test/utils/start-server.ts & npm run generate-bodystring & mocha -r ts-node/register './test/integration/**/*spec.ts' & stop-autorest-testserver", "debug": "node --inspect-brk ./dist/src/main.js", - "generate-bodystring": "npm run build && autorest-beta --typescript --output-folder=./generated/bodyString --use=/Users/joheredi/Projects/Azure/autorest.typescript.v3 --title=BodyStringClient --input-file=node_modules/@autorest/test-server/__files/swagger/body-string.json --package-name=bodyString --package-version=1.0.0-preview1", - "generate-bodycomplex": "npm run build && autorest-beta --typescript --output-folder=./generated/bodyComplex --use=/Users/joheredi/Projects/Azure/autorest.typescript.v3 --title=BodyComplexClient --input-file=node_modules/@autorest/test-server/__files/swagger/body-complex.json --package-name=bodyString --package-version=1.0.0-preview1" + "generate-bodystring": "npm run build && autorest-beta --typescript --output-folder=./generated/bodyString --use=. --title=BodyStringClient --input-file=node_modules/@autorest/test-server/__files/swagger/body-string.json --package-name=bodyString --package-version=1.0.0-preview1", + "generate-bodycomplex": "npm run build && autorest-beta --typescript --output-folder=./generated/bodyComplex --use=. --title=BodyComplexClient --input-file=node_modules/@autorest/test-server/__files/swagger/body-complex.json --package-name=bodyString --package-version=1.0.0-preview1" }, "dependencies": { diff --git a/src/generators/clientFileGenerator.ts b/src/generators/clientFileGenerator.ts index dd924e8067..1a13ed8191 100644 --- a/src/generators/clientFileGenerator.ts +++ b/src/generators/clientFileGenerator.ts @@ -10,7 +10,6 @@ import { NameType } from "../utils/nameUtils"; import { CodeModel } from "@azure-tools/codemodel"; -import { transformOperationGroup } from "../operationTransforms"; export function generateClient( codeModel: CodeModel, @@ -54,14 +53,10 @@ export function generateClient( extends: clientContextClassName }); - const operations = codeModel.operationGroups.map(og => { - const operationGoupDetails = transformOperationGroup(og); + const operations = clientDetails.operationGroups.map(og => { return { - name: normalizeName(operationGoupDetails.name, NameType.Property), - typeName: `operations.${normalizeName( - operationGoupDetails.name, - NameType.Class - )}` + name: normalizeName(og.name, NameType.Property), + typeName: `operations.${normalizeName(og.name, NameType.Class)}` }; }); diff --git a/src/generators/mappersGenerator.ts b/src/generators/mappersGenerator.ts index 15e8e5f5b1..9874f2652b 100644 --- a/src/generators/mappersGenerator.ts +++ b/src/generators/mappersGenerator.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { CodeModel } from "@azure-tools/codemodel"; -import { transformMapper } from "../mapperTransforms"; +import { transformMapper } from "../transforms/mapperTransforms"; import { Project, VariableDeclarationKind } from "ts-morph"; export function generateMappers(codeModel: CodeModel, project: Project) { diff --git a/src/generators/modelsGenerator.ts b/src/generators/modelsGenerator.ts index 68fa3418e1..548991da3d 100644 --- a/src/generators/modelsGenerator.ts +++ b/src/generators/modelsGenerator.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { CodeModel } from "@azure-tools/codemodel"; -import { transformCodeModel } from "../transforms"; +import { transformCodeModel } from "../transforms/transforms"; import { Project, PropertySignatureStructure, StructureKind } from "ts-morph"; export function generateModels(codeModel: CodeModel, project: Project) { diff --git a/src/generators/operationGenerator.ts b/src/generators/operationGenerator.ts index 4259e7e6f2..05ad9e1360 100644 --- a/src/generators/operationGenerator.ts +++ b/src/generators/operationGenerator.ts @@ -11,7 +11,7 @@ import { } from "ts-morph"; import { normalizeName, NameType } from "../utils/nameUtils"; import { ClientDetails } from "../models/clientDetails"; -import { transformOperationSpec } from "../operationTransforms"; +import { transformOperationSpec } from "../transforms/operationTransforms"; import { Mapper } from "@azure/core-http"; import { OperationGroupDetails, @@ -232,7 +232,7 @@ function addOperations( const optionalOptionsParams = [...params, getOptionsParameter(true)]; const requiredCallbackParams = [...params, getCallbackParameter(false)]; - const retuiredOptionsAndCallbackParams = [ + const requiredOptionsAndCallbackParams = [ ...params, getOptionsParameter(false), getCallbackParameter(false) @@ -264,8 +264,8 @@ function addOperations( returnType: "void" }, { - parameters: retuiredOptionsAndCallbackParams, - docs: [generateOperationJSDoc(retuiredOptionsAndCallbackParams)], + parameters: requiredOptionsAndCallbackParams, + docs: [generateOperationJSDoc(requiredOptionsAndCallbackParams)], returnType: "void" } ]); diff --git a/src/mapperTransforms.ts b/src/transforms/mapperTransforms.ts similarity index 100% rename from src/mapperTransforms.ts rename to src/transforms/mapperTransforms.ts diff --git a/src/operationTransforms.ts b/src/transforms/operationTransforms.ts similarity index 91% rename from src/operationTransforms.ts rename to src/transforms/operationTransforms.ts index 3a7e2b7b15..fd0ac6c3f6 100644 --- a/src/operationTransforms.ts +++ b/src/transforms/operationTransforms.ts @@ -11,7 +11,7 @@ import { ParameterLocation, Parameter } from "@azure-tools/codemodel"; -import { normalizeName, NameType } from "./utils/nameUtils"; +import { normalizeName, NameType } from "../utils/nameUtils"; import { OperationGroupDetails, OperationDetails, @@ -21,9 +21,9 @@ import { OperationSpecDetails, OperationSpecResponses, OperationSpecRequest -} from "./models/operationDetails"; -import { getLanguageMetadata } from "./utils/languageHelpers"; -import { getTypeForSchema } from "./utils/schemaHelpers"; +} from "../models/operationDetails"; +import { getLanguageMetadata } from "../utils/languageHelpers"; +import { getTypeForSchema } from "../utils/schemaHelpers"; export function transformOperationSpec( operationDetails: OperationDetails @@ -32,27 +32,28 @@ export function transformOperationSpec( const httpInfo = extractHttpDetails(operationDetails.request); return { ...httpInfo, - responses: extractSpecResponses(operationDetails.responses), + responses: extractSpecResponses(operationDetails), requestBody: extractSpecRequest(operationDetails) }; } export function extractHttpDetails({ path, method }: OperationRequestDetails) { return { + // TODO: Revisit how we should handle {$host} path: path.replace("{$host}/", ""), httpMethod: method.toUpperCase() as HttpMethods }; } -export function extractSpecResponses( - responses?: OperationResponseDetails[] -): OperationSpecResponses { +export function extractSpecResponses({ + name, + responses +}: OperationDetails): OperationSpecResponses { if (!responses || !responses.length) { - // Should we throw an error here? - return {}; + throw new Error(`The operation ${name} contains no responses`); } - const schemaResponses = extractSchemaResponses(responses); + const schemaResponses = extractSchemaResponses(responses); return schemaResponses; } @@ -155,6 +156,7 @@ export function transformOperationRequest( ): OperationRequestDetails { if (request.protocol.http) { return { + // TODO: Revisit how we should handle {$host} path: request.protocol.http.path.replace("{$host}/", ""), method: request.protocol.http.method, parameters: request.parameters @@ -229,14 +231,14 @@ function mergeResponsesAndExceptions(operation: Operation) { if (operation.responses) { responses = [ - ...(responses || []), + ...responses, ...operation.responses.map(transformOperationResponse) ]; } if (operation.exceptions) { responses = [ - ...(responses || []), + ...responses, ...operation.exceptions.map(transformOperationResponse) ]; } diff --git a/src/transforms.ts b/src/transforms/transforms.ts similarity index 83% rename from src/transforms.ts rename to src/transforms/transforms.ts index da4d6161fc..ba482ebe92 100644 --- a/src/transforms.ts +++ b/src/transforms/transforms.ts @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { ClientDetails } from "./models/clientDetails"; -import { UnionDetails } from "./models/unionDetails"; -import { ModelDetails, PropertyDetails } from "./models/modelDetails"; +import { ClientDetails } from "../models/clientDetails"; +import { UnionDetails } from "../models/unionDetails"; +import { ModelDetails, PropertyDetails } from "../models/modelDetails"; import { CodeModel, @@ -11,10 +11,14 @@ import { Property, ChoiceSchema } from "@azure-tools/codemodel"; -import { normalizeName, NameType, guardReservedNames } from "./utils/nameUtils"; -import { getTypeForSchema } from "./utils/schemaHelpers"; -import { getStringForValue } from "./utils/valueHelpers"; -import { getLanguageMetadata } from "./utils/languageHelpers"; +import { + normalizeName, + NameType, + guardReservedNames +} from "../utils/nameUtils"; +import { getTypeForSchema } from "../utils/schemaHelpers"; +import { getStringForValue } from "../utils/valueHelpers"; +import { getLanguageMetadata } from "../utils/languageHelpers"; import { transformOperationGroup } from "./operationTransforms"; export function transformProperty(property: Property): PropertyDetails { diff --git a/src/typescriptGenerator.ts b/src/typescriptGenerator.ts index 6f20b004ee..2f7c3ea677 100755 --- a/src/typescriptGenerator.ts +++ b/src/typescriptGenerator.ts @@ -6,7 +6,7 @@ import { CodeModel } from "@azure-tools/codemodel"; import { Project, IndentationText } from "ts-morph"; import { Host } from "@azure-tools/autorest-extension-base"; import { PackageDetails } from "./models/packageDetails"; -import { transformCodeModel } from "./transforms"; +import { transformCodeModel } from "./transforms/transforms"; import { generateClient } from "./generators/clientFileGenerator"; import { generateClientContext } from "./generators/clientContextFileGenerator"; diff --git a/test/unit/mapperTransforms.spec.ts b/test/unit/transforms/mapperTransforms.spec.ts similarity index 98% rename from test/unit/mapperTransforms.spec.ts rename to test/unit/transforms/mapperTransforms.spec.ts index db55e1ded4..447dc8d4f7 100644 --- a/test/unit/mapperTransforms.spec.ts +++ b/test/unit/transforms/mapperTransforms.spec.ts @@ -1,5 +1,5 @@ import * as assert from "assert"; -import { transformMapper } from "../../src/mapperTransforms"; +import { transformMapper } from "../../../src/transforms/mapperTransforms"; import { ObjectSchema, Property, diff --git a/test/unit/operationTransforms.spec.ts b/test/unit/transforms/operationTransforms.spec.ts similarity index 98% rename from test/unit/operationTransforms.spec.ts rename to test/unit/transforms/operationTransforms.spec.ts index 5f4a9098b0..3c1fd13515 100644 --- a/test/unit/operationTransforms.spec.ts +++ b/test/unit/transforms/operationTransforms.spec.ts @@ -4,7 +4,7 @@ import { getSpecType, transformOperation, extractSpecRequest -} from "../../src/operationTransforms"; +} from "../../../src/transforms/operationTransforms"; import { Operation, SchemaResponse, @@ -19,7 +19,7 @@ import { } from "@azure-tools/codemodel"; import { KnownMediaType } from "@azure-tools/codegen"; import { Mapper } from "@azure/core-http"; -import { OperationSpecDetails } from "../../src/models/operationDetails"; +import { OperationSpecDetails } from "../../../src/models/operationDetails"; const choice = new ChoiceSchema("mockChoice", "", { choices: [ diff --git a/test/unit/transforms.spec.ts b/test/unit/transforms/transforms.spec.ts similarity index 98% rename from test/unit/transforms.spec.ts rename to test/unit/transforms/transforms.spec.ts index c1260de8c8..ae90ee1329 100644 --- a/test/unit/transforms.spec.ts +++ b/test/unit/transforms/transforms.spec.ts @@ -4,7 +4,7 @@ import { transformObject, transformProperty, transformChoice -} from "../../src/transforms"; +} from "../../../src/transforms/transforms"; import { CodeModel, From 9d34d658c9011b6ba25133a38d3208b308d688a7 Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Wed, 27 Nov 2019 10:45:54 -0800 Subject: [PATCH 09/10] fix integration-test script --- package.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 92521b1d70..6a118a05ca 100755 --- a/package.json +++ b/package.json @@ -4,13 +4,15 @@ "scripts": { "build": "tsc -p .", "start": "node ./dist/src/main.js", - "test": "npm run unit-test & npm run integration-test", + "test": "npm-run-all unit-test integration-test", "unit-test": "mocha -r ts-node/register './test/unit/**/*spec.ts'", - "integration-test": "ts-node test/utils/start-server.ts & npm run generate-bodystring & mocha -r ts-node/register './test/integration/**/*spec.ts' & stop-autorest-testserver", + "integration-test": "npm-run-all start-test-server generate-bodystring integration-test:alone stop-test-server", + "integration-test:alone": "mocha -r ts-node/register './test/integration/**/*spec.ts'", + "start-test-server": "ts-node test/utils/start-server.ts", + "stop-test-server": "stop-autorest-testserver", "debug": "node --inspect-brk ./dist/src/main.js", "generate-bodystring": "npm run build && autorest-beta --typescript --output-folder=./generated/bodyString --use=. --title=BodyStringClient --input-file=node_modules/@autorest/test-server/__files/swagger/body-string.json --package-name=bodyString --package-version=1.0.0-preview1", "generate-bodycomplex": "npm run build && autorest-beta --typescript --output-folder=./generated/bodyComplex --use=. --title=BodyComplexClient --input-file=node_modules/@autorest/test-server/__files/swagger/body-complex.json --package-name=bodyString --package-version=1.0.0-preview1" - }, "dependencies": { "@autorest/autorest": "^3.0.6122", @@ -39,6 +41,7 @@ "mocha": "6.2.1", "mocha-typescript": "1.1.17", "node-cmd": "^3.0.0", + "npm-run-all": "^4.1.5", "ts-node": "^8.5.2", "typescript": "~3.6.2", "wait-port": "^0.2.6" From 3973127e35845b4059bab85fc2c239d174b21a50 Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Wed, 27 Nov 2019 10:46:20 -0800 Subject: [PATCH 10/10] Reduce to 4 attempts on start-server --- test/utils/start-server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/start-server.ts b/test/utils/start-server.ts index 358415667d..7b5c58b338 100644 --- a/test/utils/start-server.ts +++ b/test/utils/start-server.ts @@ -5,7 +5,7 @@ type OutputType = "silent" | "dots"; /** * Function that starts the test server with retries */ -const startTestServer = () => retry(startServer, 10); +const startTestServer = () => retry(startServer, 4); /** * Function that starts the tests server and verifies it is ready to receive requests