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/package.json b/package.json index 2081cc898b..6a118a05ca 100755 --- a/package.json +++ b/package.json @@ -4,10 +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 & mocha -r ts-node/register './test/integration/**/*spec.ts' & stop-autorest-testserver", - "debug": "node --inspect-brk ./dist/src/main.js" + "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", @@ -36,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" 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..1a13ed8191 100644 --- a/src/generators/clientFileGenerator.ts +++ b/src/generators/clientFileGenerator.ts @@ -1,15 +1,24 @@ // 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"; -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 +53,21 @@ 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 = clientDetails.operationGroups.map(og => { + return { + name: normalizeName(og.name, NameType.Property), + typeName: `operations.${normalizeName(og.name, NameType.Class)}` + }; + }); + + clientClass.addProperties( + operations.map(op => { + return { + name: op.name, + type: op.typeName + } as PropertyDeclarationStructure; + }) + ); const clientConstructor = clientClass.addConstructor({ docs: [ @@ -65,20 +80,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/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 new file mode 100644 index 0000000000..05ad9e1360 --- /dev/null +++ b/src/generators/operationGenerator.ts @@ -0,0 +1,352 @@ +import { ParameterLocation } from "@azure-tools/codemodel"; +import { + Project, + SourceFile, + VariableDeclarationKind, + Scope, + ClassDeclaration, + ParameterDeclarationStructure, + OptionalKind, + ExportDeclarationStructure +} from "ts-morph"; +import { normalizeName, NameType } from "../utils/nameUtils"; +import { ClientDetails } from "../models/clientDetails"; +import { transformOperationSpec } from "../transforms/operationTransforms"; +import { Mapper } from "@azure/core-http"; +import { + 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)); + generateOperation(operationDetails, clientDetails, project); + }); + + const operationIndexFile = project.createSourceFile( + "src/operations/index.ts", + undefined, + { overwrite: true } + ); + + operationIndexFile.addExportDeclarations( + fileNames.map(fileName => { + return { + moduleSpecifier: `./${fileName}` + } as ExportDeclarationStructure; + }) + ); +} + +/** + * 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( + `src/operations/${name}.ts`, + undefined, + { overwrite: true } + ); + + addImports(operationGroupFile, clientDetails); + addClass(operationGroupFile, operationGroupDetails, clientDetails); + addOperationSpecs(operationGroupDetails, operationGroupFile); +} + +/** + * 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 `{ + path: "${spec.path}", + httpMethod: "${spec.httpMethod}", + responses: {${responses.join(", ")}}, + ${requestBody} + serializer + }`; +} + +/** + * This function transforms the requestBody of OperationSpecDetails into its string representation + * to insert in generated files + */ +function buildRequestBody({ requestBody }: OperationSpecDetails): string { + if (!requestBody) { + return ""; + } + + // 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); + + return `requestBody: { + parameterPath: "${requestBody.parameterPath}", + mapper: ${mapper} + },`; +} + +/** + * 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 Mapper).type + ) { + 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} + }`); + } + }); + + 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, + clientDetails: ClientDetails +) { + const className = normalizeName(operationGroupDetails.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. \n@param client Reference to the service client` + } + ], + parameters: [ + { + name: "client", + hasQuestionToken: false, + type: clientDetails.className + } + ] + }); + + constructorDefinition.addStatements(["this.client = client"]); + 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 +) { + 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, + 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 requiredOptionsAndCallbackParams = [ + ...params, + getOptionsParameter(false), + getCallbackParameter(false) + ]; + const operationMethod = operationGroupClass.addMethod({ + name: normalizeName(operation.name, NameType.Property), + parameters: allParams, + 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([ + { + parameters: optionalOptionsParams, + docs: [ + generateOperationJSDoc(optionalOptionsParams, operation.description) + ], + returnType: "Promise" // TODO: Add correct return type + }, + { + parameters: requiredCallbackParams, + docs: [generateOperationJSDoc(requiredCallbackParams)], + returnType: "void" + }, + { + parameters: requiredOptionsAndCallbackParams, + docs: [generateOperationJSDoc(requiredOptionsAndCallbackParams)], + returnType: "void" + } + ]); + }); +} + +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}`; +} + +/** + * Generates and inserts into the file the operation specs + * for a given operation group + */ +function addOperationSpecs( + operationGroupDetails: OperationGroupDetails, + file: SourceFile +): void { + file.addStatements("// Operation Specifications"); + 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 +) { + 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/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/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 new file mode 100644 index 0000000000..35fd83e634 --- /dev/null +++ b/src/models/operationDetails.ts @@ -0,0 +1,81 @@ +// 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; + bodyMapper?: Mapper | string; +} + +/** + * Details of an operation, transformed from Operation. + */ +export interface OperationDetails { + name: string; + description: string; + apiVersions: string[]; + request: OperationRequestDetails; + 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. + */ +export interface OperationGroupDetails { + key: string; + 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/transforms.ts b/src/transforms.ts deleted file mode 100644 index 66182385eb..0000000000 --- a/src/transforms.ts +++ /dev/null @@ -1,159 +0,0 @@ -// 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 { - CodeModel, - ObjectSchema, - Languages, - Language, - Schema, - SchemaType, - Property, - ChoiceSchema, - ChoiceValue, - ValueSchema, - ConstantSchema -} 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; - default: - throw new Error(`Unsupported schema type: ${schema.type}`); - } - - return { - typeName, - isConstant: schema.type === SchemaType.Constant, - defaultValue - }; -} - -export function transformProperty(property: Property): PropertyDetails { - const metadata = getLanguageMetadata(property.language); - const { typeName, isConstant, defaultValue } = getTypeForSchema( - property.schema - ); - - return { - name: normalizeName(metadata.name, NameType.Property), - description: !metadata.description.startsWith("MISSING") - ? metadata.description - : undefined, - serializedName: property.serializedName, - type: typeName, - required: !!property.required, - readOnly: !!property.readOnly, - isConstant, - defaultValue - }; -} - -export function transformChoice(choice: ChoiceSchema): UnionDetails { - const metadata = getLanguageMetadata(choice.language); - let name = - ReservedModelNames.indexOf(metadata.name) > -1 - ? `${metadata.name}Model` - : metadata.name; - - return { - name, - description: `Defines values for ${metadata.name}.`, - serializedName: metadata.name, - values: choice.choices.map(c => - getStringForValue(c.value, choice.choiceType) - ) - }; -} - -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 - ); - - return { - name, - description: `An interface representing ${metadata.name}.`, - serializedName: metadata.name, - properties: obj.properties - ? obj.properties.map(prop => transformProperty(prop)) - : [] - }; -} - -export function transformCodeModel(codeModel: CodeModel): ClientDetails { - const className = normalizeName(codeModel.info.title, NameType.Class); - return { - name: codeModel.info.title, - className, - description: codeModel.info.description, - sourceFileName: normalizeName(className, NameType.File), - models: codeModel.schemas.objects - ? codeModel.schemas.objects.map(transformObject) - : [], - unions: codeModel.schemas.choices - ? codeModel.schemas.choices.map(transformChoice) - : [] - }; -} 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/transforms/operationTransforms.ts b/src/transforms/operationTransforms.ts new file mode 100644 index 0000000000..fd0ac6c3f6 --- /dev/null +++ b/src/transforms/operationTransforms.ts @@ -0,0 +1,247 @@ +import { HttpMethods, Mapper, MapperType } from "@azure/core-http"; +import { + Operation, + Request, + SchemaResponse, + Response, + Schema, + SchemaType, + ChoiceSchema, + OperationGroup, + ParameterLocation, + Parameter +} from "@azure-tools/codemodel"; +import { normalizeName, NameType } from "../utils/nameUtils"; +import { + OperationGroupDetails, + OperationDetails, + OperationResponseDetails, + OperationRequestDetails, + OperationRequestParameterDetails, + OperationSpecDetails, + OperationSpecResponses, + OperationSpecRequest +} from "../models/operationDetails"; +import { getLanguageMetadata } from "../utils/languageHelpers"; +import { getTypeForSchema } from "../utils/schemaHelpers"; + +export function transformOperationSpec( + operationDetails: OperationDetails +): OperationSpecDetails { + // Extract protocol information + const httpInfo = extractHttpDetails(operationDetails.request); + return { + ...httpInfo, + 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({ + name, + responses +}: OperationDetails): OperationSpecResponses { + if (!responses || !responses.length) { + throw new Error(`The operation ${name} contains no responses`); + } + + const schemaResponses = extractSchemaResponses(responses); + return schemaResponses; +} + +export function extractSpecRequest( + operationDetails: OperationDetails +): OperationSpecRequest | undefined { + 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: string; + allowedValues?: string[]; + reference?: 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"; + 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; + reference = `Mappers.${normalizeName(name, NameType.Class)}`; + break; + default: + throw new Error(`Unsupported Spec Type ${responseSchema.type}`); + } + + let result = { + name: typeName as any, + reference + }; + + return !!allowedValues ? { ...result, allowedValues } : result; +} + +export function extractSchemaResponses(responses: OperationResponseDetails[]) { + return responses.reduce( + (result: OperationSpecResponses, response: OperationResponseDetails) => { + const statusCodes = response.statusCodes; + + if (!statusCodes || !statusCodes.length) { + return result; + } + + const statusCode = statusCodes[0]; + result[statusCode] = {}; + if (response.bodyMapper) { + result[statusCode] = { + bodyMapper: response.bodyMapper + }; + } + return result; + }, + {} + ); +} + +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 { + // TODO: Revisit how we should handle {$host} + 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 | SchemaResponse +): OperationResponseDetails { + let modelType: string | undefined = undefined; + let bodyMapper: Mapper | string | undefined = undefined; + + if ((response as SchemaResponse).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, + bodyMapper + }; + } else { + throw new Error("Operation does not specify HTTP response details."); + } +} + +export function transformOperation(operation: Operation): OperationDetails { + const metadata = getLanguageMetadata(operation.language); + return { + name: normalizeName(metadata.name, NameType.Property), + apiVersions: operation.apiVersions + ? operation.apiVersions.map(v => v.version) + : [], + description: metadata.description, + request: transformOperationRequest(operation.request), + responses: mergeResponsesAndExceptions(operation) + }; +} + +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[] = []; + + if (operation.responses) { + responses = [ + ...responses, + ...operation.responses.map(transformOperationResponse) + ]; + } + + if (operation.exceptions) { + responses = [ + ...responses, + ...operation.exceptions.map(transformOperationResponse) + ]; + } + + return responses; +} diff --git a/src/transforms/transforms.ts b/src/transforms/transforms.ts new file mode 100644 index 0000000000..ba482ebe92 --- /dev/null +++ b/src/transforms/transforms.ts @@ -0,0 +1,87 @@ +// 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 { + CodeModel, + ObjectSchema, + 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 { transformOperationGroup } from "./operationTransforms"; + +export function transformProperty(property: Property): PropertyDetails { + const metadata = getLanguageMetadata(property.language); + const { typeName, isConstant, defaultValue } = getTypeForSchema( + property.schema + ); + + return { + name: normalizeName(metadata.name, NameType.Property), + description: !metadata.description.startsWith("MISSING") + ? metadata.description + : undefined, + serializedName: property.serializedName, + type: typeName, + required: !!property.required, + readOnly: !!property.readOnly, + isConstant, + defaultValue + }; +} + +export function transformChoice(choice: ChoiceSchema): UnionDetails { + const metadata = getLanguageMetadata(choice.language); + let name = guardReservedNames(metadata.name); + + return { + name, + description: `Defines values for ${metadata.name}.`, + serializedName: metadata.name, + values: choice.choices.map(c => + getStringForValue(c.value, choice.choiceType) + ) + }; +} + +export function transformObject(obj: ObjectSchema): ModelDetails { + const metadata = getLanguageMetadata(obj.language); + let name = normalizeName(guardReservedNames(metadata.name), NameType.Class); + + return { + name, + description: `An interface representing ${metadata.name}.`, + serializedName: metadata.name, + properties: obj.properties + ? obj.properties.map(prop => transformProperty(prop)) + : [] + }; +} + +export function transformCodeModel(codeModel: CodeModel): ClientDetails { + const className = normalizeName(codeModel.info.title, NameType.Class); + return { + name: codeModel.info.title, + className, + description: codeModel.info.description, + sourceFileName: normalizeName(className, NameType.File), + models: codeModel.schemas.objects + ? codeModel.schemas.objects.map(transformObject) + : [], + 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 129a8c092e..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"; @@ -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", @@ -73,10 +74,11 @@ 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); + 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 08dd1509b4..b49a1fadd7 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, @@ -12,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); @@ -21,7 +26,9 @@ 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 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/integration/bodyString.spec.ts b/test/integration/bodyString.spec.ts new file mode 100644 index 0000000000..39d7655b78 --- /dev/null +++ b/test/integration/bodyString.spec.ts @@ -0,0 +1,33 @@ +import { equal, fail, ok } from "assert"; +import { BodyStringClient } from "../../generated/bodyString/src/bodyStringClient"; + +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/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/transforms/operationTransforms.spec.ts b/test/unit/transforms/operationTransforms.spec.ts new file mode 100644 index 0000000000..3c1fd13515 --- /dev/null +++ b/test/unit/transforms/operationTransforms.spec.ts @@ -0,0 +1,210 @@ +import * as assert from "assert"; +import { + transformOperationSpec, + getSpecType, + transformOperation, + extractSpecRequest +} from "../../../src/transforms/operationTransforms"; +import { + Operation, + SchemaResponse, + SchemaType, + Schema, + ChoiceValue, + ChoiceSchema, + ObjectSchema, + ParameterLocation, + ConstantSchema, + ConstantValue +} from "@azure-tools/codemodel"; +import { KnownMediaType } from "@azure-tools/codegen"; +import { Mapper } from "@azure/core-http"; +import { OperationSpecDetails } from "../../../src/models/operationDetails"; + +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 ConstantSchema( + "paths·string-null·get·responses·200·content·application-json·schema", + "", + { + value: new ConstantValue("Some constant value"), + valueType: { type: SchemaType.String } + } +); + +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.reference, `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: OperationSpecDetails) => { + 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 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 operationDetails = transformOperation(mockOperation); + const operationSpec = transformOperationSpec(operationDetails); + 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 operationDetails = transformOperation(mockOperation); + const operationSpec = transformOperationSpec(operationDetails); + checkHttpMethodAndPath(operationSpec); + const okResponse = operationSpec.responses[200]; + assert.deepEqual((okResponse.bodyMapper as Mapper).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 operationDetails = transformOperation(mockOperation); + const operationSpec = transformOperationSpec(operationDetails); + checkHttpMethodAndPath(operationSpec); + const okResponse = operationSpec.responses[200]; + assert.deepEqual((okResponse.bodyMapper as Mapper).type, { + name: "Enum", + allowedValues: ["red color", "green-color", "blue_color"] + }); + assert.deepEqual( + operationSpec.responses.default.bodyMapper, + "Mappers.ErrorModel" + ); + }); + }); + }); + + describe("extractSpecRequest", () => { + it("should extract request with expected parameterPath", () => { + const request = extractSpecRequest({ + request: { + parameters: [{ location: ParameterLocation.Body, name: "stringBody" }] + } + } as any); + assert.deepEqual(request!.parameterPath, "stringBody"); + }); + }); +}); diff --git a/test/unit/transforms.spec.ts b/test/unit/transforms/transforms.spec.ts similarity index 51% rename from test/unit/transforms.spec.ts rename to test/unit/transforms/transforms.spec.ts index 523f9c5b6b..ae90ee1329 100644 --- a/test/unit/transforms.spec.ts +++ b/test/unit/transforms/transforms.spec.ts @@ -3,10 +3,8 @@ import * as assert from "assert"; import { transformObject, transformProperty, - transformChoice, - getTypeForSchema, - getStringForValue -} from "../../src/transforms"; + transformChoice +} from "../../../src/transforms/transforms"; import { CodeModel, @@ -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` + ); + }); + }); +}); diff --git a/test/utils/start-server.ts b/test/utils/start-server.ts index a484c15e24..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, 3); +const startTestServer = () => retry(startServer, 4); /** * Function that starts the tests server and verifies it is ready to receive requests