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

Add converter for uint8array map to string and serialization for models #1934

Merged
Merged
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
StreamableMethod,
operationOptionsToRequestParameters,
} from "@azure-rest/core-client";
import { uint8ArrayToString, stringToUint8Array } from "@azure/core-util";
qiaozha marked this conversation as resolved.
Show resolved Hide resolved
import {
PublishCloudEventOptions,
PublishCloudEventsOptions,
Expand All @@ -52,7 +53,20 @@ export function _publishCloudEventSend(
contentType:
(options.contentType as any) ??
"application/cloudevents+json; charset=utf-8",
body: { event: event },
body: {
event: {
id: event["id"],
source: event["source"],
data: event["data"],
data_base64: uint8ArrayToString(event["dataBase64"] ?? "", "base64"),
type: event["type"],
time: new Date(event["time"] ?? ""),
qiaozha marked this conversation as resolved.
Show resolved Hide resolved
specversion: event["specversion"],
dataschema: event["dataschema"],
datacontenttype: event["datacontenttype"],
subject: event["subject"],
},
},
qiaozha marked this conversation as resolved.
Show resolved Hide resolved
});
}

Expand Down Expand Up @@ -167,7 +181,7 @@ export async function _receiveCloudEventsDeserialize(
id: p.event["id"],
source: p.event["source"],
data: p.event["data"],
dataBase64: Buffer.from(p.event["data_base64"] ?? ""),
dataBase64: stringToUint8Array(p.event["data_base64"] ?? "", "base64"),
type: p.event["type"],
time: new Date(p.event["time"] ?? ""),
specversion: p.event["specversion"],
Expand Down
6 changes: 4 additions & 2 deletions packages/typespec-ts/src/modular/buildCodeModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
IntrinsicType,
getProjectedName,
isNullType,
getEncode,
isTemplateDeclarationOrInstance
} from "@typespec/compiler";
import {
Expand Down Expand Up @@ -996,11 +997,12 @@ function emitCredentialUnion(cred_types: CredentialTypeUnion) {
}

function emitStdScalar(
program: Program,
scalar: Scalar & { name: IntrinsicScalarName }
): Record<string, any> {
switch (scalar.name) {
case "bytes":
return { type: "byte-array", format: "byte" };
return { type: "byte-array", format: getEncode(program, scalar) };
case "int8":
case "int16":
case "int32":
Expand Down Expand Up @@ -1095,7 +1097,7 @@ function applyIntrinsicDecorators(
function emitScalar(program: Program, scalar: Scalar): Record<string, any> {
let result: Record<string, any> = {};
if (program.checker.isStdType(scalar)) {
result = emitStdScalar(scalar);
result = emitStdScalar(program, scalar);
} else if (scalar.baseScalar) {
result = emitScalar(program, scalar.baseScalar);
}
Expand Down
10 changes: 10 additions & 0 deletions packages/typespec-ts/src/modular/buildOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Client, Operation } from "./modularCodeModel.js";
import { isRLCMultiEndpoint } from "../utils/clientUtils.js";
import { SdkContext } from "@azure-tools/typespec-client-generator-core";

export const utilImports: Set<string> = new Set<string>();
/**
* This function creates a file under /api for each operation group.
* If there is no operation group in the TypeSpec program, we create a single
Expand All @@ -24,6 +25,7 @@ export function buildOperationFiles(
needUnexpectedHelper: boolean = true
) {
for (const operationGroup of client.operationGroups) {
utilImports.clear();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it feels odd to have a module-level Set that gets cleared by this method. Does the set need to live outside this method or could it be created when buildOperationFiles is called?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put this outside the scope of this buildOperationFiles functions as explained here https://github.com/Azure/autorest.typescript/pull/1934/files#r1282500160

I need to clear this set because the apis per each operationGroup should be generated in an independent file.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh I see. I'm not wholly against it, but it does create some complexity that could cause unexpected behavior later if someone isn't careful.

Maybe we could later refactor this a bit - I remember when I wrote a prototype TS emitter I had a global bag of context that I passed through every generation method that handled things like tracking deps, etc.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion, will take that into consideration when I try to adopt to TCGC.

const fileName = operationGroup.className
? `${operationGroup.className}`
: // When the program has no operation groups defined all operations are put
Expand Down Expand Up @@ -97,6 +99,14 @@ export function buildOperationFiles(
]
}
]);
if (utilImports.size > 0) {
operationGroupFile.addImportDeclarations([
{
moduleSpecifier: "@azure/core-util",
namedImports: [...utilImports.values()]
}
]);
}
modelOptionsFile.addImportDeclarations([
{
moduleSpecifier: "@azure-rest/core-client",
Expand Down
116 changes: 111 additions & 5 deletions packages/typespec-ts/src/modular/helpers/operationHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from "@azure-tools/rlc-common";
import { getOperationName } from "./namingHelpers.js";
import { getFixmeForMultilineDocs } from "./fixmeHelpers.js";
import { utilImports } from "../buildOperations.js";

function getRLCResponseType(rlcResponse?: OperationResponse) {
if (!rlcResponse?.responses) {
Expand Down Expand Up @@ -174,7 +175,7 @@ function getOperationSignatureParameters(
operation.bodyParameter.type
)
);
}
}

operation.parameters
.filter(
Expand Down Expand Up @@ -311,14 +312,25 @@ function buildBodyParameter(bodyParameter: BodyParameter | undefined) {

if (bodyParameter.type.type === "model") {
const bodyParts: string[] = [];
let hasSerializeBody = false;
for (const param of bodyParameter?.type.properties?.filter(
(p) => !p.readonly
) ?? []) {
bodyParts.push(getParameterMap(param));
if (param.type.type === "model") {
hasSerializeBody = true;
bodyParts.push(...getRequestBodyMapping(param.type, param.clientName));
} else {
bodyParts.push(getParameterMap(param));
}
}

if (bodyParameter && bodyParameter.type.properties) {
return `\nbody: {${bodyParts.join(",\n")}},`;
if (hasSerializeBody) {
return `\nbody: {"${bodyParameter.restApiName}": {${bodyParts.join(",\n")}}},`;
} else {
return `\nbody: {${bodyParts.join(",\n")}},`;
}

}
}

Expand Down Expand Up @@ -500,6 +512,67 @@ function getNullableCheck(name: string, type: Type) {

return `${name} === null ? null :`;
}

/**
*
* This function helps translating an HLC request to RLC request,
* extracting properties from body and headers and building the RLC response object
*/
function getRequestBodyMapping(
modelPropertyType: Type,
propertyPath: string = "body"
) {
if (!modelPropertyType.properties || !modelPropertyType.properties) {
return [];
}
const props: string[] = [];
const properties: Property[] = modelPropertyType.properties;
for (const property of properties) {
if (property.readonly) {
continue;
}
const propertyFullName = `${propertyPath}.${property.restApiName}`;
if (property.type.type === "model") {
let definition;
if (property.type.isCoreErrorType) {
definition = `"${property.restApiName}": ${getNullableCheck(
propertyFullName,
property.type
)} ${
!property.optional ? "" : `!${propertyFullName} ? undefined :`
} ${propertyFullName}`;
} else {
definition = `"${property.restApiName}": ${getNullableCheck(
propertyFullName,
property.type
)} ${
!property.optional ? "" : `!${propertyFullName} ? undefined :`
} {${getResponseMapping(
property.type.properties ?? [],
`${propertyPath}.${property.restApiName}${
property.optional ? "?" : ""
}`
)}}`;
}

props.push(definition);
} else {
const dot = propertyPath.endsWith("?") ? "." : "";
const restValue = `${
propertyPath ? `${propertyPath}${dot}` : `${dot}`
}["${property.clientName}"]`;
props.push(
`"${property.restApiName}": ${serializeRequestValue(
property.type,
restValue
)}`
);
}
}

return props;
}

/**
* This function helps translating an RLC response to an HLC response,
* extracting properties from body and headers and building the HLC response object
Expand Down Expand Up @@ -562,8 +635,6 @@ function deserializeResponseValue(type: Type, restValue: string): string {
switch (type.type) {
case "datetime":
return `new Date(${restValue} ?? "")`;
case "byte-array":
return `Buffer.from(${restValue} ?? "")`;
case "list":
if (type.elementType?.type === "model") {
return `(${restValue} ?? []).map(p => ({${getResponseMapping(
Expand All @@ -580,7 +651,42 @@ function deserializeResponseValue(type: Type, restValue: string): string {
} else {
return restValue;
}
case "byte-array":
utilImports.add("stringToUint8Array");
qiaozha marked this conversation as resolved.
Show resolved Hide resolved
return `stringToUint8Array(${restValue} ?? "", "${type.format ?? "base64"}")`;
default:
return restValue;
}
}

/**
* This function helps converting strings into JS complex types recursively.
* We need to drill down into Array elements to make sure that the element type is
* deserialized correctly
*/
function serializeRequestValue(type: Type, restValue: string): string {
switch (type.type) {
case "datetime":
return `new Date(${restValue} ?? "")`;
case "list":
if (type.elementType?.type === "model") {
return `(${restValue} ?? []).map(p => ({${getResponseMapping(
type.elementType?.properties ?? [],
"p"
)}}))`;
} else if (
type.elementType?.properties?.some((p) => needsDeserialize(p.type))
) {
return `(${restValue} ?? []).map(p => ${deserializeResponseValue(
type.elementType!,
"p"
)})`;
} else {
return restValue;
}
case "byte-array":
utilImports.add("uint8ArrayToString");
return `uint8ArrayToString(${restValue} ?? "", "${type.format ?? "base64"}")`;
default:
return restValue;
}
Expand Down
Loading