Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[typespec-aaz] Support data-plane #393

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 90 additions & 29 deletions src/typespec-aaz/src/convertor.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 {
Expand Down Expand Up @@ -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),
}
};

Expand All @@ -117,28 +117,76 @@ 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,
}, operation, lroMetadata);
return op;
}

// function extractHostParameters(context: AAZEmitterContext) {
// // TODO: resolve host parameters
// // return context.sdkContext.host;
// }
function extractHostPathAndParameters(context: AAZOperationEmitterContext): { hostPath: string, hostParameters: Record<string, CMDSchema> } | undefined {
const servers = getServers(context.program, context.service.type);
if (servers === undefined || servers.length > 1) {
return undefined;
}
const server = servers[0];
// 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;
}
// using r"\{([^{}]*)}" to iterate over all parameter names in hostPath, the name should not contain '{' or '}'
const hostParameters: Record<string, CMDSchema> = {};
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;
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<string, CMDSchema>): CMDHttpRequest | undefined {
const request: CMDHttpRequest = {
method: operation.verb,
};

let schemaContext;
const methodParams = operation.parameters;
const paramModels: Record<string, Record<string, CMDSchema>> = {};
// 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)) {
Expand All @@ -157,8 +205,6 @@ function extractHttpRequest(context: AAZOperationEmitterContext, operation: Http
continue;
}



schema.required = !httpOpParam.param.optional;

if (paramModels[httpOpParam.type] === undefined) {
Expand Down Expand Up @@ -387,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,
Expand Down Expand Up @@ -1716,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);
}
Expand All @@ -1728,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;
Expand Down Expand Up @@ -1765,7 +1826,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;
Expand Down
2 changes: 1 addition & 1 deletion src/typespec-aaz/src/emit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ function createGetResourceOperationEmitter(context: EmitContext<AAZEmitterOption
}
}
const results = [];
// tracer.trace("resOps", JSON.stringify(resOps, null, 2));

for (const id in resOps) {
if (resOps[id].pathItem) {
results.push(resOps[id]);
Expand Down
6 changes: 6 additions & 0 deletions src/typespec-aaz/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AAZEmitterOptions>,
Expand Down
Loading