Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@
package software.amazon.smithy.aws.typescript.codegen;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import software.amazon.smithy.aws.traits.protocols.AwsJson1_0Trait;
import software.amazon.smithy.aws.traits.protocols.AwsJson1_1Trait;
import software.amazon.smithy.aws.traits.protocols.AwsQueryCompatibleTrait;
import software.amazon.smithy.aws.traits.protocols.AwsQueryTrait;
import software.amazon.smithy.aws.traits.protocols.Ec2QueryTrait;
import software.amazon.smithy.aws.traits.protocols.RestJson1Trait;
import software.amazon.smithy.aws.traits.protocols.RestXmlTrait;
import software.amazon.smithy.codegen.core.SymbolProvider;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.traits.XmlNamespaceTrait;
import software.amazon.smithy.protocol.traits.Rpcv2CborTrait;
import software.amazon.smithy.typescript.codegen.LanguageTarget;
import software.amazon.smithy.typescript.codegen.TypeScriptSettings;
import software.amazon.smithy.typescript.codegen.TypeScriptWriter;
Expand Down Expand Up @@ -60,6 +63,13 @@ public void addConfigInterfaceFields(
// by the smithy client config interface.
}

@Override
public List<String> runAfter() {
return List.of(
software.amazon.smithy.typescript.codegen.integration.AddProtocolConfig.class.getCanonicalName()
);
}

