Initial move of csharp client emitter (#3098)

Fixes https://github.com/microsoft/typespec/issues/3095

---------

Co-authored-by: Wes Haggard <weshaggard@users.noreply.github.com>
Co-authored-by: Brian Terlson <brian.terlson@microsoft.com>
Co-authored-by: Timothee Guerin <tiguerin@microsoft.com>
Co-authored-by: Patrick Hallisey <hallipr@gmail.com>
This commit is contained in:
m-nash 2024-04-04 16:26:05 -07:00 коммит произвёл GitHub
Родитель f490d448a7
Коммит 0889ba248c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
70 изменённых файлов: 10296 добавлений и 13 удалений

Просмотреть файл

@ -54,3 +54,6 @@ versionPolicies:
- "@typespec/library-linter"
changelog: ["@chronus/github/changelog", { repo: "microsoft/typespec" }]
ignore:
- "@typespec/http-client-csharp"

9
.github/CODEOWNERS поставляемый
Просмотреть файл

@ -1 +1,8 @@
* @bterlson @markcowl @allenjzhang @timotheeguerin
######################
# CSharp
######################
/packages/http-client-csharp @m-nash
/packages/http-client-csharp-generator @m-nash
# Catch all
* @bterlson @markcowl @allenjzhang @timotheeguerin

3
.gitignore поставляемый
Просмотреть файл

@ -190,3 +190,6 @@ obj/
docs/**/js-api/
# csharp emitter
!packages/http-client-csharp/package-lock.json

Просмотреть файл

@ -112,6 +112,8 @@ words:
- WINDOWSVMIMAGE
- xlarge
- xplat
- isequal # only needed until https://github.com/microsoft/typespec/issues/3108 is resolved
- TCGC
ignorePaths:
- "**/node_modules/**"
- "**/dist/**"

Просмотреть файл

@ -1,9 +1,14 @@
# Continuous Integration
trigger:
- main
# For patch releases
- release/*
branches:
include:
- main
# For patch releases
- release/*
paths:
exclude:
- packages/http-client-csharp
pr: none

Просмотреть файл

@ -1,8 +1,13 @@
name: PR Tools
trigger: none
pr:
- main
- release/*
branches:
include:
- main
- release/*
paths:
exclude:
- packages/http-client-csharp
extends:
template: /eng/pipelines/templates/1es-redirect.yml

Просмотреть файл

@ -3,10 +3,18 @@ trigger:
include:
- main
- gh-readonly-queue/* # Used to trigger for GitHub merge queues
paths:
exclude:
- packages/http-client-csharp
pr:
- main
- release/*
branches:
include:
- main
- release/*
paths:
exclude:
- packages/http-client-csharp
extends:
template: /eng/pipelines/templates/1es-redirect.yml

Просмотреть файл

@ -6,21 +6,22 @@
"scripts": {
"build": "pnpm build:all && pnpm gen-compiler-extern-signature",
"build:all": "pnpm -r --workspace-concurrency=Infinity --aggregate-output --reporter=append-only build && pnpm gen-compiler-extern-signature",
"setup:min": "pnpm install && pnpm --filter \"@typespec/prettier-plugin-typespec...\" run build",
"check-version-mismatch": "syncpack list-mismatches",
"change": "chronus",
"clean": "pnpm -r run clean",
"cspell": "cspell --no-progress .",
"dogfood": "pnpm install && pnpm build && pnpm -r dogfood",
"fix-version-mismatch": "syncpack fix-mismatches",
"format": "pnpm run prettier --write",
"format:check": "pnpm run prettier --check",
"format": "prettier . --write",
"format:check": "prettier . --check",
"format:dir": "prettier --write",
"gen-compiler-extern-signature": "pnpm run -r --filter \"@typespec/compiler\" gen-extern-signature",
"lint": "pnpm -r --parallel --aggregate-output --reporter=append-only run lint",
"merge-coverage": "c8 -- report --reporter=cobertura --reporter=text",
"pack:all": "chronus pack --pack-destination ./temp/artifacts",
"preinstall": "npx only-allow pnpm",
"prepare-publish": "pnpm chronus version",
"prettier": "prettier .",
"purge": "rimraf --glob \"packages/*/node_modules/\"",
"regen-docs": "pnpm -r --parallel --aggregate-output --reporter=append-only run regen-docs",
"regen-samples": "pnpm -r run regen-samples",

Просмотреть файл

@ -0,0 +1,10 @@
require("@typespec/eslint-config-typespec/patch/modern-module-resolution");
module.exports = {
plugins: ["@typespec/eslint-plugin-typespec"],
extends: [
"@typespec/eslint-config-typespec",
"plugin:@typespec/eslint-plugin-typespec/recommended",
],
parserOptions: { tsconfigRootDir: __dirname },
};

Просмотреть файл

@ -0,0 +1,45 @@
trigger: none
pr: none
extends:
template: /eng/pipelines/templates/1es-redirect.yml
parameters:
variables:
BlobFeedUrl: https://azuresdkartifacts.blob.core.windows.net/azure-sdk-tools/index.json
OfficialBuildId: $(Build.BuildNumber)
nugetMultiFeedWarnLevel: "none"
stages:
- stage: "Build_and_Test"
pool:
name: $(WINDOWSPOOL)
image: $(WINDOWSVMIMAGE)
os: windows
jobs:
- job: Build
timeoutInMinutes: 120
steps:
- checkout: self
fetchDepth: 1
- task: NodeTool@0
displayName: "Install Node 18.x"
inputs:
versionSpec: "18.x"
- script: |
npm ci
displayName: "Install packages"
workingDirectory: $(Build.SourcesDirectory)/packages/http-client-csharp
- script: |
npm ls -a
displayName: "List packages"
workingDirectory: $(Build.SourcesDirectory)/packages/http-client-csharp
- task: Npm@1
displayName: "Build TypeSpec csharp emitter"
inputs:
command: custom
customCommand: run build
workingDir: $(Build.SourcesDirectory)/packages/http-client-csharp
- script: |
npm run test
displayName: "Unit Test"
workingDirectory: $(Build.SourcesDirectory)/packages/http-client-csharp

4625
packages/http-client-csharp/package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,70 @@
{
"name": "@typespec/http-client-csharp",
"version": "0.2.0",
"author": "Microsoft Corporation",
"description": "The typespec library that can be used to generate C# models from a TypeSpec REST protocol binding",
"homepage": "https://github.com/Microsoft/typespec",
"readme": "https://github.com/Microsoft/typespec/blob/main/packages/http-client-csharp/readme.md",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/Microsoft/typespec.git"
},
"bugs": {
"url": "https://github.com/Microsoft/typespec/issues"
},
"keywords": [
"typespec"
],
"type": "module",
"main": "dist/src/index.js",
"scripts": {
"clean": "rimraf ./dist ./temp",
"build": "tsc -p .",
"watch": "tsc -p . --watch",
"lint-typespec-library": "tsp compile . --warn-as-error --import @typespec/library-linter --no-emit",
"test": "mocha dist/test/**/*.js",
"test-official": "c8 mocha --forbid-only dist/test/**/*.js",
"lint": "eslint . --ext .ts --max-warnings=0",
"lint:fix": "eslint . --fix --ext .ts",
"format": "pnpm -w format:dir packages/http-client-csharp"
},
"files": [
"dist/src/**"
],
"dependencies": {
"json-serialize-refs": "0.1.0-0",
"winston": "^3.8.2"
},
"peerDependencies": {
"@azure-tools/typespec-azure-core": ">=0.36.0 <1.0.0",
"@azure-tools/typespec-client-generator-core": ">=0.36.0 <1.0.0",
"@typespec/compiler": ">=0.50.0 <1.0.0",
"@typespec/http": ">=0.50.0 <1.0.0",
"@typespec/rest": ">=0.50.0 <1.0.0",
"@typespec/versioning": ">=0.50.0 <1.0.0",
"@typespec/openapi": ">=0.50.0 <1.0.0"
},
"devDependencies": {
"@azure-tools/typespec-azure-core": "0.40.0",
"@azure-tools/typespec-client-generator-core": "0.40.0",
"@typespec/compiler": "0.54.0",
"@typespec/eslint-config-typespec": "0.54.0",
"@typespec/eslint-plugin": "0.54.0",
"@typespec/http": "0.54.0",
"@typespec/library-linter": "0.54.0",
"@typespec/rest": "0.54.0",
"@typespec/versioning": "0.54.0",
"@typespec/openapi": "0.54.0",
"@typespec/json-schema": "0.54.0",
"@types/lodash.isequal": "^4.5.6",
"@types/mocha": "~9.1.0",
"@types/node": "~18.13.0",
"c8": "~7.11.0",
"eslint": "^8.57.0",
"lodash.isequal": "^4.5.0",
"mocha": "~9.2.0",
"rimraf": "~5.0.5",
"typescript": "~5.4.3"
}
}

Просмотреть файл

