From fa3cb4f01bdd8371b121279a4663586a12b2645b Mon Sep 17 00:00:00 2001 From: kai ru Date: Tue, 13 Aug 2024 13:29:29 +0800 Subject: [PATCH 1/2] Support host path and paramters in typespec-emiter --- src/typespec-aaz/src/convertor.ts | 77 +++++++++++++++++++++++++------ src/typespec-aaz/src/lib.ts | 6 +++ 2 files changed, 68 insertions(+), 15 deletions(-) diff --git a/src/typespec-aaz/src/convertor.ts b/src/typespec-aaz/src/convertor.ts index 7a36a998..8fcd842d 100644 --- a/src/typespec-aaz/src/convertor.ts +++ b/src/typespec-aaz/src/convertor.ts @@ -1,4 +1,4 @@ -import { HttpOperation, HttpOperationBody, HttpOperationMultipartBody, HttpOperationResponse, HttpStatusCodeRange, HttpStatusCodesEntry, Visibility, createMetadataInfo, getHeaderFieldOptions, getQueryParamOptions, getStatusCodeDescription, getVisibilitySuffix, isContentTypeHeader, isVisible, resolveRequestVisibility } from "@typespec/http"; +import { HttpOperation, HttpOperationBody, HttpOperationMultipartBody, HttpOperationResponse, HttpStatusCodeRange, HttpStatusCodesEntry, Visibility, createMetadataInfo, getHeaderFieldOptions, getQueryParamOptions, getServers, getStatusCodeDescription, getVisibilitySuffix, isContentTypeHeader, resolveRequestVisibility } from "@typespec/http"; import { AAZEmitterContext, AAZOperationEmitterContext, AAZSchemaEmitterContext } from "./context.js"; import { resolveOperationId } from "./utils.js"; import { TypeSpecPathItem } from "./model/path_item.js"; @@ -17,7 +17,6 @@ import { import { getMaxProperties, getMinProperties, getMultipleOf, getUniqueItems } from "@typespec/json-schema"; import { shouldFlattenProperty } from "@azure-tools/typespec-client-generator-core"; import { CMDArrayFormat, CMDFloatFormat, CMDIntegerFormat, CMDObjectFormat, CMDResourceIdFormat, CMDStringFormat } from "./model/format.js"; -import { match } from "assert"; interface DiscriminatorInfo { @@ -96,13 +95,14 @@ export function retrieveAAZOperation(context: AAZEmitterContext, operation: Http function convert2CMDOperation(context: AAZOperationEmitterContext, operation: HttpOperation): CMDHttpOperation { // TODO: resolve host parameters for the operation - + const hostPathAndParameters = extractHostPathAndParameters(context); + const hostPath = hostPathAndParameters?.hostPath ?? ""; const op: CMDHttpOperation = { operationId: context.operationId, description: getDoc(context.program, operation.operation), http: { - // TODO: add host path if exists - path: getPathWithoutQuery(operation.path), + // merge host path and operation path + path: hostPath + getPathWithoutQuery(operation.path), } }; @@ -117,7 +117,7 @@ function convert2CMDOperation(context: AAZOperationEmitterContext, operation: Ht // TODO: add support for custom polling information } - op.http.request = extractHttpRequest(context, operation); + op.http.request = extractHttpRequest(context, operation, hostPathAndParameters?.hostParameters ?? {}); op.http.responses = extractHttpResponses({ ...context, visibility: Visibility.Read, @@ -125,13 +125,55 @@ function convert2CMDOperation(context: AAZOperationEmitterContext, operation: Ht return op; } -// function extractHostParameters(context: AAZEmitterContext) { -// // TODO: resolve host parameters -// // return context.sdkContext.host; -// } +function extractHostPathAndParameters(context: AAZOperationEmitterContext): { hostPath: string, hostParameters: Record } | undefined { + const servers = getServers(context.program, context.service.type); + if (servers === undefined || servers.length > 1) { + return undefined; + } + const server = servers[0]; + const url = new URL(server.url); + const hostPath = url.pathname; + if (hostPath === "/" || hostPath === "") { + return undefined; + } + // using r"\{([^{}]*)}" to iterate over all parameter names in hostPath, the name should not contain '{' or '}' + const hostParameters: Record = {}; + const hostPathParameters = hostPath.matchAll(/\{([^{}]*)\}/g); + for (const match of hostPathParameters) { + const name = match[1]; + const param = server.parameters?.get(name); + let schema; + if (param === undefined) { + reportDiagnostic(context.program, { + code: "missing-host-parameter", + target: context.service.type, + message: `Host parameter '${name}' is not defined in the server parameters.`, + }); + } else { + schema = convert2CMDSchema({ + ...context, + visibility: Visibility.Read, + supportClsSchema: false, + }, param, name); + } + if (schema === undefined) { + schema ={ + name, + type: "string", + }; + } + schema.required = true; + schema.skipUrlEncoding = true; + hostParameters[name] = schema; + } + + return { + hostPath, + hostParameters, + } +} -function extractHttpRequest(context: AAZOperationEmitterContext, operation: HttpOperation): CMDHttpRequest | undefined { - // TODO: Add host parameters to the request +function extractHttpRequest(context: AAZOperationEmitterContext, operation: HttpOperation, hostParameters: Record): CMDHttpRequest | undefined { const request: CMDHttpRequest = { method: operation.verb, }; @@ -139,6 +181,13 @@ function extractHttpRequest(context: AAZOperationEmitterContext, operation: Http let schemaContext; const methodParams = operation.parameters; const paramModels: Record> = {}; + // add host parameters from the host path to path parameters + if (hostParameters && Object.keys(hostParameters).length > 0) { + paramModels["path"] = { + ...hostParameters, + }; + } + let clientRequestIdName; for (const httpOpParam of methodParams.parameters) { if (httpOpParam.type === "header" && isContentTypeHeader(context.program, httpOpParam.param)) { @@ -157,8 +206,6 @@ function extractHttpRequest(context: AAZOperationEmitterContext, operation: Http continue; } - - schema.required = !httpOpParam.param.optional; if (paramModels[httpOpParam.type] === undefined) { @@ -1765,7 +1812,7 @@ function getClsDefinitionModel(schema: CMDClsSchemaBase): CMDObjectSchemaBase | return schema.type.pendingSchema.schema! } -function getDefaultValue(content: AAZSchemaEmitterContext, defaultType: Value): any { +function getDefaultValue(content: AAZSchemaEmitterContext, defaultType: Value): unknown { switch (defaultType.valueKind) { case "StringValue": return defaultType.value; diff --git a/src/typespec-aaz/src/lib.ts b/src/typespec-aaz/src/lib.ts index 15ca9741..beddf9ae 100644 --- a/src/typespec-aaz/src/lib.ts +++ b/src/typespec-aaz/src/lib.ts @@ -92,6 +92,12 @@ const libDef = { default: "Invalid default value", } }, + "missing-host-parameter": { + severity: "error", + messages: { + default: "Missing host parameter", + } + } }, emitter: { options: EmitterOptionsSchema as JSONSchemaType, From 27f202b2e254c0c063b5da2dce3ac39582d6c26b Mon Sep 17 00:00:00 2001 From: kai ru Date: Tue, 13 Aug 2024 16:51:53 +0800 Subject: [PATCH 2/2] Fix error response type classify --- src/typespec-aaz/src/convertor.ts | 48 ++++++++++++++++++++----------- src/typespec-aaz/src/emit.ts | 2 +- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/src/typespec-aaz/src/convertor.ts b/src/typespec-aaz/src/convertor.ts index 8fcd842d..4e103094 100644 --- a/src/typespec-aaz/src/convertor.ts +++ b/src/typespec-aaz/src/convertor.ts @@ -131,8 +131,8 @@ function extractHostPathAndParameters(context: AAZOperationEmitterContext): { ho return undefined; } const server = servers[0]; - const url = new URL(server.url); - const hostPath = url.pathname; + // using r"^(.*://)?[^/]*(/.*)$" to split server url into host and path if url matches the pattern else the path should be empty + const hostPath = server.url.match(/^(.*:\/\/)?[^/]*(\/.*)$/)?.[2] ?? ""; if (hostPath === "/" || hostPath === "") { return undefined; } @@ -163,7 +163,6 @@ function extractHostPathAndParameters(context: AAZOperationEmitterContext): { ho }; } schema.required = true; - schema.skipUrlEncoding = true; hostParameters[name] = schema; } @@ -434,26 +433,29 @@ function convert2CMDHttpResponse(context: AAZOperationEmitterContext, response: if (body.bodyKind === "multipart") { throw new Error("NotImplementedError: Multipart form data responses are not supported."); } - let schema = convert2CMDSchemaBase( - { - ...buildSchemaEmitterContext(context, body.type, "body"), - visibility: Visibility.Read, - }, - body.type - ); - if (!schema) { - throw new Error("Invalid Response Schema. It's None."); - } + let schema; if (isError) { - const errorFormat = classifyErrorFormat(context, schema); + const errorFormat = classifyErrorFormat(context, body.type); if (errorFormat === undefined) { throw new Error("Error response schema is not supported yet."); } schema = { - readOnly: schema.readOnly, + readOnly: true, type: `@${errorFormat}`, } + } else { + schema = convert2CMDSchemaBase( + { + ...buildSchemaEmitterContext(context, body.type, "body"), + visibility: Visibility.Read, + }, + body.type + ); } + if (!schema) { + throw new Error("Invalid Response Schema. It's None."); + } + res.body = { json: { schema: schema, @@ -1763,7 +1765,20 @@ function getResponseDescriptionForStatusCodes(statusCodes: string[] | undefined) return getStatusCodeDescription(statusCodes[0]) ?? undefined; } -function classifyErrorFormat(context: AAZOperationEmitterContext, schema: CMDSchemaBase): "ODataV4Format" | "MgmtErrorFormat" | undefined { +function classifyErrorFormat(context: AAZOperationEmitterContext, type: Type): "ODataV4Format" | "MgmtErrorFormat" | undefined { + // In order to not effect the normal schema's cls reference count, create the new context + let schema = convert2CMDSchemaBase({ + ...context, + pendingSchemas: new TwoLevelMap(), + refs: new TwoLevelMap(), + visibility: Visibility.Read, + supportClsSchema: true, + }, type); + + if (schema === undefined) { + return undefined; + } + if (schema.type instanceof ClsType) { schema = getClsDefinitionModel(schema as CMDClsSchemaBase); } @@ -1775,7 +1790,6 @@ function classifyErrorFormat(context: AAZOperationEmitterContext, schema: CMDSch for (const prop of errorSchema.props!) { props[prop.name] = prop; } - if (props.error) { if (props.error.type instanceof ClsType) { errorSchema = getClsDefinitionModel(props.error as CMDClsSchemaBase) as CMDObjectSchemaBase; diff --git a/src/typespec-aaz/src/emit.ts b/src/typespec-aaz/src/emit.ts index bf5bbf3a..c7f536fe 100644 --- a/src/typespec-aaz/src/emit.ts +++ b/src/typespec-aaz/src/emit.ts @@ -123,7 +123,7 @@ function createGetResourceOperationEmitter(context: EmitContext