From 2e640709ae4ddae06e06b39e64c672929b7e7876 Mon Sep 17 00:00:00 2001 From: George Fu Date: Fri, 22 Mar 2024 19:53:36 +0000 Subject: [PATCH] feat(core): wip --- packages/core/src/protocols/index.ts | 2 + .../core/src/protocols/serde/AwsRestJson1.ts | 342 ++++++++++++++++++ .../core/src/protocols/serde/AwsRestXml.ts | 41 +-- .../serde/AwsRuntimeModelInterpreter.ts | 29 ++ .../src/protocols/serde/parameterNameMap.ts | 12 + 5 files changed, 404 insertions(+), 22 deletions(-) create mode 100644 packages/core/src/protocols/serde/AwsRestJson1.ts create mode 100644 packages/core/src/protocols/serde/AwsRuntimeModelInterpreter.ts create mode 100644 packages/core/src/protocols/serde/parameterNameMap.ts diff --git a/packages/core/src/protocols/index.ts b/packages/core/src/protocols/index.ts index 09a6ac214ca00..1a730aeaf492c 100644 --- a/packages/core/src/protocols/index.ts +++ b/packages/core/src/protocols/index.ts @@ -2,3 +2,5 @@ export * from "./coercing-serializers"; export * from "./json/awsExpectUnion"; export * from "./json/parseJsonBody"; export * from "./xml/parseXmlBody"; +export { AwsRestJson1 } from "./serde/AwsRestJson1"; +export { AwsRestXml } from "./serde/AwsRestXml"; diff --git a/packages/core/src/protocols/serde/AwsRestJson1.ts b/packages/core/src/protocols/serde/AwsRestJson1.ts new file mode 100644 index 0000000000000..5ee8db4282074 --- /dev/null +++ b/packages/core/src/protocols/serde/AwsRestJson1.ts @@ -0,0 +1,342 @@ +import type { + ISmithyModelOperationShape, + ISmithyModelShapeId, + ISmithyModelStructureShape, + ISmithyModelTraits, + RuntimeModelInterpreterDeserialization, + RuntimeModelInterpreterSerialization, +} from "@smithy/core"; +import { RequestBuilder, requestBuilder } from "@smithy/core"; +import { + _json, + collectBody, + expectBoolean, + expectByte, + expectNonNull, + expectNumber, + expectString, + expectUnion, + parseEpochTimestamp, + serializeFloat, +} from "@smithy/smithy-client"; +import type { HttpRequest, HttpResponse, ResponseMetadata, SerdeContext } from "@smithy/types"; + +import { parseJsonBody } from "../json/parseJsonBody"; +import { AwsRuntimeModelInterpreter } from "./AwsRuntimeModelInterpreter"; +import { parameterNameMap } from "./parameterNameMap"; + +/** + * + * Model runtime interpreter for RestJson1. + * + * @internal + * + */ +export class AwsRestJson1 + extends AwsRuntimeModelInterpreter + implements RuntimeModelInterpreterSerialization, RuntimeModelInterpreterDeserialization +{ + protected parameterNameMap = parameterNameMap; + + public async serialize( + input: any, + operationShapeId: ISmithyModelShapeId, + context: SerdeContext + ): Promise { + const operationShape = this.se_0_getOperationShape(operationShapeId); + const requestShape = this.se_1_getRequestShape(operationShape.input.target); + const b = requestBuilder(input, context); + const http = operationShape.traits["smithy.api#http"]; + this.se_2_traitHttp(http, b); + + const headers = this.se_2_initHeaders(); + const query = this.se_2_initQuery(); + + let body: any = undefined; + let jsonStringifyBody: boolean | undefined = undefined; + + await this.se_3_iterateRequestShapeMembers( + Object.entries(requestShape.members), + async ([name, { target, traits = {} }]) => { + const httpHeader = traits["smithy.api#httpHeader"]; + const httpPayload = !!traits["smithy.api#httpPayload"]; + const httpLabel = !!traits["smithy.api#httpLabel"]; + const httpQuery = traits["smithy.api#httpQuery"]; + const jsonName = traits["smithy.api#jsonName"]; + + if (input[name] == null) { + return; + } + + if (httpPayload) { + if (body !== undefined) { + throw new Error("incompatible httpPayload member and body member."); + } + jsonStringifyBody = false; + body = this.se_4_memberTraitHttpPayload(httpPayload, name, input); + } else if (httpHeader) { + this.se_4_memberTraitHttpHeader(httpHeader, name, input, headers); + } else if (httpLabel) { + this.se_4_memberTraitHttpLabel(httpLabel, name, input, b); + } else if (httpQuery) { + this.se_4_memberTraitHttpQuery(httpQuery, name, input, query); + } else { + this.se_4_memberWithoutTrait(() => { + if (jsonStringifyBody === false) { + throw new Error("incompatible httpPayload member and body member."); + } + if (input[name] != null) { + body[jsonName ?? name] = this.serializeShape(input[name], target, context); + } + jsonStringifyBody = true; + }); + } + } + ); + + if (jsonStringifyBody) { + body = JSON.stringify(body); + } + + return b.m(http!.method).h(headers).q(query).b(body).build(); + } + + public async deserialize( + httpResponse: HttpResponse, + operationShapeId: ISmithyModelShapeId, + context: SerdeContext + ): Promise { + if (httpResponse.statusCode >= 300) { + this.de_0_handleErrorStatusCode(httpResponse); + } + + const operationShape = this.de_1_getOperationShape(operationShapeId); + const responseShape = this.de_2_getResponseShape(operationShape.output.target); + + let parsedJsonBody: any; + const output = this.de_3_initializeOutputWithMetadata(httpResponse); + + await this.de_5_iterateResponseShapeMembers( + Object.entries(responseShape.members), + async ([name, { target, traits = {} }]) => { + const jsonName = traits["smithy.api#jsonName"]; + const httpResponseCode = traits["smithy.api#httpResponseCode"]; + const httpHeader = traits["smithy.api#httpHeader"]; + const httpPayload = traits["smithy.api#httpPayload"]; + + if (httpResponseCode) { + this.de_6_memberTraitHttpResponseCode(httpResponseCode, name, output, httpResponse); + } else if (httpHeader) { + this.de_6_memberTraitHttpHeader(httpHeader, name, output, httpResponse); + } else if (httpPayload) { + await this.de_6_memberTraitHttpPayload(httpPayload, name, output, httpResponse, context); + } else { + await this.de_6_memberWithoutTrait(async () => { + if (!parsedJsonBody) { + parsedJsonBody = await parseJsonBody(httpResponse.body, context); + } + output[name] = this.deserializeShape(parsedJsonBody[jsonName ?? name], target, context); + }); + } + } + ); + + return output as O; + } + + private serializeShape(input: any, shapeId: ISmithyModelShapeId, context: SerdeContext) { + const shape = this.getShape(shapeId); + switch (shape.type) { + case "blob": + return context.base64Encoder(input); + case "float": + return serializeFloat(input as number); + case "structure": + const struct = {} as any; + for (const [name, { target, traits = {} }] of Object.entries((shape as ISmithyModelStructureShape).members)) { + const jsonName = traits["smithy.api#jsonName"]; + if (input[name] != null) { + struct[jsonName ?? name] = this.serializeShape(input[name], target, context); + } + } + return struct; + case "timestamp": + // TODO handle various timestamp formats + return Math.round((input as Date).getTime() / 1000); + case "union": + // TODO: handle other types + } + return _json(input); + } + + private deserializeShape(output: any, shapeId: ISmithyModelShapeId, context: SerdeContext) { + const shape = this.getShape(shapeId); + switch (shape.type) { + case "blob": + return context.base64Decoder(output as string); + case "union": + return expectUnion(output as object); + case "boolean": + return expectBoolean(output as boolean); + case "byte": + return expectByte(output as number); + case "string": + return expectString(output as string); + case "timestamp": + // TODO handle various timestamp formats + return expectNonNull(parseEpochTimestamp(expectNumber(output as number))); + case "structure": + const struct = {} as any; + for (const [name, { target, traits = {} }] of Object.entries((shape as ISmithyModelStructureShape).members)) { + const jsonName = traits["smithy.api#jsonName"]; + if (output?.[jsonName ?? name]) { + struct[name] = this.deserializeShape(output[jsonName ?? name], target, context); + } + } + return struct; + case "float": + // TODO: handle various number types, BigInt etc. + // TODO: handle lazyjsonstring mediatype + // TODO: handle other types + } + return _json(output); + } + + de_0_handleErrorStatusCode(httpResponse: HttpResponse): void { + // TODO: defer to de_CommandError. + throw new Error("error handler not yet implemented"); + } + + de_1_getOperationShape(operationShapeId: ISmithyModelShapeId): ISmithyModelOperationShape { + return this.getShape(operationShapeId); + } + + de_2_getResponseShape(responseShapeId: ISmithyModelShapeId): ISmithyModelStructureShape { + return this.getShape(responseShapeId); + } + + de_3_initializeOutputWithMetadata(httpResponse: HttpResponse): any { + return { + $metadata: deserializeMetadata(httpResponse), + }; + } + + async de_5_iterateResponseShapeMembers( + entries: [string, ISmithyModelStructureShape["members"][""]][], + iterationFn: (entry: (typeof entries)[0]) => Promise + ): Promise { + await Promise.all(entries.map(iterationFn)); + } + + de_6_memberTraitHttpHeader( + httpHeader: ISmithyModelTraits["smithy.api#httpHeader"], + memberName: string, + output: any, + httpResponse: HttpResponse + ): void { + if (httpResponse.headers[httpHeader!.toLowerCase()]) { + output[memberName] = httpResponse.headers[httpHeader!.toLowerCase()]; + } + } + + async de_6_memberTraitHttpPayload( + httpPayload: ISmithyModelTraits["smithy.api#httpPayload"], + memberName: string, + output: any, + httpResponse: HttpResponse, + context: SerdeContext + ): Promise { + output[memberName] = await collectBody(httpResponse.body, context); + } + + de_6_memberTraitHttpResponseCode( + httpResponseCode: ISmithyModelTraits["smithy.api#httpResponseCode"], + memberName: string, + output: any, + httpResponse: HttpResponse + ): void { + output[memberName] = httpResponse.statusCode; + } + + async de_6_memberWithoutTrait(fn: () => void | Promise): Promise { + await fn(); + } + + se_0_getOperationShape(operationShapeId: ISmithyModelShapeId): ISmithyModelOperationShape { + return this.getShape(operationShapeId); + } + + se_1_getRequestShape(requestShapeId: ISmithyModelShapeId): ISmithyModelStructureShape { + return this.getShape(requestShapeId); + } + + se_2_initHeaders(): Record { + return { + "content-type": "application/json", + }; + } + + se_2_initQuery(): Record { + return {}; + } + + se_2_traitHttp(http: ISmithyModelTraits["smithy.api#http"], b: RequestBuilder): void { + b.bp(http!.uri); + } + + async se_3_iterateRequestShapeMembers( + entries: [string, ISmithyModelStructureShape["members"][""]][], + iterationFn: (entry: (typeof entries)[0]) => Promise + ): Promise { + await Promise.all(entries.map(iterationFn)); + } + + se_4_memberTraitHttpHeader( + httpHeader: ISmithyModelTraits["smithy.api#httpHeader"], + memberName: string, + input: any, + headers: Record + ): void { + headers[httpHeader!] = input[memberName]; + } + + se_4_memberTraitHttpLabel( + httpLabel: ISmithyModelTraits["smithy.api#httpLabel"], + memberName: string, + input: any, + b: RequestBuilder + ): void { + // TODO: determine label greediness. + const isGreedyLabel = false; + b.p(memberName, () => input[memberName], `{${memberName}}`, isGreedyLabel); + } + + se_4_memberTraitHttpPayload( + httpPayload: ISmithyModelTraits["smithy.api#httpPayload"], + memberName: string, + input: any + ): any { + return input[memberName]; + } + + se_4_memberTraitHttpQuery( + httpQuery: ISmithyModelTraits["smithy.api#httpQuery"], + memberName: string, + input: any, + query: Record + ): void { + query[httpQuery!] = input[memberName]; + } + + se_4_memberWithoutTrait(fn: () => void | Promise): void | Promise { + return fn(); + } +} + +const deserializeMetadata = (output: HttpResponse): ResponseMetadata => ({ + httpStatusCode: output.statusCode, + requestId: + output.headers["x-amzn-requestid"] ?? output.headers["x-amzn-request-id"] ?? output.headers["x-amz-request-id"], + extendedRequestId: output.headers["x-amz-id-2"], + cfId: output.headers["x-amz-cf-id"], +}); diff --git a/packages/core/src/protocols/serde/AwsRestXml.ts b/packages/core/src/protocols/serde/AwsRestXml.ts index 86bb34aa69142..becc25ac33dd0 100644 --- a/packages/core/src/protocols/serde/AwsRestXml.ts +++ b/packages/core/src/protocols/serde/AwsRestXml.ts @@ -1,35 +1,32 @@ -// import { XmlNode } from "[@aws-sdk]/xml-builder"; -import { requestBuilder } from "@smithy/core"; -import { RuntimeModelInterpreter } from "@smithy/core"; -import { ISmithyModelShapeId, ISmithyModelStructureShape } from "@smithy/core"; -import { HttpRequest as IHttpRequest, HttpResponse as IHttpResponse, SerdeContext } from "@smithy/types"; +import type { ISmithyModelShapeId } from "@smithy/core"; +import type { HttpRequest, HttpResponse, SerdeContext } from "@smithy/types"; + +import { AwsRuntimeModelInterpreter } from "./AwsRuntimeModelInterpreter"; +import { parameterNameMap } from "./parameterNameMap"; + +/** + * + * Model runtime interpreter for AwsRestXml. + * + * @internal + * + */ +export class AwsRestXml extends AwsRuntimeModelInterpreter { + public parameterNameMap = parameterNameMap; -export class AwsRestXml extends RuntimeModelInterpreter { public async serialize( input: I, requestShapeId: ISmithyModelShapeId, context: SerdeContext - ): Promise { - const requestShape: ISmithyModelStructureShape = this.getShape(requestShapeId); - const b = requestBuilder(input, context); - const headers = {} as Record; - const query = {} as Record; - let body: any; - for (const [name, { target, traits }] of Object.entries(requestShape.members)) { - const headerName = traits["smithy.api#httpHeader"]; - const isPayload = traits["smithy.api#httpPayload"]; - if (traits["smithy.api#httpHeader"]) { - } - } - return null as any; + ): Promise { + throw new Error("not yet implemented"); } public async deserialize( - httpResponse: IHttpResponse, + httpResponse: HttpResponse, responseShapeId: ISmithyModelShapeId, context: SerdeContext ): Promise { - const responseShape: ISmithyModelStructureShape = this.getShape(responseShapeId); - return null as any; + throw new Error("not yet implemented"); } } diff --git a/packages/core/src/protocols/serde/AwsRuntimeModelInterpreter.ts b/packages/core/src/protocols/serde/AwsRuntimeModelInterpreter.ts new file mode 100644 index 0000000000000..2709d1f6e46c1 --- /dev/null +++ b/packages/core/src/protocols/serde/AwsRuntimeModelInterpreter.ts @@ -0,0 +1,29 @@ +import { RuntimeModelInterpreter } from "@smithy/core"; +import { Client, createAggregatedClient } from "@smithy/smithy-client"; + +export abstract class AwsRuntimeModelInterpreter extends RuntimeModelInterpreter { + /** + * @param BaseClient - a constructor for a base client. + * + * @returns a new client constructor with additional methods derived from + * the operations on the service model. Since it is created at runtime, there are no + * typings available for this class. + */ + public create(BaseClient: any): { + new (config?: any): Client & { [method: string]: (...args: any[]) => Promise }; + } { + const commands = {} as Record; + + for (const operation of this.getServiceShape().operations) { + const target = operation.target; + const commandName = target.split("#")[1] + "Command"; + commands[commandName] = this.createCommand(target); + } + + class AwsSdkClient extends BaseClient {} + + createAggregatedClient(commands, AwsSdkClient); + + return AwsSdkClient; + } +} diff --git a/packages/core/src/protocols/serde/parameterNameMap.ts b/packages/core/src/protocols/serde/parameterNameMap.ts new file mode 100644 index 0000000000000..e5d23dc7881dc --- /dev/null +++ b/packages/core/src/protocols/serde/parameterNameMap.ts @@ -0,0 +1,12 @@ +export const parameterNameMap = { + Region: "region", + UseFIPS: "useFipsEndpoint", + UseDualStack: "useDualstackEndpoint", + ForcePathStyle: "forcePathStyle", + Accelerate: "useAccelerateEndpoint", + DisableMRAP: "disableMultiregionAccessPoints", + DisableMultiRegionAccessPoints: "disableMultiregionAccessPoints", + UseArnRegion: "useArnRegion", + Endpoint: "endpoint", + UseGlobalEndpoint: "useGlobalEndpoint", +};