@ -0,0 +1,66 @@
# TypeSpec csharp emitter library
This is a TypeSpec library that will emit a .NET SDK from TypeSpec.
## Prerequisite
Install [Node.js](https://nodejs.org/download/) 16 or above. (Verify by `node --version`)
Install [**.NET 8.0 SDK**](https://dotnet.microsoft.com/download/dotnet/8.0) for your specific platform. (or a higher version)
## Getting started
### Initialize TypeSpec Project
Follow [TypeSpec Getting Started](https://github.com/microsoft/typespec/#using-node--npm) to initialize your TypeSpec project.
Make sure `npx tsp compile .` runs correctly.
### Add typespec csharp
Run `npm install @typespec/http-client-csharp`.
### Generate .NET client
Run command `npx tsp compile --emit @typespec/http-client-csharp <path-to-typespec-file>`
e.g.
```cmd
npx tsp compile main.tsp --emit @typespec/http-client-csharp
```
## Configuration
You can further configure the SDK generated, using the emitter options on @typespec/http-client-csharp.
You can set options in the command line directly via `--option @typespec/http-client-csharp.<optionName>=XXX`, e.g. `--option @typespec/http-client-csharp.namespace=MyService.Namespace`
or
Modify `tspconfig.yaml` in typespec project, add emitter options under options/@typespec/http-client-csharp.
```diff
emit:
- "@typespec/http-client-csharp"
options:
"@typespec/http-client-csharp":
+ namespace: MyService.Namespace
```
**Supported Emitter options**:
- `namespace` define the client library namespace. e.g. MyService.Namespace.
- `emitter-output-dir` define the output dire path which will store the generated code.
- `generate-protocol-methods` indicate if you want to generate **protocol method** for every operation or not. The default value is true.
- `generate-convenience-methods` indicate if you want to generate **convenience method** for every operation or not. The default value is true.
- `unreferenced-types-handling` define the strategy how to handle the unreferenced types. It can be `removeOrInternalize`, `internalize` or `keepAll`
- `model-namespace` indicate if we want to put the models in their own namespace which is a sub namespace of the client library namespace plus ".Models". if it is set `false`, the models will be put in the same namespace of the client. The default value is `true`.
- `clear-output-folder` indicate if you want to clear up the output folder.
- `package-name` define the package name.
## Convenience API
By default, TypeSpec csharp generates all protocol APIs and convenience APIs.
A few exceptions are API of JSON Merge Patch, and API of long-running operation with ambiguous response type.
You can configure whether generate convenience API or not via `convenienceAPI` decorator.

Просмотреть файл

@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
export const projectedNameJsonKey = "json";
export const projectedNameCSharpKey = "csharp";
export const projectedNameClientKey = "client";
export const mockApiVersion = "0000-00-00";
export const tspOutputFileName = "tspCodeModel.json";
export const configurationFileName = "Configuration.json";

Просмотреть файл

@ -0,0 +1,184 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { createSdkContext } from "@azure-tools/typespec-client-generator-core";
import {
EmitContext,
Program,
createTypeSpecLibrary,
logDiagnostics,
paramMessage,
resolvePath,
} from "@typespec/compiler";
import fs, { existsSync } from "fs";
import { PreserveType, stringifyRefs } from "json-serialize-refs";
import { configurationFileName, tspOutputFileName } from "./constants.js";
import { createModel } from "./lib/clientModelBuilder.js";
import { LoggerLevel, logger } from "./lib/logger.js";
import {
NetEmitterOptions,
NetEmitterOptionsSchema,
resolveOptions,
resolveOutputFolder,
} from "./options.js";
import { Configuration } from "./type/configuration.js";
export const $lib = createTypeSpecLibrary({
name: "@typespec/http-client-csharp",
diagnostics: {
"No-APIVersion": {
severity: "error",
messages: {
default: paramMessage`No APIVersion Provider for service ${"service"}`,
},
},
"No-Route": {
severity: "error",
messages: {
default: paramMessage`No Route for service for service ${"service"}`,
},
},
"Invalid-Name": {
severity: "warning",
messages: {
default: paramMessage`Invalid interface or operation group name ${"name"} when configuration "model-namespace" is on`,
},
},
},
emitter: {
options: NetEmitterOptionsSchema,
},
});
export async function $onEmit(context: EmitContext<NetEmitterOptions>) {
const program: Program = context.program;
const options = resolveOptions(context);
const outputFolder = resolveOutputFolder(context);
/* set the loglevel. */
for (const transport of logger.transports) {
transport.level = options.logLevel ?? LoggerLevel.INFO;
}
if (!program.compilerOptions.noEmit && !program.hasError()) {
// Write out the dotnet model to the output path
const sdkContext = createSdkContext(context, "@typespec/http-client-csharp");
const root = createModel(sdkContext);
if (
context.program.diagnostics.length > 0 &&
context.program.diagnostics.filter((digs) => digs.severity === "error").length > 0
) {
logDiagnostics(context.program.diagnostics, context.program.host.logSink);
process.exit(1);
}
const tspNamespace = root.Name; // this is the top-level namespace defined in the typespec file, which is actually always different from the namespace of the SDK
// await program.host.writeFile(outPath, prettierOutput(JSON.stringify(root, null, 2)));
if (root) {
const generatedFolder = outputFolder.endsWith("src")
? resolvePath(outputFolder, "Generated")
: resolvePath(outputFolder, "src", "Generated");
if (!fs.existsSync(generatedFolder)) {
fs.mkdirSync(generatedFolder, { recursive: true });
}
await program.host.writeFile(
resolvePath(generatedFolder, tspOutputFileName),
prettierOutput(stringifyRefs(root, null, 1, PreserveType.Objects))
);
//emit configuration.json
const namespace = options.namespace ?? tspNamespace;
const configurations: Configuration = {
"output-folder": ".",
namespace: namespace,
"library-name": options["library-name"] ?? namespace,
"single-top-level-client": options["single-top-level-client"],
"unreferenced-types-handling": options["unreferenced-types-handling"],
"keep-non-overloadable-protocol-signature":
options["keep-non-overloadable-protocol-signature"],
"model-namespace": options["model-namespace"],
"models-to-treat-empty-string-as-null": options["models-to-treat-empty-string-as-null"],
"intrinsic-types-to-treat-empty-string-as-null": options[
"models-to-treat-empty-string-as-null"
]
? options["additional-intrinsic-types-to-treat-empty-string-as-null"].concat(
["Uri", "Guid", "ResourceIdentifier", "DateTimeOffset"].filter(
(item) =>
options["additional-intrinsic-types-to-treat-empty-string-as-null"].indexOf(
item
) < 0
)
)
: undefined,
"methods-to-keep-client-default-value": options["methods-to-keep-client-default-value"],
"head-as-boolean": options["head-as-boolean"],
"deserialize-null-collection-as-null-value":
options["deserialize-null-collection-as-null-value"],
flavor: options["flavor"],
//only emit these if they are not the default values
"generate-sample-project":
options["generate-sample-project"] === true
? undefined
: options["generate-sample-project"],
"generate-test-project":
options["generate-test-project"] === false ? undefined : options["generate-test-project"],
"use-model-reader-writer": options["use-model-reader-writer"] ?? true,
};
await program.host.writeFile(
resolvePath(generatedFolder, configurationFileName),
prettierOutput(JSON.stringify(configurations, null, 2))
);
if (options.skipSDKGeneration !== true) {
const csProjFile = resolvePath(outputFolder, `${configurations["library-name"]}.csproj`);
logger.info(`Checking if ${csProjFile} exists`);
const newProjectOption =
options["new-project"] || !existsSync(csProjFile) ? "--new-project" : "";
const existingProjectOption = options["existing-project-folder"]
? `--existing-project-folder ${options["existing-project-folder"]}`
: "";
const debugFlag = options.debug ?? false ? " --debug" : "";
logger.info("TODO connect the dotnet generator");
//const command = `dotnet --roll-forward Major ${resolvePath(
// options.csharpGeneratorPath
//)} --project-path ${outputFolder} ${newProjectOption} ${existingProjectOption} --clear-output-folder ${
// options["clear-output-folder"]
//}${debugFlag}`;
//logger.info(command);
//
//try {
// execSync(command, { stdio: "inherit" });
//} catch (error: any) {
// if (error.message) logger.info(error.message);
// if (error.stderr) logger.error(error.stderr);
// if (error.stdout) logger.verbose(error.stdout);
// throw error;
//}
}
if (!options["save-inputs"]) {
// delete
deleteFile(resolvePath(generatedFolder, tspOutputFileName));
deleteFile(resolvePath(generatedFolder, configurationFileName));
}
}
}
}
function deleteFile(filePath: string) {
fs.unlink(filePath, (err) => {
if (err) {
logger.error(`stderr: ${err}`);
} else {
logger.info(`File ${filePath} is deleted.`);
}
});
}
function prettierOutput(output: string) {
return output + "\n";
}

Просмотреть файл

@ -0,0 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
export * from "./emitter.js";

Просмотреть файл

@ -0,0 +1,295 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import {
SdkClient,
SdkContext,
SdkOperationGroup,
getLibraryName,
listClients,
listOperationGroups,
listOperationsInOperationGroup,
} from "@azure-tools/typespec-client-generator-core";
import {
EmitContext,
NoTarget,
Service,
getDoc,
getNamespaceFullName,
ignoreDiagnostics,
listServices,
} from "@typespec/compiler";
import {
HttpOperation,
getAllHttpServices,
getAuthentication,
getHttpOperation,
getServers,
} from "@typespec/http";
import { getVersions } from "@typespec/versioning";
import { $lib } from "../emitter.js";
import { NetEmitterOptions, resolveOptions } from "../options.js";
import { ClientKind } from "../type/clientKind.js";
import { CodeModel } from "../type/codeModel.js";
import { InputClient } from "../type/inputClient.js";
import { InputConstant } from "../type/inputConstant.js";
import { InputOperation } from "../type/inputOperation.js";
import { InputOperationParameterKind } from "../type/inputOperationParameterKind.js";
import { InputParameter } from "../type/inputParameter.js";
import { InputPrimitiveTypeKind } from "../type/inputPrimitiveTypeKind.js";
import { InputEnumType, InputModelType, InputPrimitiveType } from "../type/inputType.js";
import { InputTypeKind } from "../type/inputTypeKind.js";
import { RequestLocation } from "../type/requestLocation.js";
import { Usage } from "../type/usage.js";
import { getExternalDocs } from "./decorators.js";
import { logger } from "./logger.js";
import { getUsages, navigateModels } from "./model.js";
import { loadOperation } from "./operation.js";
import { processServiceAuthentication } from "./serviceAuthentication.js";
import { resolveServers } from "./typespecServer.js";
import { createContentTypeOrAcceptParameter } from "./utils.js";
export function createModel(sdkContext: SdkContext<NetEmitterOptions>): CodeModel {
const services = listServices(sdkContext.emitContext.program);
if (services.length === 0) {
services.push({
type: sdkContext.emitContext.program.getGlobalNamespaceType(),
});
}
// TODO: support multiple service. Current only chose the first service.
const service = services[0];
const serviceNamespaceType = service.type;
if (serviceNamespaceType === undefined) {
throw Error("Can not emit yaml for a namespace that doesn't exist.");
}
return createModelForService(sdkContext, service);
}
export function createModelForService(
sdkContext: SdkContext<NetEmitterOptions>,
service: Service
): CodeModel {
const emitterOptions = resolveOptions(sdkContext.emitContext);
const program = sdkContext.emitContext.program;
const serviceNamespaceType = service.type;
const apiVersions: Set<string> | undefined = new Set<string>();
let defaultApiVersion: string | undefined = undefined;
const versions = getVersions(program, service.type)[1]?.getVersions();
if (versions && versions.length > 0) {
for (const ver of versions) {
apiVersions.add(ver.value);
}
defaultApiVersion = versions[versions.length - 1].value;
}
const defaultApiVersionConstant: InputConstant | undefined = defaultApiVersion
? {
Type: {
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.String,
IsNullable: false,
} as InputPrimitiveType,
Value: defaultApiVersion,
}
: undefined;
const description = getDoc(program, serviceNamespaceType);
const externalDocs = getExternalDocs(sdkContext, serviceNamespaceType);
const servers = getServers(program, serviceNamespaceType);
const namespace = getNamespaceFullName(serviceNamespaceType) || "client";
const authentication = getAuthentication(program, serviceNamespaceType);
let auth = undefined;
if (authentication) {
auth = processServiceAuthentication(authentication);
}
const modelMap = new Map<string, InputModelType>();
const enumMap = new Map<string, InputEnumType>();
let urlParameters: InputParameter[] | undefined = undefined;
let url: string = "";
const convenienceOperations: HttpOperation[] = [];
//create endpoint parameter from servers
if (servers !== undefined) {
const typespecServers = resolveServers(sdkContext, servers, modelMap, enumMap);
if (typespecServers.length > 0) {
/* choose the first server as endpoint. */
url = typespecServers[0].url;
urlParameters = typespecServers[0].parameters;
}
}
const [services] = getAllHttpServices(program);
const routes = services[0].operations;
if (routes.length === 0) {
$lib.reportDiagnostic(program, {
code: "No-Route",
format: { service: services[0].namespace.name },
target: NoTarget,
});
}
logger.info("routes:" + routes.length);
const clients: InputClient[] = [];
const dpgClients = listClients(sdkContext);
for (const client of dpgClients) {
clients.push(emitClient(client));
addChildClients(sdkContext.emitContext, client, clients);
}
for (const client of clients) {
for (const op of client.Operations) {
const apiVersionIndex = op.Parameters.findIndex(
(value: InputParameter) => value.IsApiVersion
);
if (apiVersionIndex === -1) {
continue;
}
const apiVersionInOperation = op.Parameters[apiVersionIndex];
if (defaultApiVersionConstant !== undefined) {
if (!apiVersionInOperation.DefaultValue?.Value) {
apiVersionInOperation.DefaultValue = defaultApiVersionConstant;
}
} else {
apiVersionInOperation.Kind = InputOperationParameterKind.Method;
}
}
}
navigateModels(sdkContext, serviceNamespaceType, modelMap, enumMap);
const usages = getUsages(sdkContext, convenienceOperations, modelMap);
setUsage(usages, modelMap);
setUsage(usages, enumMap);
const clientModel = {
Name: namespace,
Description: description,
ApiVersions: Array.from(apiVersions.values()),
Enums: Array.from(enumMap.values()),
Models: Array.from(modelMap.values()),
Clients: clients,
Auth: auth,
} as CodeModel;
return clientModel;
function addChildClients(
context: EmitContext<NetEmitterOptions>,
client: SdkClient | SdkOperationGroup,
clients: InputClient[]
) {
const dpgOperationGroups = listOperationGroups(sdkContext, client as SdkClient);
for (const dpgGroup of dpgOperationGroups) {
var subClient = emitClient(dpgGroup, client);
clients.push(subClient);
addChildClients(context, dpgGroup, clients);
}
}
function getClientName(client: SdkClient | SdkOperationGroup): string {
if (client.kind === ClientKind.SdkClient) {
return client.name;
}
var pathParts = client.groupPath.split(".");
if (pathParts?.length >= 3) {
return pathParts.slice(pathParts.length - 2).join("");
}
var clientName = getLibraryName(sdkContext, client.type);
if (
clientName === "Models" &&
resolveOptions(sdkContext.emitContext)["model-namespace"] !== false
) {
$lib.reportDiagnostic(program, {
code: "Invalid-Name",
format: { name: clientName },
target: client.type,
});
return "ModelsOps";
}
return clientName;
}
function emitClient(
client: SdkClient | SdkOperationGroup,
parent?: SdkClient | SdkOperationGroup
): InputClient {
const operations = listOperationsInOperationGroup(sdkContext, client);
let clientDesc = "";
if (operations.length > 0) {
const container = ignoreDiagnostics(getHttpOperation(program, operations[0])).container;
clientDesc = getDoc(program, container) ?? "";
}
const inputClient = {
Name: getClientName(client),
Description: clientDesc,
Operations: [],
Protocol: {},
Creatable: client.kind === ClientKind.SdkClient,
Parent: parent === undefined ? undefined : getClientName(parent),
} as InputClient;
for (const op of operations) {
const httpOperation = ignoreDiagnostics(getHttpOperation(program, op));
const inputOperation: InputOperation = loadOperation(
sdkContext,
httpOperation,
url,
urlParameters,
serviceNamespaceType,
modelMap,
enumMap
);
applyDefaultContentTypeAndAcceptParameter(inputOperation);
inputClient.Operations.push(inputOperation);
if (inputOperation.GenerateConvenienceMethod) convenienceOperations.push(httpOperation);
}
return inputClient;
}
}
function setUsage(
usages: { inputs: string[]; outputs: string[]; roundTrips: string[] },
models: Map<string, InputModelType | InputEnumType>
) {
for (const [name, m] of models) {
if (m.Usage !== undefined && m.Usage !== Usage.None) continue;
if (usages.inputs.includes(name)) {
m.Usage = Usage.Input;
} else if (usages.outputs.includes(name)) {
m.Usage = Usage.Output;
} else if (usages.roundTrips.includes(name)) {
m.Usage = Usage.RoundTrip;
} else {
m.Usage = Usage.None;
}
}
}
function applyDefaultContentTypeAndAcceptParameter(operation: InputOperation): void {
const defaultValue: string = "application/json";
if (
operation.Parameters.some((value) => value.Location === RequestLocation.Body) &&
!operation.Parameters.some((value) => value.IsContentType === true)
) {
operation.Parameters.push(
createContentTypeOrAcceptParameter([defaultValue], "contentType", "Content-Type")
);
operation.RequestMediaTypes = [defaultValue];
}
if (
!operation.Parameters.some(
(value) =>
value.Location === RequestLocation.Header && value.NameInRequest.toLowerCase() === "accept"
)
) {
operation.Parameters.push(
createContentTypeOrAcceptParameter([defaultValue], "accept", "Accept")
);
}
}

Просмотреть файл

@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { SdkContext } from "@azure-tools/typespec-client-generator-core";
import { DecoratedType, Operation, Type } from "@typespec/compiler";
import { ExternalDocs } from "../type/externalDocs.js";
const externalDocsKey = Symbol("externalDocs");
export function getExternalDocs(context: SdkContext, entity: Type): ExternalDocs | undefined {
return context.program.stateMap(externalDocsKey).get(entity);
}
const operationIdsKey = Symbol("operationIds");
/**
* @returns operationId set via the @operationId decorator or `undefined`
*/
export function getOperationId(context: SdkContext, entity: Operation): string | undefined {
return context.program.stateMap(operationIdsKey).get(entity);
}
export function hasDecorator(type: DecoratedType, name: string): boolean {
return type.decorators.find((it) => it.decorator.name === name) !== undefined;
}

Просмотреть файл

@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import * as winston from "winston";
export enum LoggerLevel {
ERROR = "error",
WARN = "warn",
INFO = "info",
DEBUG = "debug",
VERBOSE = "verbose",
}
export var logger: winston.Logger = winston.createLogger({
transports: [
new winston.transports.Console({
level: LoggerLevel.INFO,
}),
],
});

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,386 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { getLroMetadata } from "@azure-tools/typespec-azure-core";
import {
SdkContext,
getAccess,
isApiVersion,
isInternal,
shouldGenerateConvenient,
shouldGenerateProtocol,
} from "@azure-tools/typespec-client-generator-core";
import {
Model,
ModelProperty,
Namespace,
Operation,
getDeprecated,
getDoc,
getSummary,
isErrorModel,
} from "@typespec/compiler";
import { HttpOperation, HttpOperationParameter, HttpOperationResponse } from "@typespec/http";
import { getResourceOperation } from "@typespec/rest";
import { NetEmitterOptions } from "../options.js";
import { BodyMediaType, typeToBodyMediaType } from "../type/bodyMediaType.js";
import { collectionFormatToDelimMap } from "../type/collectionFormat.js";
import { HttpResponseHeader } from "../type/httpResponseHeader.js";
import { InputConstant } from "../type/inputConstant.js";
import { InputOperation } from "../type/inputOperation.js";
import { InputOperationParameterKind } from "../type/inputOperationParameterKind.js";
import { InputParameter } from "../type/inputParameter.js";
import {
InputEnumType,
InputListType,
InputModelType,
InputType,
isInputLiteralType,
isInputModelType,
isInputUnionType,
} from "../type/inputType.js";
import { InputTypeKind } from "../type/inputTypeKind.js";
import { convertLroFinalStateVia } from "../type/operationFinalStateVia.js";
import { OperationLongRunning } from "../type/operationLongRunning.js";
import { OperationPaging } from "../type/operationPaging.js";
import { OperationResponse } from "../type/operationResponse.js";
import { RequestLocation, requestLocationMap } from "../type/requestLocation.js";
import { RequestMethod, parseHttpRequestMethod } from "../type/requestMethod.js";
import { Usage } from "../type/usage.js";
import { getExternalDocs, getOperationId, hasDecorator } from "./decorators.js";
import { logger } from "./logger.js";
import {
getDefaultValue,
getEffectiveSchemaType,
getFormattedType,
getInputType,
} from "./model.js";
import { capitalize, createContentTypeOrAcceptParameter, getTypeName } from "./utils.js";
export function loadOperation(
sdkContext: SdkContext<NetEmitterOptions>,
operation: HttpOperation,
uri: string,
urlParameters: InputParameter[] | undefined = undefined,
serviceNamespaceType: Namespace,
models: Map<string, InputModelType>,
enums: Map<string, InputEnumType>
): InputOperation {
const { path: fullPath, operation: op, verb, parameters: typespecParameters } = operation;
const program = sdkContext.program;
logger.info(`load operation: ${op.name}, path:${fullPath} `);
const resourceOperation = getResourceOperation(program, op);
const desc = getDoc(program, op);
const summary = getSummary(program, op);
const externalDocs = getExternalDocs(sdkContext, op);
const parameters: InputParameter[] = [];
if (urlParameters) {
for (const param of urlParameters) {
parameters.push(param);
}
}
for (const p of typespecParameters.parameters) {
parameters.push(loadOperationParameter(sdkContext, p));
}
if (typespecParameters.body?.parameter) {
parameters.push(loadBodyParameter(sdkContext, typespecParameters.body?.parameter));
} else if (typespecParameters.body?.type) {
const effectiveBodyType = getEffectiveSchemaType(sdkContext, typespecParameters.body.type);
if (effectiveBodyType.kind === "Model") {
let bodyParameter = loadBodyParameter(sdkContext, effectiveBodyType);
if (effectiveBodyType.name === "") {
bodyParameter.Kind = InputOperationParameterKind.Spread;
}
// TODO: remove this after https://github.com/Azure/typespec-azure/issues/69 is resolved
// workaround for alias model
if (isInputModelType(bodyParameter.Type) && bodyParameter.Type.Name === "") {
// give body type a name
bodyParameter.Type.Name = `${capitalize(op.name)}Request`;
var bodyModelType = bodyParameter.Type as InputModelType;
bodyModelType.Usage = Usage.Input;
// update models cache
models.delete("");
models.set(bodyModelType.Name, bodyModelType);
// give body parameter a name
bodyParameter.Name = `${capitalize(op.name)}Request`;
}
parameters.push(bodyParameter);
}
}
const responses: OperationResponse[] = [];
for (const res of operation.responses) {
const operationResponse = loadOperationResponse(sdkContext, res);
if (operationResponse) {
responses.push(operationResponse);
}
if (operationResponse?.ContentTypes && operationResponse.ContentTypes.length > 0) {
const acceptParameter = createContentTypeOrAcceptParameter(
[operationResponse.ContentTypes[0]], // We currently only support one content type per response
"accept",
"Accept"
);
const acceptIndex = parameters.findIndex((p) => p.NameInRequest.toLowerCase() === "accept");
if (acceptIndex > -1) {
parameters.splice(acceptIndex, 1, acceptParameter);
} else {
parameters.push(acceptParameter);
}
}
}
const mediaTypes: string[] = [];
const contentTypeParameter = parameters.find((value) => value.IsContentType);
if (contentTypeParameter) {
if (isInputLiteralType(contentTypeParameter.Type)) {
mediaTypes.push(contentTypeParameter.DefaultValue?.Value);
} else if (isInputUnionType(contentTypeParameter.Type)) {
const mediaTypeValues = contentTypeParameter.Type.UnionItemTypes.map((item) =>
isInputLiteralType(item) ? item.Value : undefined
);
if (mediaTypeValues.some((item) => item === undefined)) {
throw "Media type of content type should be string.";
}
mediaTypes.push(...mediaTypeValues);
}
}
const requestMethod = parseHttpRequestMethod(verb);
const generateProtocol: boolean = shouldGenerateProtocol(sdkContext, op);
const generateConvenience: boolean =
requestMethod !== RequestMethod.PATCH && shouldGenerateConvenient(sdkContext, op);
/* handle lro */
/* handle paging. */
let paging: OperationPaging | undefined = undefined;
for (const res of operation.responses) {
const body = res.responses[0]?.body;
if (body?.type) {
const bodyType = getEffectiveSchemaType(sdkContext, body.type);
if (bodyType.kind === "Model" && hasDecorator(bodyType, "$pagedResult")) {
const itemsProperty = Array.from(bodyType.properties.values()).find((it) =>
hasDecorator(it, "$items")
);
const nextLinkProperty = Array.from(bodyType.properties.values()).find((it) =>
hasDecorator(it, "$nextLink")
);
paging = {
NextLinkName: nextLinkProperty?.name,
ItemName: itemsProperty?.name,
} as OperationPaging;
}
}
}
/* TODO: handle lro */
return {
Name: getTypeName(sdkContext, op),
ResourceName:
resourceOperation?.resourceType.name ??
getOperationGroupName(sdkContext, op, serviceNamespaceType),
Summary: summary,
Deprecated: getDeprecated(program, op),
Description: desc,
Accessibility: isInternal(sdkContext, op) ? "internal" : getAccess(sdkContext, op),
Parameters: parameters,
Responses: responses,
HttpMethod: requestMethod,
RequestBodyMediaType: typeToBodyMediaType(typespecParameters.body?.type),
Uri: uri,
Path: fullPath,
ExternalDocsUrl: externalDocs?.url,
RequestMediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
BufferResponse: true,
LongRunning: loadLongRunningOperation(sdkContext, operation),
Paging: paging,
GenerateProtocolMethod: generateProtocol,
GenerateConvenienceMethod: generateConvenience,
} as InputOperation;
function loadOperationParameter(
context: SdkContext<NetEmitterOptions>,
parameter: HttpOperationParameter
): InputParameter {
const { type: location, name, param } = parameter;
const format = parameter.type === "path" ? undefined : parameter.format;
const typespecType = param.type;
const inputType: InputType = getInputType(
context,
getFormattedType(program, param),
models,
enums
);
let defaultValue = undefined;
const value = getDefaultValue(typespecType);
if (value) {
defaultValue = {
Type: inputType,
Value: value,
} as InputConstant;
}
const requestLocation = requestLocationMap[location];
const isApiVer: boolean = isApiVersion(sdkContext, parameter);
const isContentType: boolean =
requestLocation === RequestLocation.Header && name.toLowerCase() === "content-type";
const kind: InputOperationParameterKind =
isContentType || inputType.Kind === InputTypeKind.Literal
? InputOperationParameterKind.Constant
: isApiVer
? defaultValue
? InputOperationParameterKind.Constant
: InputOperationParameterKind.Client
: InputOperationParameterKind.Method;
return {
Name: getTypeName(sdkContext, param),
NameInRequest: name,
Description: getDoc(program, param),
Type: inputType,
Location: requestLocation,
DefaultValue: defaultValue,
IsRequired: !param.optional,
IsApiVersion: isApiVer,
IsResourceParameter: false,
IsContentType: isContentType,
IsEndpoint: false,
SkipUrlEncoding: false, //TODO: retrieve out value from extension
Explode: (inputType as InputListType).ElementType && format === "multi" ? true : false,
Kind: kind,
ArraySerializationDelimiter: format ? collectionFormatToDelimMap[format] : undefined,
} as InputParameter;
}
function loadBodyParameter(
context: SdkContext<NetEmitterOptions>,
body: ModelProperty | Model
): InputParameter {
const inputType: InputType = getInputType(
context,
getFormattedType(program, body),
models,
enums
);
const requestLocation = RequestLocation.Body;
const kind: InputOperationParameterKind = InputOperationParameterKind.Method;
return {
Name: getTypeName(context, body),
NameInRequest: body.name,
Description: getDoc(program, body),
Type: inputType,
Location: requestLocation,
IsRequired: body.kind === "Model" ? true : !body.optional,
IsApiVersion: false,
IsResourceParameter: false,
IsContentType: false,
IsEndpoint: false,
SkipUrlEncoding: false,
Explode: false,
Kind: kind,
} as InputParameter;
}
function loadOperationResponse(
context: SdkContext<NetEmitterOptions>,
response: HttpOperationResponse
): OperationResponse | undefined {
if (!response.statusCode || response.statusCode === "*") {
return undefined;
}
const status: number[] = [];
status.push(Number(response.statusCode));
//TODO: what to do if more than 1 response?
const body = response.responses[0]?.body;
let type: InputType | undefined = undefined;
if (body?.type) {
const typespecType = getEffectiveSchemaType(context, body.type);
const inputType: InputType = getInputType(
context,
getFormattedType(program, typespecType),
models,
enums
);
type = inputType;
}
const headers = response.responses[0]?.headers;
const responseHeaders: HttpResponseHeader[] = [];
if (headers) {
for (const key of Object.keys(headers)) {
responseHeaders.push({
Name: key,
NameInResponse: headers[key].name,
Description: getDoc(program, headers[key]) ?? "",
Type: getInputType(context, getFormattedType(program, headers[key].type), models, enums),
} as HttpResponseHeader);
}
}
return {
StatusCodes: status,
BodyType: type,
BodyMediaType: BodyMediaType.Json,
Headers: responseHeaders,
IsErrorResponse: isErrorModel(program, response.type),
ContentTypes: body?.contentTypes,
} as OperationResponse;
}
function loadLongRunningOperation(
context: SdkContext<NetEmitterOptions>,
op: HttpOperation
): OperationLongRunning | undefined {
const metadata = getLroMetadata(program, op.operation);
if (metadata === undefined) {
return undefined;
}
var bodyType = undefined;
if (
op.verb !== "delete" &&
metadata.finalResult !== undefined &&
metadata.finalResult !== "void"
) {
const formattedType = getFormattedType(program, metadata.finalEnvelopeResult as Model);
bodyType = getInputType(context, formattedType, models, enums);
}
return {
FinalStateVia: convertLroFinalStateVia(metadata.finalStateVia),
FinalResponse: {
// in swagger, we allow delete to return some meaningful body content
// for now, let assume we don't allow return type
StatusCodes: op.verb === "delete" ? [204] : [200],
BodyType: bodyType,
BodyMediaType: BodyMediaType.Json,
} as OperationResponse,
ResultPath: metadata.finalResultPath,
} as OperationLongRunning;
}
}
function getOperationGroupName(
context: SdkContext,
operation: Operation,
serviceNamespaceType: Namespace
): string {
const explicitOperationId = getOperationId(context, operation);
if (explicitOperationId) {
const ids: string[] = explicitOperationId.split("_");
if (ids.length > 1) {
return ids.slice(0, -2).join("_");
}
}
if (operation.interface) {
return operation.interface.name;
}
let namespace = operation.namespace;
if (!namespace) {
namespace = context.program.checker.getGlobalNamespaceType() ?? serviceNamespaceType;
}
if (namespace) return namespace.name;
else return "";
}

Просмотреть файл

@ -0,0 +1,59 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { ServiceAuthentication } from "@typespec/http";
import { InputApiKeyAuth } from "../type/inputApiKeyAuth.js";
import { InputAuth } from "../type/inputAuth.js";
import { InputOAuth2Auth } from "../type/inputOAuth2Auth.js";
import { logger } from "./logger.js";
export function processServiceAuthentication(authentication: ServiceAuthentication): InputAuth {
const auth = {} as InputAuth;
let scopes: Set<string> | undefined;
for (const option of authentication.options) {
for (const scheme of option.schemes) {
switch (scheme.type) {
case "apiKey":
auth.ApiKey = { Name: scheme.name } as InputApiKeyAuth;
break;
case "oauth2":
for (const flow of scheme.flows) {
if (flow.scopes) {
scopes ??= new Set<string>();
for (const scope of flow.scopes) {
scopes.add(scope.value);
}
}
}
break;
case "http":
const schemeOrApiKeyPrefix = scheme.scheme;
if (schemeOrApiKeyPrefix === "basic") {
logger.warn(`{schemeOrApiKeyPrefix} auth method is currently not supported.`);
} else if (schemeOrApiKeyPrefix === "bearer") {
auth.ApiKey = {
Name: "Authorization",
Prefix: "Bearer",
} as InputApiKeyAuth;
} else {
auth.ApiKey = {
Name: "Authorization",
Prefix: schemeOrApiKeyPrefix,
} as InputApiKeyAuth;
}
break;
default:
throw new Error("Not supported authentication.");
}
}
}
if (scopes) {
auth.OAuth2 = {
Scopes: Array.from(scopes.values()),
} as InputOAuth2Auth;
}
return auth;
}

Просмотреть файл

@ -0,0 +1,123 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { SdkContext } from "@azure-tools/typespec-client-generator-core";
import { getDoc, Type } from "@typespec/compiler";
import { HttpServer } from "@typespec/http";
import { NetEmitterOptions } from "../options.js";
import { InputConstant } from "../type/inputConstant.js";
import { InputOperationParameterKind } from "../type/inputOperationParameterKind.js";
import { InputParameter } from "../type/inputParameter.js";
import { InputPrimitiveTypeKind } from "../type/inputPrimitiveTypeKind.js";
import { InputEnumType, InputModelType, InputPrimitiveType, InputType } from "../type/inputType.js";
import { InputTypeKind } from "../type/inputTypeKind.js";
import { RequestLocation } from "../type/requestLocation.js";
import { getFormattedType, getInputType } from "./model.js";
export interface TypeSpecServer {
url: string;
description?: string;
parameters: InputParameter[];
}
function getDefaultValue(type: Type): any {
switch (type.kind) {
case "String":
return type.value;
case "Number":
return type.value;
case "Boolean":
return type.value;
case "Tuple":
return type.values.map(getDefaultValue);
default:
return undefined;
}
}
export function resolveServers(
context: SdkContext<NetEmitterOptions>,
servers: HttpServer[],
models: Map<string, InputModelType>,
enums: Map<string, InputEnumType>
): TypeSpecServer[] {
return servers.map((server) => {
const parameters: InputParameter[] = [];
let url: string = server.url;
const endpoint: string = url.replace("http://", "").replace("https://", "").split("/")[0];
for (const [name, prop] of server.parameters) {
const isEndpoint: boolean = endpoint === `{${name}}`;
let defaultValue = undefined;
const value = prop.default ? getDefaultValue(prop.default) : "";
const inputType: InputType = isEndpoint
? ({
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.Uri,
IsNullable: false,
} as InputPrimitiveType)
: getInputType(context, getFormattedType(context.program, prop), models, enums);
if (value) {
defaultValue = {
Type: inputType,
Value: value,
} as InputConstant;
}
const variable: InputParameter = {
Name: name,
NameInRequest: name,
Description: getDoc(context.program, prop),
Type: inputType,
Location: RequestLocation.Uri,
IsApiVersion: name.toLowerCase() === "apiversion" || name.toLowerCase() === "api-version",
IsResourceParameter: false,
IsContentType: false,
IsRequired: true,
IsEndpoint: isEndpoint,
SkipUrlEncoding: false,
Explode: false,
Kind: InputOperationParameterKind.Client,
DefaultValue: defaultValue,
};
parameters.push(variable);
}
/* add default server. */
if (server.url && parameters.length === 0) {
const variable: InputParameter = {
Name: "host",
NameInRequest: "host",
Description: server.description,
Type: {
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.String,
IsNullable: false,
} as InputPrimitiveType,
Location: RequestLocation.Uri,
IsApiVersion: false,
IsResourceParameter: false,
IsContentType: false,
IsRequired: true,
IsEndpoint: true,
SkipUrlEncoding: false,
Explode: false,
Kind: InputOperationParameterKind.Client,
DefaultValue: {
Type: {
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.String,
IsNullable: false,
} as InputPrimitiveType,
Value: server.url,
} as InputConstant,
};
url = `{host}`;
parameters.push(variable);
}
return {
url: url,
description: server.description,
parameters,
};
});
}

Просмотреть файл

@ -0,0 +1,98 @@
import {
SdkContext,
getLibraryName,
getSdkModel,
} from "@azure-tools/typespec-client-generator-core";
import {
Enum,
EnumMember,
Model,
ModelProperty,
Namespace,
Operation,
Scalar,
} from "@typespec/compiler";
import { InputConstant } from "../type/inputConstant.js";
import { InputOperationParameterKind } from "../type/inputOperationParameterKind.js";
import { InputParameter } from "../type/inputParameter.js";
import { InputPrimitiveTypeKind } from "../type/inputPrimitiveTypeKind.js";
import { InputPrimitiveType, InputType } from "../type/inputType.js";
import { InputTypeKind } from "../type/inputTypeKind.js";
import { RequestLocation } from "../type/requestLocation.js";
export function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function getNameForTemplate(model: Model): string {
if (model.name !== "" && model.templateMapper && model.templateMapper.args) {
return model.name + model.templateMapper.args.map((it) => (it as Model).name).join("");
}
return model.name;
}
export function getTypeName(
context: SdkContext,
type: Model | Enum | EnumMember | ModelProperty | Scalar | Operation
): string {
var name = getLibraryName(context, type);
if (type.kind !== "Model") return name;
if (type.name === name) {
var templateName = getNameForTemplate(type);
if (templateName === "") {
const sdkModel = getSdkModel(context, type as Model);
return sdkModel.generatedName || sdkModel.name;
}
return templateName;
}
return name;
}
export function getFullNamespaceString(namespace: Namespace | undefined): string {
if (!namespace || !namespace.name) {
return "";
}
let namespaceString: string = namespace.name;
let current: Namespace | undefined = namespace.namespace;
while (current && current.name) {
namespaceString = `${current.name}.${namespaceString}`;
current = current.namespace;
}
return namespaceString;
}
export function createContentTypeOrAcceptParameter(
mediaTypes: string[],
name: string,
nameInRequest: string
): InputParameter {
const isContentType: boolean = nameInRequest.toLowerCase() === "content-type";
const inputType: InputType = {
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.String,
IsNullable: false,
} as InputPrimitiveType;
return {
Name: name,
NameInRequest: nameInRequest,
Type: inputType,
Location: RequestLocation.Header,
IsApiVersion: false,
IsResourceParameter: false,
IsContentType: isContentType,
IsRequired: true,
IsEndpoint: false,
SkipUrlEncoding: false,
Explode: false,
Kind: InputOperationParameterKind.Constant,
DefaultValue:
mediaTypes.length === 1
? ({
Type: inputType,
Value: mediaTypes[0],
} as InputConstant)
: undefined,
} as InputParameter;
}

Просмотреть файл

@ -0,0 +1,153 @@
import { SdkEmitterOptions } from "@azure-tools/typespec-client-generator-core";
import { EmitContext, JSONSchemaType, resolvePath } from "@typespec/compiler";
import { tspOutputFileName } from "./constants.js";
import { LoggerLevel } from "./lib/logger.js";
export type NetEmitterOptions = {
outputFile?: string;
logFile?: string;
namespace: string;
"library-name": string;
"single-top-level-client"?: boolean;
skipSDKGeneration?: boolean;
"unreferenced-types-handling"?: "removeOrInternalize" | "internalize" | "keepAll";
"new-project"?: boolean;
csharpGeneratorPath?: string;
"clear-output-folder"?: boolean;
"save-inputs"?: boolean;
"model-namespace"?: boolean;
"existing-project-folder"?: string;
"keep-non-overloadable-protocol-signature"?: boolean;
debug?: boolean;
"models-to-treat-empty-string-as-null"?: string[];
"additional-intrinsic-types-to-treat-empty-string-as-null"?: string[];
"methods-to-keep-client-default-value"?: string[];
"deserialize-null-collection-as-null-value"?: boolean;
logLevel?: string;
"package-dir"?: string;
"head-as-boolean"?: boolean;
flavor?: string;
"generate-sample-project"?: boolean;
"generate-test-project"?: boolean;
"use-model-reader-writer"?: boolean;
} & SdkEmitterOptions;
export const NetEmitterOptionsSchema: JSONSchemaType<NetEmitterOptions> = {
type: "object",
additionalProperties: false,
properties: {
outputFile: { type: "string", nullable: true },
logFile: { type: "string", nullable: true },
namespace: { type: "string" },
"library-name": { type: "string" },
"single-top-level-client": { type: "boolean", nullable: true },
skipSDKGeneration: { type: "boolean", default: false, nullable: true },
"unreferenced-types-handling": {
type: "string",
enum: ["removeOrInternalize", "internalize", "keepAll"],
nullable: true,
},
"new-project": { type: "boolean", nullable: true },
csharpGeneratorPath: {
type: "string",
default: "fake-location",
nullable: true,
},
"clear-output-folder": { type: "boolean", nullable: true },
"save-inputs": { type: "boolean", nullable: true },
"model-namespace": { type: "boolean", nullable: true },
"generate-protocol-methods": { type: "boolean", nullable: true },
"generate-convenience-methods": { type: "boolean", nullable: true },
"filter-out-core-models": { type: "boolean", nullable: true },
"package-name": { type: "string", nullable: true },
"existing-project-folder": { type: "string", nullable: true },
"keep-non-overloadable-protocol-signature": {
type: "boolean",
nullable: true,
},
debug: { type: "boolean", nullable: true },
"models-to-treat-empty-string-as-null": {
type: "array",
nullable: true,
items: { type: "string" },
},
"additional-intrinsic-types-to-treat-empty-string-as-null": {
type: "array",
nullable: true,
items: { type: "string" },
},
"methods-to-keep-client-default-value": {
type: "array",
nullable: true,
items: { type: "string" },
},
"deserialize-null-collection-as-null-value": {
type: "boolean",
nullable: true,
},
logLevel: {
type: "string",
enum: [
LoggerLevel.ERROR,
LoggerLevel.WARN,
LoggerLevel.INFO,
LoggerLevel.DEBUG,
LoggerLevel.VERBOSE,
],
nullable: true,
},
"package-dir": { type: "string", nullable: true },
"head-as-boolean": { type: "boolean", nullable: true },
flavor: { type: "string", nullable: true },
"generate-sample-project": {
type: "boolean",
nullable: true,
default: true,
},
"generate-test-project": {
type: "boolean",
nullable: true,
default: false,
},
"use-model-reader-writer": { type: "boolean", nullable: true },
},
required: [],
};
const defaultOptions = {
outputFile: tspOutputFileName,
logFile: "log.json",
skipSDKGeneration: false,
"new-project": false,
csharpGeneratorPath: "fake-location",
"clear-output-folder": false,
"save-inputs": false,
"generate-protocol-methods": true,
"generate-convenience-methods": true,
"package-name": undefined,
debug: undefined,
"models-to-treat-empty-string-as-null": undefined,
"additional-intrinsic-types-to-treat-empty-string-as-null": [],
"methods-to-keep-client-default-value": undefined,
"deserialize-null-collection-as-null-value": undefined,
logLevel: LoggerLevel.INFO,
flavor: undefined,
"generate-test-project": false,
};
export function resolveOptions(context: EmitContext<NetEmitterOptions>) {
const emitterOptions = context.options;
const emitterOutputDir = context.emitterOutputDir;
const resolvedOptions = { ...defaultOptions, ...emitterOptions };
const outputFolder = resolveOutputFolder(context);
return {
...resolvedOptions,
outputFile: resolvePath(outputFolder, resolvedOptions.outputFile),
logFile: resolvePath(emitterOutputDir ?? "./tsp-output", resolvedOptions.logFile),
};
}
export function resolveOutputFolder(context: EmitContext<NetEmitterOptions>): string {
return resolvePath(context.emitterOutputDir ?? "./tsp-output");
}

Просмотреть файл

@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { Type } from "@typespec/compiler";
export enum BodyMediaType {
None = "None",
Binary = "Binary",
Form = "Form",
Json = "Json",
Multipart = "Multipart",
Text = "Text",
Xml = "Xml",
}
export function typeToBodyMediaType(type: Type | undefined) {
if (type === undefined) {
return BodyMediaType.None;
}
if (type.kind === "Model") {
return BodyMediaType.Json;
} else if (type.kind === "String") {
return BodyMediaType.Text;
} else if (type.kind === "Scalar" && type.name === "bytes") {
return BodyMediaType.Binary;
}
return BodyMediaType.None;
}

Просмотреть файл

@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
export enum ClientKind {
SdkClient = "SdkClient",
SdkOperationGroup = "SdkOperationGroup",
}

Просмотреть файл

@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { InputAuth } from "./inputAuth.js";
import { InputClient } from "./inputClient.js";
import { InputEnumType, InputModelType } from "./inputType.js";
export interface CodeModel {
Name: string;
Description?: string;
ApiVersions: string[];
Enums: InputEnumType[];
Models: InputModelType[];
Clients: InputClient[];
Auth?: InputAuth;
}

Просмотреть файл

@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
export enum CollectionFormat {
CSV = "csv",
Simple = "simple",
SSV = "ssv",
TSV = "tsv",
Pipes = "pipes",
Multi = "multi",
Form = "form",
}
export const collectionFormatToDelimMap: {
[key: string]: string | undefined;
} = {
[CollectionFormat.CSV.toString()]: ",",
[CollectionFormat.Simple.toString()]: ",", // csv and simple are used interchangeably
[CollectionFormat.SSV.toString()]: " ",
[CollectionFormat.TSV.toString()]: "\t",
[CollectionFormat.Pipes.toString()]: "|",
[CollectionFormat.Multi.toString()]: undefined,
[CollectionFormat.Form.toString()]: undefined, // multi and form are used interchangeably
};

Просмотреть файл

@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
export interface Configuration {
"output-folder": string;
namespace: string;
"library-name": string | null;
flavor?: string;
"single-top-level-client"?: boolean;
"unreferenced-types-handling"?: "removeOrInternalize" | "internalize" | "keepAll";
"model-namespace"?: boolean;
"models-to-treat-empty-string-as-null"?: string[];
"additional-intrinsic-types-to-treat-empty-string-as-null"?: string[];
"methods-to-keep-client-default-value"?: string[];
"keep-non-overloadable-protocol-signature"?: boolean;
"intrinsic-types-to-treat-empty-string-as-null"?: string[];
"head-as-boolean"?: boolean;
"deserialize-null-collection-as-null-value"?: boolean;
"generate-sample-project"?: boolean;
"generate-test-project"?: boolean;
"use-model-reader-writer"?: boolean;
}

Просмотреть файл

@ -0,0 +1,57 @@
import {
SdkContext,
SdkEnumType,
SdkEnumValueType,
} from "@azure-tools/typespec-client-generator-core";
import { Enum, UsageFlags } from "@typespec/compiler";
import { setUsage } from "../lib/model.js";
import { getFullNamespaceString } from "../lib/utils.js";
import { InputEnumTypeValue } from "./inputEnumTypeValue.js";
import { InputEnumType } from "./inputType.js";
import { InputTypeKind } from "./inputTypeKind.js";
import { Usage } from "./usage.js";
export function fromSdkEnumType(
enumType: SdkEnumType,
context: SdkContext,
enums: Map<string, InputEnumType>,
addToCollection: boolean = true
): InputEnumType {
let enumName = enumType.generatedName || enumType.name;
let inputEnumType = enums.get(enumName);
if (inputEnumType === undefined) {
const newInputEnumType: InputEnumType = {
Kind: InputTypeKind.Enum,
Name: enumName,
EnumValueType: enumType.valueType.kind,
AllowedValues: enumType.values.map((v) => fromSdkEnumValueType(v)),
Namespace: getFullNamespaceString((enumType.__raw! as Enum).namespace),
Accessibility: enumType.access,
Deprecated: enumType.deprecation,
Description: enumType.description,
IsExtensible: enumType.isFixed ? false : true,
IsNullable: enumType.nullable,
Usage: fromUsageFlags(enumType.usage),
};
setUsage(context, enumType.__raw! as Enum, newInputEnumType);
if (addToCollection) enums.set(enumName, newInputEnumType);
inputEnumType = newInputEnumType;
}
inputEnumType.IsNullable = enumType.nullable; // TO-DO: https://github.com/Azure/autorest.csharp/issues/4314
return inputEnumType;
}
export function fromSdkEnumValueType(enumValueType: SdkEnumValueType): InputEnumTypeValue {
return {
Name: enumValueType.name,
Value: enumValueType.value,
Description: enumValueType.description,
} as InputEnumTypeValue;
}
export function fromUsageFlags(usage: UsageFlags): Usage {
if (usage === UsageFlags.Input) return Usage.Input;
else if (usage === UsageFlags.Output) return Usage.Output;
else if (usage === (UsageFlags.Input | UsageFlags.Output)) return Usage.RoundTrip;
else return Usage.None;
}

Просмотреть файл

@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
export interface ExternalDocs {
url: string;
description?: string;
}

Просмотреть файл

@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { EncodeData, Type } from "@typespec/compiler";
export interface FormattedType {
type: Type;
format?: string;
encode?: EncodeData;
}

Просмотреть файл

@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { InputType } from "./inputType.js";
export interface HttpResponseHeader {
Name: string;
NameInResponse: string;
Description: string;
Type: InputType;
}

Просмотреть файл

@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
export interface InputApiKeyAuth {
Name: string;
Prefix?: string;
}

Просмотреть файл

@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { InputApiKeyAuth } from "./inputApiKeyAuth.js";
import { InputOAuth2Auth } from "./inputOAuth2Auth.js";
export interface InputAuth {
ApiKey?: InputApiKeyAuth;
OAuth2?: InputOAuth2Auth;
}

Просмотреть файл

@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { InputOperation } from "./inputOperation.js";
import { InputParameter } from "./inputParameter.js";
import { Protocols } from "./protocols.js";
export interface InputClient {
Name: string;
Description?: string;
Operations: InputOperation[];
Protocol?: Protocols;
Parent?: string;
Creatable: boolean;
Parameters?: InputParameter[];
}

Просмотреть файл

@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { InputType } from "./inputType.js";
export interface InputConstant {
Value?: any;
Type: InputType;
}

Просмотреть файл

@ -0,0 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
export interface InputEnumTypeValue {
Name: string;
Value: any;
Description?: string;
}

Просмотреть файл

@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
export enum InputIntrinsicTypeKind {
Error = "ErrorType",
Void = "void",
Never = "never",
Unknown = "unknown",
Null = "null",
}

Просмотреть файл

@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { InputType } from "./inputType.js";
export interface InputModelProperty {
Name: string;
SerializedName: string;
Description: string;
Type: InputType;
IsRequired: boolean;
IsReadOnly: boolean;
IsDiscriminator?: boolean;
}

Просмотреть файл

@ -0,0 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
export interface InputOAuth2Auth {
Scopes?: string[];
}

Просмотреть файл

@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { BodyMediaType } from "./bodyMediaType.js";
import { InputParameter } from "./inputParameter.js";
import { OperationLongRunning } from "./operationLongRunning.js";
import { OperationPaging } from "./operationPaging.js";
import { OperationResponse } from "./operationResponse.js";
import { RequestMethod } from "./requestMethod.js";
export interface Paging {
NextLinkName?: string;
ItemName: string;
NextPageMethod?: string;
}
export interface InputOperation {
Name: string;
ResourceName?: string;
Summary?: string;
Deprecated?: string;
Description?: string;
Accessibility?: string;
Parameters: InputParameter[];
Responses: OperationResponse[];
HttpMethod: RequestMethod;
RequestBodyMediaType: BodyMediaType;
Uri: string;
Path: string;
ExternalDocsUrl?: string;
RequestMediaTypes?: string[];
BufferResponse: boolean;
LongRunning?: OperationLongRunning;
Paging?: OperationPaging;
GenerateProtocolMethod: boolean;
GenerateConvenienceMethod: boolean;
}

Просмотреть файл

@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
export enum InputOperationParameterKind {
Method = "Method",
Client = "Client",
Constant = "Constant",
Flattened = "Flattened",
Spread = "Spread",
Grouped = "Grouped",
}

Просмотреть файл

@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { InputConstant } from "./inputConstant.js";
import { InputOperationParameterKind } from "./inputOperationParameterKind.js";
import { InputType } from "./inputType.js";
import { RequestLocation } from "./requestLocation.js";
//TODO: Define VirtualParameter for HLC
export interface VirtualParameter {}
export interface InputParameter {
Name: string;
NameInRequest: string;
Description?: string;
Type: InputType;
Location: RequestLocation;
DefaultValue?: InputConstant;
VirtualParameter?: VirtualParameter; //for HLC, set null for typespec
GroupedBy?: InputParameter;
Kind: InputOperationParameterKind;
IsRequired: boolean;
IsApiVersion: boolean;
IsResourceParameter: boolean;
IsContentType: boolean;
IsEndpoint: boolean;
SkipUrlEncoding: boolean;
Explode: boolean;
ArraySerializationDelimiter?: string;
HeaderCollectionPrefix?: string;
}

Просмотреть файл

@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
export enum InputPrimitiveTypeKind {
AzureLocation = "AzureLocation",
Boolean = "Boolean",
BinaryData = "BinaryData",
Bytes = "Bytes",
BytesBase64Url = "BytesBase64Url",
ContentType = "ContentType",
Date = "Date",
DateTime = "DateTime",
DateTimeISO8601 = "DateTimeISO8601",
DateTimeRFC1123 = "DateTimeRFC1123",
DateTimeRFC3339 = "DateTimeRFC3339",
DateTimeRFC7231 = "DateTimeRFC7231",
DateTimeUnix = "DateTimeUnix",
Decimal = "Decimal",
Decimal128 = "Decimal128",
DurationISO8601 = "DurationISO8601",
DurationConstant = "DurationConstant",
DurationSeconds = "DurationSeconds",
DurationSecondsFloat = "DurationSecondsFloat",
ETag = "Etag",
Float32 = "Float32",
Float64 = "Float64",
Float128 = "Float128",
Guid = "Guid",
Int32 = "Int32",
Int64 = "Int64",
SafeInt = "SafeInt",
IPAddress = "IPAddress",
Object = "Object",
RequestMethod = "RequestMethod",
ResourceIdentifier = "ResourceIdentifier",
ResourceType = "ResourceType",
Stream = "Stream",
String = "String",
Time = "Time",
Uri = "Uri",
Enum = "Enum",
SByte = "SByte", //int8
Byte = "Byte", //uint8
UnKnownKind = "UnknownKind",
}

Просмотреть файл

@ -0,0 +1,107 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { InputEnumTypeValue } from "./inputEnumTypeValue.js";
import { InputIntrinsicTypeKind } from "./inputIntrinsicTypeKind.js";
import { InputModelProperty } from "./inputModelProperty.js";
import { InputPrimitiveTypeKind } from "./inputPrimitiveTypeKind.js";
import { InputTypeKind } from "./inputTypeKind.js";
export interface InputType {
Name: string;
Kind: InputTypeKind;
IsNullable: boolean;
}
export interface InputPrimitiveType extends InputType {
Name: InputPrimitiveTypeKind;
}
export interface InputLiteralType extends InputType {
Kind: InputTypeKind.Literal;
Name: InputTypeKind.Literal; // literal type does not really have a name right now, we just use its kind
LiteralValueType: InputType;
Value: any;
}
export function isInputLiteralType(type: InputType): type is InputLiteralType {
return type.Kind === InputTypeKind.Literal;
}
export interface InputUnionType extends InputType {
Kind: InputTypeKind.Union;
Name: InputTypeKind.Union; // union type does not really have a name right now, we just use its kind
UnionItemTypes: InputType[];
}
export function isInputUnionType(type: InputType): type is InputUnionType {
return type.Kind === InputTypeKind.Union;
}
export interface InputModelType extends InputType {
Kind: InputTypeKind.Model;
Name: string;
Namespace?: string;
Accessibility?: string;
Deprecated?: string;
Description?: string;
Usage: string;
Properties: InputModelProperty[];
BaseModel?: InputModelType;
DiscriminatorPropertyName?: string;
DiscriminatorValue?: string;
DerivedModels?: InputModelType[];
InheritedDictionaryType?: InputDictionaryType;
}
export function isInputModelType(type: InputType): type is InputModelType {
return type.Kind === InputTypeKind.Model;
}
export interface InputEnumType extends InputType {
Kind: InputTypeKind.Enum;
Name: string;
EnumValueType: string;
AllowedValues: InputEnumTypeValue[];
Namespace?: string;
Accessibility?: string;
Deprecated?: string;
Description?: string;
IsExtensible: boolean;
Usage: string;
}
export function isInputEnumType(type: InputType): type is InputEnumType {
return type.Kind === InputTypeKind.Enum;
}
export interface InputListType extends InputType {
Kind: InputTypeKind.Array;
Name: InputTypeKind.Array; // array type does not really have a name right now, we just use its kind
ElementType: InputType;
}
export function isInputListType(type: InputType): type is InputListType {
return type.Kind === InputTypeKind.Array;
}
export interface InputDictionaryType extends InputType {
Kind: InputTypeKind.Dictionary;
Name: InputTypeKind.Dictionary; // dictionary type does not really have a name right now, we just use its kind
KeyType: InputType;
ValueType: InputType;
}
export function isInputDictionaryType(type: InputType): type is InputDictionaryType {
return type.Kind === InputTypeKind.Dictionary;
}
export interface InputIntrinsicType extends InputType {
Kind: InputTypeKind.Intrinsic;
Name: InputIntrinsicTypeKind;
IsNullable: false;
}
export function isInputIntrinsicType(type: InputType): type is InputIntrinsicType {
return type.Kind === InputTypeKind.Intrinsic;
}

Просмотреть файл

@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
export enum InputTypeKind {
Primitive = "Primitive",
Literal = "Literal",
Union = "Union",
Model = "Model",
Enum = "Enum",
Array = "Array",
Dictionary = "Dictionary",
Intrinsic = "Intrinsic",
}

Просмотреть файл

@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
export enum InputTypeSerializationFormat {
Default,
Base64Url,
Byte,
Date,
Time,
DateTime,
DateTimeUnix,
DateTimeRFC1123,
Duration,
DurationConstant,
Json,
Xml,
}

Просмотреть файл

@ -0,0 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
export interface InputTypeValue {
Name: string;
Value: string;
Description?: string;
}

Просмотреть файл

@ -0,0 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
export interface LiteralTypeContext {
ModelName: string;
PropertyName: string;
Namespace?: string;
}

Просмотреть файл

@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { FinalStateValue } from "@azure-tools/typespec-azure-core";
export enum OperationFinalStateVia {
AzureAsyncOperation,
Location,
OriginalUri,
OperationLocation,
CustomLink,
CustomOperationReference,
NoResult,
}
export function convertLroFinalStateVia(finalStateValue: FinalStateValue): OperationFinalStateVia {
switch (finalStateValue) {
case FinalStateValue.azureAsyncOperation:
return OperationFinalStateVia.AzureAsyncOperation;
// TODO: we don't have implementation of custom-link and custom-operation-reference yet
// case FinalStateValue.customLink:
// return OperationFinalStateVia.CustomLink;
// And right now some existing API specs are not correctly defined so that they are parsed
// into `custom-operation-reference` which should be `operation-location`.
// so let's fallback `custom-operation-reference` into `operation-location` as a work-around
case FinalStateValue.customOperationReference:
return OperationFinalStateVia.OperationLocation;
case FinalStateValue.location:
return OperationFinalStateVia.Location;
case FinalStateValue.originalUri:
return OperationFinalStateVia.OriginalUri;
case FinalStateValue.operationLocation:
return OperationFinalStateVia.OperationLocation;
default:
throw `Unsupported LRO final state value: ${finalStateValue}`;
}
}

Просмотреть файл

@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { OperationFinalStateVia } from "./operationFinalStateVia.js";
import { OperationResponse } from "./operationResponse.js";
export interface OperationLongRunning {
FinalStateVia: OperationFinalStateVia;
FinalResponse: OperationResponse;
ResultPath?: string;
}

Просмотреть файл

@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { InputOperation } from "./inputOperation.js";
export interface OperationPaging {
NextLinkName?: string;
ItemName?: string;
NextLinkOperation?: InputOperation;
NextLinkOperationRef?: (p: any) => void;
}

Просмотреть файл

@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { BodyMediaType } from "./bodyMediaType.js";
import { HttpResponseHeader } from "./httpResponseHeader.js";
import { InputType } from "./inputType.js";
export interface OperationResponse {
StatusCodes: number[];
BodyType?: InputType;
BodyMediaType: BodyMediaType;
Headers: HttpResponseHeader[];
ContentTypes?: string[];
IsErrorResponse: boolean;
}

Просмотреть файл

@ -0,0 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
export interface Protocols {}

Просмотреть файл

@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
export enum RequestLocation {
None = "",
Uri = "Uri",
Path = "Path",
Query = "Query",
Header = "Header",
Body = "Body",
}
export const requestLocationMap: { [key: string]: RequestLocation } = {
path: RequestLocation.Path,
query: RequestLocation.Query,
header: RequestLocation.Header,
body: RequestLocation.Body,
uri: RequestLocation.Uri,
};

Просмотреть файл

@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
export enum RequestMethod {
GET = "GET",
POST = "POST",
PUT = "PUT",
PATCH = "PATCH",
DELETE = "DELETE",
HEAD = "HEAD",
OPTIONS = "OPTIONS",
TRACE = "TRACE",
NONE = "",
}
export function parseHttpRequestMethod(method: string): RequestMethod {
if (method.length === 3) {
if (method.toLowerCase() === "get") return RequestMethod.GET;
if (method.toLowerCase() === "put") return RequestMethod.PUT;
} else if (method.length === 4) {
if (method.toLowerCase() === "post") return RequestMethod.POST;
if (method.toLowerCase() === "head") return RequestMethod.HEAD;
} else {
if (method.toLowerCase() === "patch") return RequestMethod.PATCH;
if (method.toLowerCase() === "delete") return RequestMethod.DELETE;
if (method.toLowerCase() === "options") return RequestMethod.OPTIONS;
if (method.toLowerCase() === "trace") return RequestMethod.TRACE;
}
return RequestMethod.NONE;
}

Просмотреть файл

@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
export enum Usage {
None = "None",
Input = "Input",
Output = "Output",
RoundTrip = "RoundTrip",
}

Просмотреть файл

@ -0,0 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
export enum ValidationType {
None,
AssertNotNull,
AssertNotNullOrEmpty,
}

Просмотреть файл

@ -0,0 +1,258 @@
import { TestHost } from "@typespec/compiler/testing";
import { getAllHttpServices } from "@typespec/http";
import assert from "assert";
import isEqual from "lodash.isequal";
import { loadOperation } from "../../src/lib/operation.js";
import { InputPrimitiveTypeKind } from "../../src/type/inputPrimitiveTypeKind.js";
import { InputEnumType, InputModelType, InputPrimitiveType } from "../../src/type/inputType.js";
import { InputTypeKind } from "../../src/type/inputTypeKind.js";
import {
createEmitterContext,
createEmitterTestHost,
createNetSdkContext,
navigateModels,
typeSpecCompile,
} from "./utils/TestUtil.js";
describe("Test encode duration", () => {
let runner: TestHost;
beforeEach(async () => {
runner = await createEmitterTestHost();
});
it("encode iso8601 for duration query parameter ", async () => {
const program = await typeSpecCompile(
`
op test(
@query
@encode(DurationKnownEncoding.ISO8601)
input: duration
): NoContentResponse;
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const modelMap = new Map<string, InputModelType>();
const enumMap = new Map<string, InputEnumType>();
const operation = loadOperation(
sdkContext,
services[0].operations[0],
"",
[],
services[0].namespace,
modelMap,
enumMap
);
assert(
isEqual(
{
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.DurationISO8601,
IsNullable: false,
} as InputPrimitiveType,
operation.Parameters[0].Type
)
);
});
it("encode seconds-int32 for duration query parameter ", async () => {
const program = await typeSpecCompile(
`
op test(
@query
@encode(DurationKnownEncoding.seconds, int32)
input: duration
): NoContentResponse;
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const modelMap = new Map<string, InputModelType>();
const enumMap = new Map<string, InputEnumType>();
const operation = loadOperation(
sdkContext,
services[0].operations[0],
"",
[],
services[0].namespace,
modelMap,
enumMap
);
assert(
isEqual(
{
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.DurationSeconds,
IsNullable: false,
} as InputPrimitiveType,
operation.Parameters[0].Type
)
);
});
it("encode seconds-float for duration query parameter ", async () => {
const program = await typeSpecCompile(
`
op test(
@query
@encode(DurationKnownEncoding.seconds, float)
input: duration
): NoContentResponse;
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const modelMap = new Map<string, InputModelType>();
const enumMap = new Map<string, InputEnumType>();
const operation = loadOperation(
sdkContext,
services[0].operations[0],
"",
[],
services[0].namespace,
modelMap,
enumMap
);
assert(
isEqual(
{
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.DurationSecondsFloat,
IsNullable: false,
} as InputPrimitiveType,
operation.Parameters[0].Type
)
);
});
it("encode iso8601 on duration model property", async () => {
const program = await typeSpecCompile(
`
@doc("This is a model.")
model ISO8601DurationProperty {
@encode(DurationKnownEncoding.ISO8601)
value: duration;
}
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const modelMap = new Map<string, InputModelType>();
const enumMap = new Map<string, InputEnumType>();
navigateModels(sdkContext, services[0].namespace, modelMap, enumMap);
const durationProperty = modelMap.get("ISO8601DurationProperty");
assert(durationProperty !== undefined);
assert(
isEqual(
{
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.DurationISO8601,
IsNullable: false,
} as InputPrimitiveType,
durationProperty.Properties[0].Type
)
);
});
it("encode iso8601 on duration model property", async () => {
const program = await typeSpecCompile(
`
@doc("This is a model.")
model ISO8601DurationProperty {
@encode(DurationKnownEncoding.ISO8601)
value: duration;
}
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const modelMap = new Map<string, InputModelType>();
const enumMap = new Map<string, InputEnumType>();
navigateModels(sdkContext, services[0].namespace, modelMap, enumMap);
const durationProperty = modelMap.get("ISO8601DurationProperty");
assert(durationProperty !== undefined);
assert(
isEqual(
{
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.DurationISO8601,
IsNullable: false,
} as InputPrimitiveType,
durationProperty.Properties[0].Type
)
);
});
it("encode seconds-int32 on duration model property", async () => {
const program = await typeSpecCompile(
`
@doc("This is a model.")
model Int32SecondsDurationProperty {
@encode(DurationKnownEncoding.seconds, int32)
value: duration;
}
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const modelMap = new Map<string, InputModelType>();
const enumMap = new Map<string, InputEnumType>();
navigateModels(sdkContext, services[0].namespace, modelMap, enumMap);
const durationProperty = modelMap.get("Int32SecondsDurationProperty");
assert(durationProperty !== undefined);
assert(
isEqual(
{
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.DurationSeconds,
IsNullable: false,
},
durationProperty.Properties[0].Type
)
);
});
it("encode seconds-int32 on duration model property", async () => {
const program = await typeSpecCompile(
`
@doc("This is a model.")
model FloatSecondsDurationProperty {
@encode(DurationKnownEncoding.seconds, float)
value: duration;
}
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const modelMap = new Map<string, InputModelType>();
const enumMap = new Map<string, InputEnumType>();
navigateModels(sdkContext, services[0].namespace, modelMap, enumMap);
const durationProperty = modelMap.get("FloatSecondsDurationProperty");
assert(durationProperty !== undefined);
assert(
isEqual(
{
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.DurationSecondsFloat,
IsNullable: false,
},
durationProperty.Properties[0].Type
)
);
});
});

Просмотреть файл

@ -0,0 +1,771 @@
import { TestHost } from "@typespec/compiler/testing";
import { getAllHttpServices } from "@typespec/http";
import assert from "assert";
import isEqual from "lodash.isequal";
import { createModel } from "../../src/lib/clientModelBuilder.js";
import { InputIntrinsicTypeKind } from "../../src/type/inputIntrinsicTypeKind.js";
import { InputModelProperty } from "../../src/type/inputModelProperty.js";
import { InputPrimitiveTypeKind } from "../../src/type/inputPrimitiveTypeKind.js";
import { InputDictionaryType, InputEnumType, InputModelType } from "../../src/type/inputType.js";
import { InputTypeKind } from "../../src/type/inputTypeKind.js";
import {
createEmitterContext,
createEmitterTestHost,
createNetSdkContext,
navigateModels,
typeSpecCompile,
} from "./utils/TestUtil.js";
describe("Discriminator property", () => {
let runner: TestHost;
beforeEach(async () => {
runner = await createEmitterTestHost();
});
it("Base model has discriminator property", async () => {
const program = await typeSpecCompile(
`
@doc("The base Pet model")
@discriminator("kind")
model Pet {
@doc("The name of the pet")
name: string;
}
@doc("The cat")
model Cat extends Pet {
kind: "cat";
@doc("Meow")
meow: string;
}
@doc("The dog")
model Dog extends Pet {
kind: "dog";
@doc("Woof")
woof: string;
}
op test(@body input: Pet): Pet;
`,
runner
);
runner.compileAndDiagnose;
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const root = createModel(sdkContext);
const models = root.Models;
const petModel = models.find((m) => m.Name === "Pet");
const catModel = models.find((m) => m.Name === "Cat");
const dogModel = models.find((m) => m.Name === "Dog");
// assert the discriminator property name
assert(
isEqual("kind", petModel?.DiscriminatorPropertyName),
`Discriminator property name is not correct, got ${petModel?.DiscriminatorPropertyName}`
);
// assert we have a property corresponding to the discriminator property above on the base model
const discriminatorProperty = petModel?.Properties.find(
(p) => p.Name === petModel?.DiscriminatorPropertyName
);
assert(
isEqual(
{
Name: "kind",
SerializedName: "kind",
Type: {
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.String,
IsNullable: false,
},
IsRequired: true,
IsReadOnly: false,
IsDiscriminator: true,
Description: "Discriminator",
} as InputModelProperty,
discriminatorProperty
),
`Discriminator property is not correct, got ${JSON.stringify(discriminatorProperty)}`
);
// assert we will NOT have a DiscriminatorPropertyName on the derived models
assert(
catModel?.DiscriminatorPropertyName === undefined,
"Cat model should not have the discriminator property name"
);
assert(
dogModel?.DiscriminatorPropertyName === undefined,
"Dog model should not have the discriminator property name"
);
// assert we will NOT have a property corresponding to the discriminator property on the derived models
const catDiscriminatorProperty = catModel?.Properties.find(
(p) => p.Name === petModel?.DiscriminatorPropertyName
);
const dogDiscriminatorProperty = dogModel?.Properties.find(
(p) => p.Name === petModel?.DiscriminatorPropertyName
);
assert(
catDiscriminatorProperty === undefined,
"Cat model should not have the discriminator property"
);
assert(
dogDiscriminatorProperty === undefined,
"Dog model should not have the discriminator property"
);
});
it("Discriminator property is enum with no enum value defined", async () => {
const program = await typeSpecCompile(
`
@doc("The pet kind")
enum PetKind {
Cat,
Dog,
}
@doc("The base Pet model")
@discriminator("kind")
model Pet {
@doc("The kind of the pet")
kind: PetKind;
@doc("The name of the pet")
name: string;
}
@doc("The cat")
model Cat extends Pet{
kind: PetKind.Cat;
@doc("Meow")
meow: string;
}
@doc("The dog")
model Dog extends Pet{
kind: PetKind.Dog;
@doc("Woof")
woof: string;
}
op test(@body input: Pet): Pet;
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const modelMap = new Map<string, InputModelType>();
const enumMap = new Map<string, InputEnumType>();
navigateModels(sdkContext, services[0].namespace, modelMap, enumMap);
const pet = modelMap.get("Pet");
assert(pet !== undefined);
// assert the discriminator property name
assert(
isEqual("kind", pet?.DiscriminatorPropertyName),
`Discriminator property name is not correct, got ${pet?.DiscriminatorPropertyName}`
);
// assert we have a property corresponding to the discriminator property above on the base model
const discriminatorProperty = pet?.Properties.find(
(p) => p.Name === pet?.DiscriminatorPropertyName
);
assert(
isEqual(
{
Name: "kind",
SerializedName: "kind",
Description: "The kind of the pet",
Type: {
Kind: InputTypeKind.Enum,
Name: "PetKind",
Namespace: "Azure.Csharp.Testing",
Description: "The pet kind",
Accessibility: undefined,
Deprecated: undefined,
EnumValueType: "String",
AllowedValues: [
{
Name: "Cat",
Value: "Cat",
Description: undefined,
},
{
Name: "Dog",
Value: "Dog",
Description: undefined,
},
],
IsExtensible: false,
IsNullable: false,
Usage: "None",
},
IsRequired: true,
IsReadOnly: false,
IsDiscriminator: true,
} as InputModelProperty,
discriminatorProperty
),
`Discriminator property is not correct, got ${JSON.stringify(discriminatorProperty)}`
);
// verify derived model Cat
const cat = modelMap.get("Cat");
assert(cat !== undefined);
assert(cat.DiscriminatorValue === "Cat");
assert(cat.BaseModel === pet);
// assert we will NOT have a DiscriminatorPropertyName on the derived models
assert(
cat.DiscriminatorPropertyName === undefined,
"Cat model should not have the discriminator property name"
);
// assert we will NOT have a property corresponding to the discriminator property on the derived models
const catDiscriminatorProperty = cat.Properties.find(
(p) => p.Name === pet.DiscriminatorPropertyName
);
assert(
catDiscriminatorProperty === undefined,
"Cat model should not have the discriminator property"
);
// verify derived model Dog
const dog = modelMap.get("Dog");
assert(dog !== undefined);
assert(dog.DiscriminatorValue === "Dog");
assert(dog.BaseModel === pet);
// assert we will NOT have a DiscriminatorPropertyName on the derived models
assert(
dog.DiscriminatorPropertyName === undefined,
"Dog model should not have the discriminator property name"
);
// assert we will NOT have a property corresponding to the discriminator property on the derived models
const dogDiscriminatorProperty = dog.Properties.find(
(p) => p.Name === pet.DiscriminatorPropertyName
);
assert(
dogDiscriminatorProperty === undefined,
"Dog model should not have the discriminator property"
);
});
it("Discriminator property is enum with enum value defined", async () => {
const program = await typeSpecCompile(
`
@doc("The pet kind")
enum PetKind {
Cat : "cat",
Dog : "dog",
}
@doc("The base Pet model")
@discriminator("kind")
model Pet {
@doc("The kind of the pet")
kind: PetKind;
@doc("The name of the pet")
name: string;
}
@doc("The cat")
model Cat extends Pet{
kind: PetKind.Cat;
@doc("Meow")
meow: string;
}
@doc("The dog")
model Dog extends Pet{
kind: PetKind.Dog;
@doc("Woof")
woof: string;
}
op test(@body input: Pet): Pet;
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const modelMap = new Map<string, InputModelType>();
const enumMap = new Map<string, InputEnumType>();
navigateModels(sdkContext, services[0].namespace, modelMap, enumMap);
const pet = modelMap.get("Pet");
assert(pet !== undefined);
// assert the discriminator property name
assert(
isEqual("kind", pet?.DiscriminatorPropertyName),
`Discriminator property name is not correct, got ${pet?.DiscriminatorPropertyName}`
);
// assert we have a property corresponding to the discriminator property above on the base model
const discriminatorProperty = pet?.Properties.find(
(p) => p.Name === pet?.DiscriminatorPropertyName
);
assert(
isEqual(
{
Name: "kind",
SerializedName: "kind",
Description: "The kind of the pet",
Type: {
Kind: InputTypeKind.Enum,
Name: "PetKind",
Namespace: "Azure.Csharp.Testing",
Accessibility: undefined,
Deprecated: undefined,
Description: "The pet kind",
EnumValueType: "String",
AllowedValues: [
{
Name: "Cat",
Value: "cat",
Description: undefined,
},
{
Name: "Dog",
Value: "dog",
Description: undefined,
},
],
IsExtensible: false,
IsNullable: false,
Usage: "None",
},
IsRequired: true,
IsReadOnly: false,
IsDiscriminator: true,
} as InputModelProperty,
discriminatorProperty
),
`Discriminator property is not correct, got ${JSON.stringify(discriminatorProperty)}`
);
// verify derived model Cat
const cat = modelMap.get("Cat");
assert(cat !== undefined);
assert(cat.DiscriminatorValue === "cat");
assert(cat.BaseModel === pet);
// assert we will NOT have a DiscriminatorPropertyName on the derived models
assert(
cat.DiscriminatorPropertyName === undefined,
"Cat model should not have the discriminator property name"
);
// assert we will NOT have a property corresponding to the discriminator property on the derived models
const catDiscriminatorProperty = cat.Properties.find(
(p) => p.Name === pet.DiscriminatorPropertyName
);
assert(
catDiscriminatorProperty === undefined,
"Cat model should not have the discriminator property"
);
// verify derived model Dog
const dog = modelMap.get("Dog");
assert(dog !== undefined);
assert(dog.DiscriminatorValue === "dog");
assert(dog.BaseModel === pet);
// assert we will NOT have a DiscriminatorPropertyName on the derived models
assert(
dog.DiscriminatorPropertyName === undefined,
"Dog model should not have the discriminator property name"
);
// assert we will NOT have a property corresponding to the discriminator property on the derived models
const dogDiscriminatorProperty = dog.Properties.find(
(p) => p.Name === pet.DiscriminatorPropertyName
);
assert(
dogDiscriminatorProperty === undefined,
"Dog model should not have the discriminator property"
);
});
});
describe("Additional Properties property should work with extends syntax", () => {
let runner: TestHost;
beforeEach(async () => {
runner = await createEmitterTestHost();
});
it("Model extends Record should have additional properties property", async () => {
const program = await typeSpecCompile(
`
@doc("Extends Record<unknown>")
model ExtendsUnknown extends Record<unknown> {
@doc("The name.")
name: string;
}
@doc("Extends Record<string>")
model ExtendsString extends Record<string> {
@doc("The name.")
name: string;
}
@doc("Extends Record<int32>")
model ExtendsInt32 extends Record<int32> {
@doc("The name.")
name: int32;
}
@doc("Extends Record<Foo>")
model ExtendsFoo extends Record<Foo> {
@doc("The name.")
name: Foo;
}
@doc("Extends Record<Foo[]>")
model ExtendsFooArray extends Record<Foo[]> {
@doc("The name.")
name: Foo[];
}
@doc("The Foo")
model Foo {
@doc("The name.")
name: string;
}
@route("/op1")
op op1(@body body: ExtendsUnknown): ExtendsUnknown;
@route("/op2")
op op2(@body body: ExtendsString): ExtendsString;
@route("/op3")
op op3(@body body: ExtendsInt32): ExtendsInt32;
@route("/op4")
op op4(@body body: ExtendsFoo): ExtendsFoo;
@route("/op5")
op op5(@body body: ExtendsFooArray): ExtendsFooArray;
`,
runner
);
runner.compileAndDiagnose;
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const root = createModel(sdkContext);
const models = root.Models;
const extendsUnknownModel = models.find((m) => m.Name === "ExtendsUnknown");
const extendsStringModel = models.find((m) => m.Name === "ExtendsString");
const extendsInt32Model = models.find((m) => m.Name === "ExtendsInt32");
const extendsFooModel = models.find((m) => m.Name === "ExtendsFoo");
const extendsFooArrayModel = models.find((m) => m.Name === "ExtendsFooArray");
const fooModel = models.find((m) => m.Name === "Foo");
assert(extendsUnknownModel !== undefined);
assert(extendsStringModel !== undefined);
assert(extendsInt32Model !== undefined);
assert(extendsFooModel !== undefined);
assert(extendsFooArrayModel !== undefined);
// assert the inherited dictionary type is expected
assert(
isEqual(
{
Kind: InputTypeKind.Dictionary,
Name: InputTypeKind.Dictionary,
IsNullable: false,
KeyType: {
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.String,
IsNullable: false,
},
ValueType: {
Kind: InputTypeKind.Intrinsic,
Name: InputIntrinsicTypeKind.Unknown,
IsNullable: false,
},
} as InputDictionaryType,
extendsUnknownModel.InheritedDictionaryType
),
`Inherited dictionary type is not correct, got ${JSON.stringify(
extendsUnknownModel.InheritedDictionaryType
)}`
);
assert(
isEqual(
{
Kind: InputTypeKind.Dictionary,
Name: InputTypeKind.Dictionary,
IsNullable: false,
KeyType: {
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.String,
IsNullable: false,
},
ValueType: {
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.String,
IsNullable: false,
},
} as InputDictionaryType,
extendsStringModel.InheritedDictionaryType
),
`Inherited dictionary type is not correct, got ${JSON.stringify(
extendsStringModel.InheritedDictionaryType
)}`
);
assert(
isEqual(
{
Kind: InputTypeKind.Dictionary,
Name: InputTypeKind.Dictionary,
IsNullable: false,
KeyType: {
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.String,
IsNullable: false,
},
ValueType: {
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.Int32,
IsNullable: false,
},
} as InputDictionaryType,
extendsInt32Model.InheritedDictionaryType
),
`Inherited dictionary type is not correct, got ${JSON.stringify(
extendsInt32Model.InheritedDictionaryType
)}`
);
assert(
isEqual(
{
Kind: InputTypeKind.Dictionary,
Name: InputTypeKind.Dictionary,
IsNullable: false,
KeyType: {
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.String,
IsNullable: false,
},
ValueType: fooModel,
} as InputDictionaryType,
extendsFooModel.InheritedDictionaryType
),
`Inherited dictionary type is not correct, got ${JSON.stringify(
extendsFooModel.InheritedDictionaryType
)}`
);
assert(
isEqual(
{
Kind: InputTypeKind.Dictionary,
Name: InputTypeKind.Dictionary,
IsNullable: false,
KeyType: {
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.String,
IsNullable: false,
},
ValueType: {
Kind: InputTypeKind.Array,
Name: InputTypeKind.Array,
ElementType: fooModel,
IsNullable: false,
},
} as InputDictionaryType,
extendsFooArrayModel.InheritedDictionaryType
),
`Inherited dictionary type is not correct, got ${JSON.stringify(
extendsFooArrayModel.InheritedDictionaryType
)}`
);
});
});
describe("Additional Properties property should work with is syntax", () => {
let runner: TestHost;
beforeEach(async () => {
runner = await createEmitterTestHost();
});
it("Model is Record should have additional properties property", async () => {
const program = await typeSpecCompile(
`
@doc("Is Record<unknown>")
model IsUnknown is Record<unknown> {
@doc("The name.")
name: string;
}
@doc("Is Record<string>")
model IsString is Record<string> {
@doc("The name.")
name: string;
}
@doc("Is Record<int32>")
model IsInt32 is Record<int32> {
@doc("The name.")
name: int32;
}
@doc("Is Record<Foo>")
model IsFoo is Record<Foo> {
@doc("The name.")
name: Foo;
}
@doc("Is Record<Foo[]>")
model IsFooArray is Record<Foo[]> {
@doc("The name.")
name: Foo[];
}
@doc("The Foo")
model Foo {
@doc("The name.")
name: string;
}
@route("/op1")
op op1(@body body: IsUnknown): IsUnknown;
@route("/op2")
op op2(@body body: IsString): IsString;
@route("/op3")
op op3(@body body: IsInt32): IsInt32;
@route("/op4")
op op4(@body body: IsFoo): IsFoo;
@route("/op5")
op op5(@body body: IsFooArray): IsFooArray;
`,
runner
);
runner.compileAndDiagnose;
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const root = createModel(sdkContext);
const models = root.Models;
const isUnknownModel = models.find((m) => m.Name === "IsUnknown");
const isStringModel = models.find((m) => m.Name === "IsString");
const isInt32Model = models.find((m) => m.Name === "IsInt32");
const isFooModel = models.find((m) => m.Name === "IsFoo");
const isFooArrayModel = models.find((m) => m.Name === "IsFooArray");
const fooModel = models.find((m) => m.Name === "Foo");
assert(isUnknownModel !== undefined);
assert(isStringModel !== undefined);
assert(isInt32Model !== undefined);
assert(isFooModel !== undefined);
assert(isFooArrayModel !== undefined);
// assert the inherited dictionary type is expected
assert(
isEqual(
{
Kind: InputTypeKind.Dictionary,
Name: InputTypeKind.Dictionary,
IsNullable: false,
KeyType: {
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.String,
IsNullable: false,
},
ValueType: {
Kind: InputTypeKind.Intrinsic,
Name: InputIntrinsicTypeKind.Unknown,
IsNullable: false,
},
} as InputDictionaryType,
isUnknownModel.InheritedDictionaryType
),
`Inherited dictionary type is not correct, got ${JSON.stringify(
isUnknownModel.InheritedDictionaryType
)}`
);
assert(
isEqual(
{
Kind: InputTypeKind.Dictionary,
Name: InputTypeKind.Dictionary,
IsNullable: false,
KeyType: {
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.String,
IsNullable: false,
},
ValueType: {
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.String,
IsNullable: false,
},
} as InputDictionaryType,
isStringModel.InheritedDictionaryType
),
`Inherited dictionary type is not correct, got ${JSON.stringify(
isStringModel.InheritedDictionaryType
)}`
);
assert(
isEqual(
{
Kind: InputTypeKind.Dictionary,
Name: InputTypeKind.Dictionary,
IsNullable: false,
KeyType: {
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.String,
IsNullable: false,
},
ValueType: {
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.Int32,
IsNullable: false,
},
} as InputDictionaryType,
isInt32Model.InheritedDictionaryType
),
`Inherited dictionary type is not correct, got ${JSON.stringify(
isInt32Model.InheritedDictionaryType
)}`
);
assert(
isEqual(
{
Kind: InputTypeKind.Dictionary,
Name: InputTypeKind.Dictionary,
IsNullable: false,
KeyType: {
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.String,
IsNullable: false,
},
ValueType: fooModel,
} as InputDictionaryType,
isFooModel.InheritedDictionaryType
),
`Inherited dictionary type is not correct, got ${JSON.stringify(
isFooModel.InheritedDictionaryType
)}`
);
assert(
isEqual(
{
Kind: InputTypeKind.Dictionary,
Name: InputTypeKind.Dictionary,
IsNullable: false,
KeyType: {
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.String,
IsNullable: false,
},
ValueType: {
Kind: InputTypeKind.Array,
Name: InputTypeKind.Array,
ElementType: fooModel,
IsNullable: false,
},
} as InputDictionaryType,
isFooArrayModel.InheritedDictionaryType
),
`Inherited dictionary type is not correct, got ${JSON.stringify(
isFooArrayModel.InheritedDictionaryType
)}`
);
});
});

Просмотреть файл

@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { FinalStateValue } from "@azure-tools/typespec-azure-core";
import assert from "assert";
import { describe } from "mocha";
import {
OperationFinalStateVia,
convertLroFinalStateVia,
} from "../../src/type/operationFinalStateVia.js";
describe("convertLroFinalStateVia()", () => {
describe("normal inputs", () => {
const mappings: [FinalStateValue, OperationFinalStateVia][] = [
[FinalStateValue.azureAsyncOperation, OperationFinalStateVia.AzureAsyncOperation],
[FinalStateValue.location, OperationFinalStateVia.Location],
[FinalStateValue.originalUri, OperationFinalStateVia.OriginalUri],
[FinalStateValue.operationLocation, OperationFinalStateVia.OperationLocation],
];
for (const [input, output] of mappings) {
it(`should return '${output}' for '${input}'`, function () {
assert.equal(convertLroFinalStateVia(input), output);
});
}
});
describe("unsupported inputs", () => {
const unsupportedInputs = [FinalStateValue.customLink];
for (const input of unsupportedInputs) {
it(`should throw exception for unsupported input '${input}'`, function () {
assert.throws(
() => convertLroFinalStateVia(input),
new RegExp(`Unsupported LRO final state value: ${input}`)
);
});
}
});
});

Просмотреть файл

@ -0,0 +1,257 @@
import { TestHost } from "@typespec/compiler/testing";
import assert, { deepStrictEqual } from "assert";
import isEqual from "lodash.isequal";
import { createModel } from "../../src/lib/clientModelBuilder.js";
import { InputPrimitiveTypeKind } from "../../src/type/inputPrimitiveTypeKind.js";
import { InputEnumType, InputListType } from "../../src/type/inputType.js";
import { InputTypeKind } from "../../src/type/inputTypeKind.js";
import {
createEmitterContext,
createEmitterTestHost,
createNetSdkContext,
typeSpecCompile,
} from "./utils/TestUtil.js";
describe("Test GetInputType for array", () => {
let runner: TestHost;
beforeEach(async () => {
runner = await createEmitterTestHost();
});
it("array as request", async () => {
const program = await typeSpecCompile(
`
op test(@body input: string[]): string[];
`,
runner
);
runner.compileAndDiagnose;
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const root = createModel(sdkContext);
deepStrictEqual(root.Clients[0].Operations[0].Parameters[0].Type.Kind, InputTypeKind.Array);
assert(
isEqual(
{
Kind: InputTypeKind.Array,
Name: InputTypeKind.Array,
ElementType: {
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.String,
IsNullable: false,
},
IsNullable: false,
} as InputListType,
root.Clients[0].Operations[0].Parameters[0].Type
)
);
});
it("array as response", async () => {
const program = await typeSpecCompile(
`
op test(): string[];
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const root = createModel(sdkContext);
deepStrictEqual(root.Clients[0].Operations[0].Responses[0].BodyType?.Kind, InputTypeKind.Array);
assert(
isEqual(
{
Kind: InputTypeKind.Array,
Name: InputTypeKind.Array,
ElementType: {
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.String,
IsNullable: false,
},
IsNullable: false,
} as InputListType,
root.Clients[0].Operations[0].Responses[0].BodyType
)
);
});
});
describe("Test GetInputType for enum", () => {
let runner: TestHost;
beforeEach(async () => {
runner = await createEmitterTestHost();
});
it("Fixed string enum", async () => {
const program = await typeSpecCompile(
`
#suppress "@azure-tools/typespec-azure-core/use-extensible-enum" "Enums should be defined without the @fixed decorator."
@doc("fixed string enum")
@fixed
enum SimpleEnum {
@doc("Enum value one")
One: "1",
@doc("Enum value two")
Two: "2",
@doc("Enum value four")
Four: "4"
}
#suppress "@azure-tools/typespec-azure-core/use-standard-operations" "Operation 'test' should be defined using a signature from the Azure.Core namespace."
@doc("test fixed enum.")
op test(@doc("fixed enum as input.")@body input: SimpleEnum): string[];
`,
runner,
{ IsNamespaceNeeded: true, IsAzureCoreNeeded: true }
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const root = createModel(sdkContext);
assert(
isEqual(
{
Kind: InputTypeKind.Enum,
Name: "SimpleEnum",
Namespace: "Azure.Csharp.Testing",
Accessibility: undefined,
Deprecated: undefined,
Description: "fixed string enum",
EnumValueType: "String",
AllowedValues: [
{
Name: "One",
Value: "1",
Description: "Enum value one",
},
{
Name: "Two",
Value: "2",
Description: "Enum value two",
},
{
Name: "Four",
Value: "4",
Description: "Enum value four",
},
],
IsExtensible: false,
IsNullable: false,
Usage: "Input",
} as InputEnumType,
root.Clients[0].Operations[0].Parameters[0].Type
)
);
const type = root.Clients[0].Operations[0].Parameters[0].Type as InputEnumType;
assert(type.EnumValueType !== undefined);
deepStrictEqual(type.Name, "SimpleEnum");
deepStrictEqual(type.IsExtensible, false);
});
it("Fixed int enum", async () => {
const program = await typeSpecCompile(
`
#suppress "@azure-tools/typespec-azure-core/use-extensible-enum" "Enums should be defined without the @fixed decorator."
@doc("Fixed int enum")
@fixed
enum FixedIntEnum {
@doc("Enum value one")
One: 1,
@doc("Enum value two")
Two: 2,
@doc("Enum value four")
Four: 4
}
#suppress "@azure-tools/typespec-azure-core/use-standard-operations" "Operation 'test' should be defined using a signature from the Azure.Core namespace."
@doc("test fixed enum.")
op test(@doc("fixed enum as input.")@body input: FixedIntEnum): string[];
`,
runner,
{ IsNamespaceNeeded: true, IsAzureCoreNeeded: true }
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const root = createModel(sdkContext);
assert(
isEqual(
{
Kind: InputTypeKind.Enum,
Name: "FixedIntEnum",
Namespace: "Azure.Csharp.Testing",
Accessibility: undefined,
Deprecated: undefined,
Description: "Fixed int enum",
EnumValueType: "Float32",
AllowedValues: [
{
Name: "One",
Value: 1,
Description: "Enum value one",
},
{
Name: "Two",
Value: 2,
Description: "Enum value two",
},
{
Name: "Four",
Value: 4,
Description: "Enum value four",
},
],
IsExtensible: false,
IsNullable: false,
Usage: "Input",
} as InputEnumType,
root.Clients[0].Operations[0].Parameters[0].Type
)
);
const type = root.Clients[0].Operations[0].Parameters[0].Type as InputEnumType;
assert(type.EnumValueType !== undefined);
deepStrictEqual(type.Name, "FixedIntEnum");
deepStrictEqual(type.IsExtensible, false);
});
it("fixed enum", async () => {
const program = await typeSpecCompile(
`
@doc("Fixed enum")
enum FixedEnum {
One: "1",
Two: "2",
Four: "4"
}
op test(@body input: FixedEnum): string[];
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const root = createModel(sdkContext);
assert(
isEqual(
{
Kind: InputTypeKind.Enum,
Name: "FixedEnum",
Namespace: "Azure.Csharp.Testing",
Accessibility: undefined,
Deprecated: undefined,
Description: "Fixed enum",
EnumValueType: "String",
AllowedValues: [
{ Name: "One", Value: "1", Description: undefined },
{ Name: "Two", Value: "2", Description: undefined },
{ Name: "Four", Value: "4", Description: undefined },
],
IsExtensible: false,
IsNullable: false,
Usage: "Input",
} as InputEnumType,
root.Clients[0].Operations[0].Parameters[0].Type
)
);
const type = root.Clients[0].Operations[0].Parameters[0].Type as InputEnumType;
assert(type.EnumValueType !== undefined);
deepStrictEqual(type.Name, "FixedEnum");
deepStrictEqual((type as InputEnumType).IsExtensible, false);
});
});

Просмотреть файл

@ -0,0 +1,49 @@
import { TestHost } from "@typespec/compiler/testing";
import assert, { deepStrictEqual } from "assert";
import isEqual from "lodash.isequal";
import { createModel } from "../../src/lib/clientModelBuilder.js";
import { InputPrimitiveTypeKind } from "../../src/type/inputPrimitiveTypeKind.js";
import { InputTypeKind } from "../../src/type/inputTypeKind.js";
import {
createEmitterContext,
createEmitterTestHost,
createNetSdkContext,
typeSpecCompile,
} from "./utils/TestUtil.js";
describe("Test GetInputType for scalar", () => {
let runner: TestHost;
beforeEach(async () => {
runner = await createEmitterTestHost();
});
it("azureLocation scalar", async () => {
const program = await typeSpecCompile(
`
op test(@query location: azureLocation): void;
`,
runner,
{ IsNamespaceNeeded: true, IsAzureCoreNeeded: true }
);
runner.compileAndDiagnose;
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const root = createModel(sdkContext);
deepStrictEqual(root.Clients[0].Operations[0].Parameters[0].Type.Kind, InputTypeKind.Primitive);
deepStrictEqual(
root.Clients[0].Operations[0].Parameters[0].Type.Name,
InputPrimitiveTypeKind.AzureLocation
);
assert(
isEqual(
{
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.AzureLocation,
IsNullable: false,
},
root.Clients[0].Operations[0].Parameters[0].Type
)
);
});
});

Просмотреть файл

@ -0,0 +1,217 @@
import { TestHost } from "@typespec/compiler/testing";
import { getAllHttpServices } from "@typespec/http";
import assert from "assert";
import isEqual from "lodash.isequal";
import { loadOperation } from "../../src/lib/operation.js";
import { InputPrimitiveTypeKind } from "../../src/type/inputPrimitiveTypeKind.js";
import { InputEnumType, InputModelType, InputPrimitiveType } from "../../src/type/inputType.js";
import { InputTypeKind } from "../../src/type/inputTypeKind.js";
import {
createEmitterContext,
createEmitterTestHost,
createNetSdkContext,
navigateModels,
typeSpecCompile,
} from "./utils/TestUtil.js";
describe("Test string format", () => {
let runner: TestHost;
beforeEach(async () => {
runner = await createEmitterTestHost();
});
it("scalar url as parameter", async () => {
const program = await typeSpecCompile(
`
op test(@path sourceUrl: url): void;
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const modelMap = new Map<string, InputModelType>();
const enumMap = new Map<string, InputEnumType>();
const operation = loadOperation(
sdkContext,
services[0].operations[0],
"",
[],
services[0].namespace,
modelMap,
enumMap
);
assert(
isEqual(
{
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.Uri,
IsNullable: false,
} as InputPrimitiveType,
operation.Parameters[0].Type
)
);
});
it("scalar url as model property", async () => {
const program = await typeSpecCompile(
`
@doc("This is a model.")
model Foo {
@doc("The source url.")
source: url;
}
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const modelMap = new Map<string, InputModelType>();
const enumMap = new Map<string, InputEnumType>();
navigateModels(sdkContext, services[0].namespace, modelMap, enumMap);
const foo = modelMap.get("Foo");
assert(foo !== undefined);
assert(
isEqual(
{
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.Uri,
IsNullable: false,
} as InputPrimitiveType,
foo.Properties[0].Type
)
);
});
it("format uri on operation parameter", async () => {
const program = await typeSpecCompile(
`
op test(@path @format("Uri")sourceUrl: string): void;
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const modelMap = new Map<string, InputModelType>();
const enumMap = new Map<string, InputEnumType>();
const operation = loadOperation(
sdkContext,
services[0].operations[0],
"",
[],
services[0].namespace,
modelMap,
enumMap
);
assert(
isEqual(
{
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.Uri,
IsNullable: false,
},
operation.Parameters[0].Type
)
);
});
it("format uri on model property", async () => {
const program = await typeSpecCompile(
`
@doc("This is a model.")
model Foo {
@doc("The source url.")
@format("Uri")
source: string
}
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const modelMap = new Map<string, InputModelType>();
const enumMap = new Map<string, InputEnumType>();
navigateModels(sdkContext, services[0].namespace, modelMap, enumMap);
const foo = modelMap.get("Foo");
assert(foo !== undefined);
assert(
isEqual(
{
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.Uri,
IsNullable: false,
},
foo.Properties[0].Type
)
);
});
it("format uuid on operation parameter", async () => {
const program = await typeSpecCompile(
`
op test(@path @format("uuid")subscriptionId: string): void;
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const modelMap = new Map<string, InputModelType>();
const enumMap = new Map<string, InputEnumType>();
const operation = loadOperation(
sdkContext,
services[0].operations[0],
"",
[],
services[0].namespace,
modelMap,
enumMap
);
assert(
isEqual(
{
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.Guid,
IsNullable: false,
},
operation.Parameters[0].Type
)
);
});
it("format on model property", async () => {
const program = await typeSpecCompile(
`
@doc("This is a model.")
model Foo {
@doc("The subscription id.")
@format("uuid")
subscriptionId: string
}
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const modelMap = new Map<string, InputModelType>();
const enumMap = new Map<string, InputEnumType>();
navigateModels(sdkContext, services[0].namespace, modelMap, enumMap);
const foo = modelMap.get("Foo");
assert(foo !== undefined);
assert(
isEqual(
{
Kind: InputTypeKind.Primitive,
Name: InputPrimitiveTypeKind.Guid,
IsNullable: false,
} as InputPrimitiveType,
foo.Properties[0].Type
)
);
});
});

Просмотреть файл

@ -0,0 +1,629 @@
import { TestHost } from "@typespec/compiler/testing";
import { getAllHttpServices } from "@typespec/http";
import assert from "assert";
import { getUsages } from "../../src/lib/model.js";
import {
createEmitterContext,
createEmitterTestHost,
createNetSdkContext,
typeSpecCompile,
} from "./utils/TestUtil.js";
describe("Test getUsages", () => {
let runner: TestHost;
beforeEach(async () => {
runner = await createEmitterTestHost();
});
it("Get usage for body parameter type", async () => {
const program = await typeSpecCompile(
`
@doc("This is a model.")
model Foo {
@doc("name of the Foo")
name: string;
}
op test(@path id: string, @body foo: Foo): void;
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const usages = getUsages(sdkContext, services[0].operations);
assert(usages.inputs.includes("Foo"));
});
it("Get usage for response body", async () => {
const program = await typeSpecCompile(
`
@doc("This is a model.")
model Foo {
@doc("name of the Foo")
name: string;
}
op test(@path id: string): Foo;
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const usages = getUsages(sdkContext, services[0].operations);
assert(usages.outputs.includes("Foo"));
});
it("Get usage for the model in both input and output", async () => {
const program = await typeSpecCompile(
`
@doc("This is a model.")
model Foo {
@doc("name of the Foo")
name: string;
}
op test(@path id: string, @body foo: Foo): Foo;
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const usages = getUsages(sdkContext, services[0].operations);
assert(usages.roundTrips.includes("Foo"));
});
it("Get usage for the model which is used in two operations", async () => {
const program = await typeSpecCompile(
`
@doc("This is a model.")
model Foo {
@doc("name of the Foo")
name: string;
}
op test(@path id: string, @body foo: Foo): void;
op test2(@path id: string): Foo;
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const usages = getUsages(sdkContext, services[0].operations);
assert(usages.roundTrips.includes("Foo"));
});
it("Get usage for the model as the template argument", async () => {
const program = await typeSpecCompile(
`
@doc("This is a model template.")
model TemplateModel<T> {
@doc("name of the model.")
name: string;
prop: T;
}
@doc("This is a model.")
model Foo {
@doc("name of the Foo")
name: string;
}
op test(@path id: string, @body foo: TemplateModel<Foo>): void;
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const usages = getUsages(sdkContext, services[0].operations);
assert(usages.inputs.includes("TemplateModelFoo"));
assert(usages.inputs.includes("Foo"));
});
it("Test the usage inheritance between base model and derived model", async () => {
const program = await typeSpecCompile(
`
@doc("This is a base model.")
model BaseModel {
@doc("name of the model.")
base: string;
}
@doc("This is a model.")
model Foo extends BaseModel{
@doc("name of the Foo")
name: string;
}
op test(@path id: string, @body foo: Foo): void;
op test2(@path id: string): BaseModel;
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const usages = getUsages(sdkContext, services[0].operations);
// verify that the baseModel will not apply the usage of derived model.
assert(usages.outputs.includes("BaseModel"));
// verify that the derived model will inherit the usage of base model
assert(usages.roundTrips.includes("Foo"));
});
it("Test the usage inheritance between base model and derived model which has model property", async () => {
const program = await typeSpecCompile(
`
@doc("This a model of a property in base model")
model propertyModel {
@doc("name of the model.")
base: string;
}
@doc("This is a base model.")
model BaseModel {
@doc("name of the model.")
base: string;
@doc("a property")
prop: propertyModel;
}
@doc("This is a model.")
model Foo extends BaseModel{
@doc("name of the Foo")
name: string;
}
op test(@path id: string, @body foo: Foo): void;
op test2(@path id: string): BaseModel;
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const usages = getUsages(sdkContext, services[0].operations);
// verify that the baseModel will not apply the usage of derived model.
assert(usages.outputs.includes("BaseModel"));
// verify that the derived model will inherit the usage of base model
assert(usages.roundTrips.includes("Foo"));
//verify that the property model of base model will inherit the usage of the derived model
assert(usages.roundTrips.includes("propertyModel"));
});
it("Test the usage of models spread alias", async () => {
const program = await typeSpecCompile(
`
alias FooAlias = {
@path id: string;
@doc("name of the Foo")
name: string;
};
op test(...FooAlias): void;
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const usages = getUsages(sdkContext, services[0].operations);
assert(usages.inputs.includes("TestRequest"));
});
it("Test the usage of body parameter of azure core operation.", async () => {
const program = await typeSpecCompile(
`
@doc("This is a model.")
@resource("items")
model Foo {
@doc("id of Foo")
@key
@visibility("read","create","query")
id: string;
@doc("name of Foo")
name: string;
}
@doc("The item information.")
model FooInfo {
@doc("name of Foo")
name: string;
}
@doc("this is a response model.")
model BatchCreateFooListItemsRequest {
@doc("The items to create")
fooInfos: FooInfo[];
}
@doc("this is a response model.")
model BatchCreateTextListItemsResponse {
@doc("The item list.")
fooList: Foo[];
}
interface TextLists{
@doc("create items")
addItems is ResourceAction<Foo, BatchCreateFooListItemsRequest, BatchCreateTextListItemsResponse>;
}
`,
runner,
{ IsNamespaceNeeded: true, IsAzureCoreNeeded: true }
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const usages = getUsages(sdkContext, services[0].operations);
assert(usages.inputs.includes("BatchCreateFooListItemsRequest"));
assert(usages.inputs.includes("FooInfo"));
assert(usages.outputs.includes("BatchCreateTextListItemsResponse"));
});
it("Test the usage of body parameter and return type of azure core resource operation.", async () => {
const program = await typeSpecCompile(
`
@doc("This is a model.")
@resource("items")
model Foo {
@doc("id of Foo")
@key
@visibility("read","create","query")
id: string;
@doc("name of Foo")
name: string;
}
interface FooClient{
@doc("create Foo")
createFoo is ResourceCreateOrUpdate<Foo>;
}
`,
runner,
{ IsNamespaceNeeded: true, IsAzureCoreNeeded: true }
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const usages = getUsages(sdkContext, services[0].operations);
assert(usages.roundTrips.includes("Foo"));
});
it("Test the usage of body polymorphism type in azure core resource operation.", async () => {
const program = await typeSpecCompile(
`
@doc("This is a model.")
@resource("items")
model Foo {
@doc("id of Foo")
@key
@visibility("read","create","query")
id: string;
@doc("name of Foo")
name: string;
}
#suppress "@azure-tools/typespec-azure-core/documentation-required" "The ModelProperty named 'discriminatorProperty' should have a documentation or description, please use decorator @doc to add it"
@discriminator("discriminatorProperty")
@doc("Base model with discriminator property.")
model BaseModelWithDiscriminator {
@doc("Optional property on base")
optionalPropertyOnBase?: string;
@doc("Required property on base")
requiredPropertyOnBase: int32;
}
#suppress "@azure-tools/typespec-azure-core/documentation-required" "The ModelProperty named 'discriminatorProperty' should have a documentation or description, please use decorator @doc to add it"
@doc("Deriver model with discriminator property.")
model DerivedModelWithDiscriminatorA extends BaseModelWithDiscriminator {
discriminatorProperty: "A";
@doc("Required string.")
requiredString: string;
}
interface FooClient{
@doc("create Foo")
op testFoo is Azure.Core.StandardResourceOperations.ResourceCollectionAction<
Foo,
BaseModelWithDiscriminator,
{}
>;
}
`,
runner,
{ IsNamespaceNeeded: true, IsAzureCoreNeeded: true }
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const usages = getUsages(sdkContext, services[0].operations);
assert(usages.inputs.includes("BaseModelWithDiscriminator"));
assert(usages.inputs.includes("DerivedModelWithDiscriminatorA"));
});
it("Test the usage of response polymorphism type in azure core resource operation.", async () => {
const program = await typeSpecCompile(
`
@doc("This is a model.")
@resource("items")
model Foo {
@doc("id of Foo")
@key
@visibility("read","create","query")
id: string;
@doc("name of Foo")
name: string;
}
@doc("This is nested model.")
model NestedModel {
@doc("id of NestedModel")
id: string;
}
#suppress "@azure-tools/typespec-azure-core/documentation-required" "The ModelProperty named 'discriminatorProperty' should have a documentation or description, please use decorator @doc to add it"
@discriminator("discriminatorProperty")
@doc("Base model with discriminator property.")
model BaseModelWithDiscriminator {
@doc("Optional property on base")
optionalPropertyOnBase?: string;
@doc("Required property on base")
requiredPropertyOnBase: int32;
}
#suppress "@azure-tools/typespec-azure-core/documentation-required" "The ModelProperty named 'discriminatorProperty' should have a documentation or description, please use decorator @doc to add it"
@doc("Deriver model with discriminator property.")
model DerivedModelWithDiscriminatorA extends BaseModelWithDiscriminator {
discriminatorProperty: "A";
@doc("Required string.")
requiredString: string;
@doc("property with complex model type.")
nestedModel: NestedModel;
}
interface FooClient{
@doc("create Foo")
op testFoo is Azure.Core.StandardResourceOperations.ResourceCollectionAction<
Foo,
{},
BaseModelWithDiscriminator
>;
}
`,
runner,
{ IsNamespaceNeeded: true, IsAzureCoreNeeded: true }
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const usages = getUsages(sdkContext, services[0].operations);
assert(usages.outputs.includes("BaseModelWithDiscriminator"));
assert(usages.outputs.includes("DerivedModelWithDiscriminatorA"));
assert(usages.outputs.includes("NestedModel"));
});
it("Get usage for the model which is renamed by projected name", async () => {
const program = await typeSpecCompile(
`
@doc("This is a model.")
@projectedName("azure", "FooRenamed")
model Foo {
@doc("name of the Foo")
name: string;
}
op test(@path id: string, @body foo: Foo): Foo;
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const usages = getUsages(sdkContext, services[0].operations);
assert(usages.roundTrips.includes("FooRenamed"));
});
it("Test the usage of enum which is renamed via @projectedName.", async () => {
const program = await typeSpecCompile(
`
@doc("fixed string enum")
@projectedName("azure", "SimpleEnumRenamed")
enum SimpleEnum {
@doc("Enum value one")
One: "1",
@doc("Enum value two")
Two: "2",
@doc("Enum value four")
Four: "4"
}
op test(@path id: SimpleEnum): void;
`,
runner,
{ IsNamespaceNeeded: true, IsAzureCoreNeeded: false }
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const usages = getUsages(sdkContext, services[0].operations);
assert(usages.inputs.includes("SimpleEnumRenamed"));
});
it("Test the usage of model which is renamed via @clientName.", async () => {
const program = await typeSpecCompile(
`
@doc("A model plan to rename")
@clientName("RenamedModel")
model ModelToRename {
value: string;
}
op test(@body body: ModelToRename): void;
`,
runner,
{ IsNamespaceNeeded: true, IsTCGCNeeded: true }
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const usages = getUsages(sdkContext, services[0].operations);
assert(usages.inputs.includes("RenamedModel"));
});
it("Test the usage of return type of a customized LRO operation.", async () => {
const program = await typeSpecCompile(
`
#suppress "@azure-tools/typespec-azure-core/documentation-required" "MUST fix in next version"
@doc("The status of the processing job.")
@lroStatus
enum JobStatus {
NotStarted: "notStarted",
Running: "running",
Succeeded: "succeeded",
Failed: "failed",
Canceled: "canceled",
}
@doc("Provides status details for long running operations.")
model HealthInsightsOperationStatus<
TStatusResult = never,
TStatusError = Foundations.Error
> {
@key("operationId")
@doc("The unique ID of the operation.")
@visibility("read")
id: Azure.Core.uuid;
@doc("The status of the operation")
@visibility("read")
@lroStatus
status: JobStatus;
@doc("The date and time when the processing job was created.")
@visibility("read")
createdDateTime?: utcDateTime;
@doc("The date and time when the processing job is set to expire.")
@visibility("read")
expirationDateTime?: utcDateTime;
@doc("The date and time when the processing job was last updated.")
@visibility("read")
lastUpdateDateTime?: utcDateTime;
@doc("Error object that describes the error when status is Failed.")
error?: TStatusError;
@doc("The result of the operation.")
@lroResult
result?: TStatusResult;
}
@doc("The location of an instance of {name}", TResource)
scalar HealthInsightsResourceLocation<TResource extends {}> extends url;
@doc("Metadata for long running operation status monitor locations")
model HealthInsightsLongRunningStatusLocation<TStatusResult = never> {
@pollingLocation
@doc("The location for monitoring the operation state.")
@TypeSpec.Http.header("Operation-Location")
operationLocation: HealthInsightsResourceLocation<HealthInsightsOperationStatus<TStatusResult>>;
}
#suppress "@azure-tools/typespec-azure-core/long-running-polling-operation-required" "This is a template"
@doc("Long running RPC operation template")
op HealthInsightsLongRunningRpcOperation<
TParams extends TypeSpec.Reflection.Model,
TResponse extends TypeSpec.Reflection.Model,
Traits extends Record<unknown> = {}
> is Azure.Core.RpcOperation<
TParams & RepeatabilityRequestHeaders,
Foundations.AcceptedResponse<HealthInsightsLongRunningStatusLocation<TResponse> &
Foundations.RetryAfterHeader> &
RepeatabilityResponseHeaders &
HealthInsightsOperationStatus,
Traits
>;
@trait("HealthInsightsRetryAfterTrait")
@doc("Health Insights retry after trait")
model HealthInsightsRetryAfterTrait {
#suppress "@azure-tools/typespec-providerhub/no-inline-model" "This inline model is never used directly in operations."
@doc("The retry-after header.")
retryAfter: {
@traitLocation(TraitLocation.Response)
response: Foundations.RetryAfterHeader;
};
}
@doc("The inference results for the Radiology Insights request.")
model RadiologyInsightsInferenceResult {
id: string;
}
alias Request = {
@doc("The list of patients, including their clinical information and data.")
patients: string[];
};
@resource("radiology-insights/jobs")
@doc("The response for the Radiology Insights request.")
model RadiologyInsightsResult
is HealthInsightsOperationStatus<RadiologyInsightsInferenceResult>;
@doc("The body of the Radiology Insights request.")
model RadiologyInsightsData {
...Request;
@doc("Configuration affecting the Radiology Insights model's inference.")
configuration?: string;
}
#suppress "@azure-tools/typespec-azure-core/long-running-polling-operation-required" "This is a template"
@doc("Long running Pool operation template")
op HealthInsightsLongRunningPollOperation<TResult extends TypeSpec.Reflection.Model> is Azure.Core.RpcOperation<
{
@doc("A processing job identifier.")
@path("id")
id: Azure.Core.uuid;
},
TResult,
HealthInsightsRetryAfterTrait
>;
interface LegacyLro {
#suppress "@azure-tools/typespec-azure-core/no-rpc-path-params" "Service uses a jobId in the path"
@summary("Get Radiology Insights job details")
@tag("RadiologyInsights")
@doc("Gets the status and details of the Radiology Insights job.")
@get
@route("/radiology-insights/jobs/{id}")
@convenientAPI(false)
getJob is HealthInsightsLongRunningPollOperation<RadiologyInsightsResult>;
#suppress "@azure-tools/typespec-azure-core/long-running-polling-operation-required" "Polling through operation-location"
#suppress "@azure-tools/typespec-azure-core/use-standard-operations" "There is no long-running RPC template in Azure.Core"
@summary("Create Radiology Insights job")
@tag("RadiologyInsights")
@doc("Creates a Radiology Insights job with the given request body.")
@pollingOperation(LegacyLro.getJob)
@route("/radiology-insights/jobs")
@convenientAPI(true)
createJob is HealthInsightsLongRunningRpcOperation<
RadiologyInsightsData,
RadiologyInsightsResult
>;
}
`,
runner,
{
IsNamespaceNeeded: true,
IsAzureCoreNeeded: true,
IsTCGCNeeded: true,
}
);
const context = createEmitterContext(program);
const sdkContext = createNetSdkContext(context);
const [services] = getAllHttpServices(program);
const convenienceOperations = services[0].operations.slice(1);
const usages = getUsages(sdkContext, convenienceOperations);
assert(usages.outputs.includes("RadiologyInsightsInferenceResult"));
});
});

Просмотреть файл

@ -0,0 +1,130 @@
import { AzureCoreTestLibrary } from "@azure-tools/typespec-azure-core/testing";
import { createSdkContext, SdkContext } from "@azure-tools/typespec-client-generator-core";
import { SdkTestLibrary } from "@azure-tools/typespec-client-generator-core/testing";
import {
CompilerOptions,
EmitContext,
isGlobalNamespace,
Namespace,
navigateTypesInNamespace,
Program,
Type,
} from "@typespec/compiler";
import { createTestHost, TestHost } from "@typespec/compiler/testing";
import { HttpTestLibrary } from "@typespec/http/testing";
import { RestTestLibrary } from "@typespec/rest/testing";
import { VersioningTestLibrary } from "@typespec/versioning/testing";
import { getFormattedType, getInputType } from "../../../src/lib/model.js";
import { NetEmitterOptions } from "../../../src/options.js";
import { InputEnumType, InputModelType } from "../../../src/type/inputType.js";
export async function createEmitterTestHost(): Promise<TestHost> {
return createTestHost({
libraries: [
RestTestLibrary,
HttpTestLibrary,
VersioningTestLibrary,
AzureCoreTestLibrary,
SdkTestLibrary,
],
});
}
export interface TypeSpecCompileOptions {
IsNamespaceNeeded?: boolean;
IsAzureCoreNeeded?: boolean;
IsTCGCNeeded?: boolean;
}
export async function typeSpecCompile(
content: string,
host: TestHost,
options?: TypeSpecCompileOptions
) {
const needNamespaces = options?.IsNamespaceNeeded ?? true;
const needAzureCore = options?.IsAzureCoreNeeded ?? false;
const needTCGC = options?.IsTCGCNeeded ?? false;
const namespace = `
@versioned(Versions)
@useAuth(ApiKeyAuth<ApiKeyLocation.header, "api-key">)
@service({
title: "Azure Csharp emitter Testing",
})
namespace Azure.Csharp.Testing;
enum Versions {
${needAzureCore ? "@useDependency(Azure.Core.Versions.v1_0_Preview_1)" : ""}
"2023-01-01-preview"
}
`;
const fileContent = `
import "@typespec/rest";
import "@typespec/http";
import "@typespec/versioning";
${needAzureCore ? 'import "@azure-tools/typespec-azure-core";' : ""}
${needTCGC ? 'import "@azure-tools/typespec-client-generator-core";' : ""}
using TypeSpec.Rest;
using TypeSpec.Http;
using TypeSpec.Versioning;
${needAzureCore ? "using Azure.Core;\nusing Azure.Core.Traits;" : ""}
${needTCGC ? "using Azure.ClientGenerator.Core;" : ""}
${needNamespaces ? namespace : ""}
${content}
`;
host.addTypeSpecFile("main.tsp", fileContent);
const cliOptions = {
warningAsError: false,
} as CompilerOptions;
await host.compile("./", cliOptions);
return host.program;
}
export function createEmitterContext(program: Program): EmitContext<NetEmitterOptions> {
return {
program: program,
emitterOutputDir: "./",
options: {
outputFile: "tspCodeModel.json",
logFile: "log.json",
skipSDKGeneration: false,
"new-project": false,
"clear-output-folder": false,
"save-inputs": false,
"generate-protocol-methods": true,
"generate-convenience-methods": true,
"package-name": undefined,
} as NetEmitterOptions,
} as EmitContext<NetEmitterOptions>;
}
/* Navigate all the models in the whole namespace. */
export function navigateModels(
context: SdkContext<NetEmitterOptions>,
namespace: Namespace,
models: Map<string, InputModelType>,
enums: Map<string, InputEnumType>
) {
const computeModel = (x: Type) =>
getInputType(context, getFormattedType(context.program, x), models, enums) as any;
const skipSubNamespaces = isGlobalNamespace(context.program, namespace);
navigateTypesInNamespace(
namespace,
{
model: (x) => x.name !== "" && x.kind === "Model" && computeModel(x),
scalar: computeModel,
enum: computeModel,
union: (x) => x.name !== undefined && computeModel(x),
},
{ skipSubNamespaces }
);
}
/* We always need to pass in the emitter name now that it is required so making a helper to do this. */
export function createNetSdkContext(
program: EmitContext<NetEmitterOptions>
): SdkContext<NetEmitterOptions> {
return createSdkContext(program, "@azure-tools/typespec-azure");
}

Просмотреть файл

@ -0,0 +1,30 @@
{
"compilerOptions": {
"composite": true,
"alwaysStrict": true,
"forceConsistentCasingInFileNames": true,
"preserveConstEnums": true,
"module": "node16",
"moduleResolution": "node16",
"esModuleInterop": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"sourceMap": true,
"declarationMap": true,
"strict": true,
"declaration": true,
"stripInternal": true,
"noEmitHelpers": false,
"target": "es2021",
"lib": ["es2021"],
"experimentalDecorators": true,
"newLine": "LF",
"outDir": "dist",
"rootDir": ".",
"tsBuildInfoFile": "temp/tsconfig.tsbuildinfo",
"types": ["node", "mocha"],
"skipLibCheck": true
},
"include": ["src/**/*.ts", "test/**/*.ts"]
}

Просмотреть файл

@ -1,3 +1,4 @@
packages:
- packages/*
- e2e
- "packages/*"
- "e2e"
- "!packages/http-client-csharp/**"