@Override
public Map<String, Consumer<TypeScriptWriter>> getRuntimeConfigWriters(
TypeScriptSettings settings,
Expand All @@ -76,6 +86,7 @@ public Map<String, Consumer<TypeScriptWriter>> getRuntimeConfigWriters(
.getTrait(XmlNamespaceTrait.class)
.map(XmlNamespaceTrait::getUri)
.orElse("");
String awsQueryCompat = settings.getService(model).hasTrait(AwsQueryCompatibleTrait.class) ? "true" : "false";

switch (target) {
case SHARED:
Expand Down Expand Up @@ -148,9 +159,15 @@ public Map<String, Consumer<TypeScriptWriter>> getRuntimeConfigWriters(
"AwsJson1_0Protocol", null,
AwsDependency.AWS_SDK_CORE, "/protocols");
writer.write(
"new AwsJson1_0Protocol({ defaultNamespace: $S, serviceTarget: $S })",
"""
new AwsJson1_0Protocol({
defaultNamespace: $S,
serviceTarget: $S,
awsQueryCompatible: $L
})""",
namespace,
rpcTarget
rpcTarget,
awsQueryCompat
);
}
);
Expand All @@ -161,9 +178,32 @@ public Map<String, Consumer<TypeScriptWriter>> getRuntimeConfigWriters(
"AwsJson1_1Protocol", null,
AwsDependency.AWS_SDK_CORE, "/protocols");
writer.write(
"new AwsJson1_1Protocol({ defaultNamespace: $S, serviceTarget: $S })",
"""
new AwsJson1_1Protocol({
defaultNamespace: $S,
serviceTarget: $S,
awsQueryCompatible: $L
})""",
namespace,
rpcTarget,
awsQueryCompat
);
}
);
} else if (Objects.equals(settings.getProtocol(), Rpcv2CborTrait.ID)) {
return MapUtils.of(
"protocol", writer -> {
writer.addImportSubmodule(
"AwsSmithyRpcV2CborProtocol", null,
AwsDependency.AWS_SDK_CORE, "/protocols");
writer.write(
"""
new AwsSmithyRpcV2CborProtocol({
defaultNamespace: $S,
awsQueryCompatible: $L
})""",
namespace,
rpcTarget
awsQueryCompat
);
}
);
Expand Down
163 changes: 163 additions & 0 deletions packages/core/src/submodules/protocols/ProtocolLib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { ErrorSchema, NormalizedSchema, TypeRegistry } from "@smithy/core/schema";
import type {
BodyLengthCalculator,
HttpResponse as IHttpResponse,
MetadataBearer,
ResponseMetadata,
SerdeFunctions,
} from "@smithy/types";
import { calculateBodyLength } from "@smithy/util-body-length-browser";

/**
* @internal
*/
type ErrorMetadataBearer = MetadataBearer & {
$response: IHttpResponse;
$fault: "client" | "server";
};

/**
* Shared code for Protocols.
*
* @internal
*/
export class ProtocolLib {
/**
* @param body - to be inspected.
* @param serdeContext - this is a subset type but in practice is the client.config having a property called bodyLengthChecker.
*
* @returns content-length value for the body if possible.
* @throws Error and should be caught and handled if not possible to determine length.
*/
public calculateContentLength(body: any, serdeContext?: SerdeFunctions) {
const bodyLengthCalculator: BodyLengthCalculator =
(
serdeContext as SerdeFunctions & {
bodyLengthChecker?: BodyLengthCalculator;
}
)?.bodyLengthChecker ?? calculateBodyLength;
return String(bodyLengthCalculator(body));
}

/**
* This is only for REST protocols.
*
* @param defaultContentType - of the protocol.
* @param inputSchema - schema for which to determine content type.
*
* @returns content-type header value or undefined when not applicable.
*/
public resolveRestContentType(defaultContentType: string, inputSchema: NormalizedSchema): string | undefined {
const members = inputSchema.getMemberSchemas();
const httpPayloadMember = Object.values(members).find((m) => {
return !!m.getMergedTraits().httpPayload;
});

if (httpPayloadMember) {
const mediaType = httpPayloadMember.getMergedTraits().mediaType as string;
if (mediaType) {
return mediaType;
} else if (httpPayloadMember.isStringSchema()) {
return "text/plain";
} else if (httpPayloadMember.isBlobSchema()) {
return "application/octet-stream";
} else {
return defaultContentType;
}
} else if (!inputSchema.isUnitSchema()) {
const hasBody = Object.values(members).find((m) => {
const { httpQuery, httpQueryParams, httpHeader, httpLabel, httpPrefixHeaders } = m.getMergedTraits();
return !httpQuery && !httpQueryParams && !httpHeader && !httpLabel && httpPrefixHeaders === void 0;
});
if (hasBody) {
return defaultContentType;
}
}
}

/**
* Shared code for finding error schema or throwing an unmodeled base error.
* @returns error schema and error metadata.
*
* @throws ServiceBaseException or generic Error if no error schema could be found.
*/
public async getErrorSchemaOrThrowBaseException(
errorIdentifier: string,
defaultNamespace: string,
response: IHttpResponse,
dataObject: any,
metadata: ResponseMetadata,
getErrorSchema?: (registry: TypeRegistry, errorName: string) => ErrorSchema
): Promise<{ errorSchema: ErrorSchema; errorMetadata: ErrorMetadataBearer }> {
let namespace = defaultNamespace;
let errorName = errorIdentifier;
if (errorIdentifier.includes("#")) {
[namespace, errorName] = errorIdentifier.split("#");
}

const errorMetadata: ErrorMetadataBearer = {
$metadata: metadata,
$response: response,
$fault: response.statusCode < 500 ? ("client" as const) : ("server" as const),
};

const registry = TypeRegistry.for(namespace);

try {
const errorSchema = getErrorSchema?.(registry, errorName) ?? (registry.getSchema(errorIdentifier) as ErrorSchema);
return { errorSchema, errorMetadata };
} catch (e) {
if (dataObject.Message) {
dataObject.message = dataObject.Message;
}
const baseExceptionSchema = TypeRegistry.for("smithy.ts.sdk.synthetic." + namespace).getBaseException();
if (baseExceptionSchema) {
const ErrorCtor = baseExceptionSchema.ctor;
throw Object.assign(new ErrorCtor({ name: errorName }), errorMetadata, dataObject);
}
throw Object.assign(new Error(errorName), errorMetadata, dataObject);
}
}

/**
* Reads the x-amzn-query-error header for awsQuery compatibility.
*
* @param output - values that will be assigned to an error object.
* @param response - from which to read awsQueryError headers.
*/
public setQueryCompatError(output: Record<string, any>, response: IHttpResponse) {
const queryErrorHeader = response.headers?.["x-amzn-query-error"];

if (output !== undefined && queryErrorHeader != null) {
const [Code, Type] = queryErrorHeader.split(";");
const entries = Object.entries(output);
const Error = {
Code,
Type,
} as any;
Object.assign(output, Error);
for (const [k, v] of entries) {
Error[k] = v;
}
delete Error.__type;
output.Error = Error;
}
}

/**
* Assigns Error, Type, Code from the awsQuery error object to the output error object.
* @param queryCompatErrorData - query compat error object.
* @param errorData - canonical error object returned to the caller.
*/
public queryCompatOutput(queryCompatErrorData: any, errorData: any) {
if (queryCompatErrorData.Error) {
errorData.Error = queryCompatErrorData.Error;
}
if (queryCompatErrorData.Type) {
errorData.Type = queryCompatErrorData.Type;
}
if (queryCompatErrorData.Code) {
errorData.Code = queryCompatErrorData.Code;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { cbor } from "@smithy/core/cbor";
import { op, SCHEMA } from "@smithy/core/schema";
import { error as registerError } from "@smithy/core/schema";
import { HttpResponse } from "@smithy/protocol-http";
import { describe, expect, test as it } from "vitest";

import { AwsSmithyRpcV2CborProtocol } from "./AwsSmithyRpcV2CborProtocol";

describe(AwsSmithyRpcV2CborProtocol.name, () => {
it("should support awsQueryCompatible", async () => {
const protocol = new AwsSmithyRpcV2CborProtocol({
defaultNamespace: "ns",
awsQueryCompatible: true,
});

class MyQueryError extends Error {}

registerError(
"ns",
"MyQueryError",
{ error: "client" },
["Message", "Prop2"],
[SCHEMA.STRING, SCHEMA.NUMERIC],
MyQueryError
);

const body = cbor.serialize({
Message: "oh no",
Prop2: 9999,
});

const error = await (async () => {
return protocol.deserializeResponse(
op("ns", "Operation", 0, "unit", "unit"),
{} as any,
new HttpResponse({
statusCode: 400,
headers: {
"x-amzn-query-error": "MyQueryError;Client",
},
body,
})
);
})().catch((e: any) => e);

expect(error.$metadata).toEqual({
cfId: undefined,
extendedRequestId: undefined,
httpStatusCode: 400,
requestId: undefined,
});

expect(error.$response).toEqual(
new HttpResponse({
body,
headers: {
"x-amzn-query-error": "MyQueryError;Client",
},
reason: undefined,
statusCode: 400,
})
);

expect(error.Code).toEqual(MyQueryError.name);
expect(error.Error.Code).toEqual(MyQueryError.name);

expect(error.Message).toEqual("oh no");
expect(error.Prop2).toEqual(9999);

expect(error.Error.Message).toEqual("oh no");
expect(error.Error.Prop2).toEqual(9999);

expect(error).toMatchObject({
$fault: "client",
Message: "oh no",
message: "oh no",
Prop2: 9999,
Error: {
Code: "MyQueryError",
Message: "oh no",
Type: "Client",
Prop2: 9999,
},
Type: "Client",
Code: "MyQueryError",
});
});
});
Loading
Loading