Skip to content

Commit 66f99eb

Browse files
committed
chore(core/protocols): awsQueryCompat support for schema-serde
1 parent 1b11aba commit 66f99eb

File tree

15 files changed

+426
-27
lines changed

15 files changed

+426
-27
lines changed

codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AddProtocolConfig.java

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,23 @@
66
package software.amazon.smithy.aws.typescript.codegen;
77

88
import java.util.Collections;
9+
import java.util.List;
910
import java.util.Map;
1011
import java.util.Objects;
1112
import java.util.function.Consumer;
1213
import software.amazon.smithy.aws.traits.protocols.AwsJson1_0Trait;
1314
import software.amazon.smithy.aws.traits.protocols.AwsJson1_1Trait;
15+
import software.amazon.smithy.aws.traits.protocols.AwsQueryCompatibleTrait;
1416
import software.amazon.smithy.aws.traits.protocols.AwsQueryTrait;
1517
import software.amazon.smithy.aws.traits.protocols.Ec2QueryTrait;
1618
import software.amazon.smithy.aws.traits.protocols.RestJson1Trait;
1719
import software.amazon.smithy.aws.traits.protocols.RestXmlTrait;
1820
import software.amazon.smithy.codegen.core.SymbolProvider;
1921
import software.amazon.smithy.model.Model;
2022
import software.amazon.smithy.model.traits.XmlNamespaceTrait;
23+
import software.amazon.smithy.protocol.traits.Rpcv2CborTrait;
2124
import software.amazon.smithy.typescript.codegen.LanguageTarget;
25+
import software.amazon.smithy.typescript.codegen.TypeScriptDependency;
2226
import software.amazon.smithy.typescript.codegen.TypeScriptSettings;
2327
import software.amazon.smithy.typescript.codegen.TypeScriptWriter;
2428
import software.amazon.smithy.typescript.codegen.integration.TypeScriptIntegration;
@@ -60,6 +64,13 @@ public void addConfigInterfaceFields(
6064
// by the smithy client config interface.
6165
}
6266

67+
@Override
68+
public List<String> runAfter() {
69+
return List.of(
70+
software.amazon.smithy.typescript.codegen.integration.AddProtocolConfig.class.getCanonicalName()
71+
);
72+
}
73+
6374
@Override
6475
public Map<String, Consumer<TypeScriptWriter>> getRuntimeConfigWriters(
6576
TypeScriptSettings settings,
@@ -76,6 +87,7 @@ public Map<String, Consumer<TypeScriptWriter>> getRuntimeConfigWriters(
7687
.getTrait(XmlNamespaceTrait.class)
7788
.map(XmlNamespaceTrait::getUri)
7889
.orElse("");
90+
String awsQueryCompat = settings.getService(model).hasTrait(AwsQueryCompatibleTrait.class) ? "true" : "false";
7991

8092
switch (target) {
8193
case SHARED:
@@ -148,9 +160,15 @@ public Map<String, Consumer<TypeScriptWriter>> getRuntimeConfigWriters(
148160
"AwsJson1_0Protocol", null,
149161
AwsDependency.AWS_SDK_CORE, "/protocols");
150162
writer.write(
151-
"new AwsJson1_0Protocol({ defaultNamespace: $S, serviceTarget: $S })",
163+
"""
164+
new AwsJson1_0Protocol({
165+
defaultNamespace: $S,
166+
serviceTarget: $S,
167+
awsQueryCompatible: $L
168+
})""",
152169
namespace,
153-
rpcTarget
170+
rpcTarget,
171+
awsQueryCompat
154172
);
155173
}
156174
);
@@ -161,9 +179,32 @@ public Map<String, Consumer<TypeScriptWriter>> getRuntimeConfigWriters(
161179
"AwsJson1_1Protocol", null,
162180
AwsDependency.AWS_SDK_CORE, "/protocols");
163181
writer.write(
164-
"new AwsJson1_1Protocol({ defaultNamespace: $S, serviceTarget: $S })",
182+
"""
183+
new AwsJson1_1Protocol({
184+
defaultNamespace: $S,
185+
serviceTarget: $S,
186+
awsQueryCompatible: $L
187+
})""",
188+
namespace,
189+
rpcTarget,
190+
awsQueryCompat
191+
);
192+
}
193+
);
194+
} else if (Objects.equals(settings.getProtocol(), Rpcv2CborTrait.ID)) {
195+
return MapUtils.of(
196+
"protocol", writer -> {
197+
writer.addImportSubmodule(
198+
"AwsSmithyRpcV2CborProtocol", null,
199+
AwsDependency.AWS_SDK_CORE, "/protocols");
200+
writer.write(
201+
"""
202+
new AwsSmithyRpcV2CborProtocol({
203+
defaultNamespace: $S,
204+
awsQueryCompatible: $L
205+
})""",
165206
namespace,
166-
rpcTarget
207+
awsQueryCompat
167208
);
168209
}
169210
);

packages/core/src/submodules/protocols/ProtocolLib.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export class ProtocolLib {
9898
const errorMetadata: ErrorMetadataBearer = {
9999
$metadata: metadata,
100100
$response: response,
101-
$fault: response.statusCode <= 500 ? ("client" as const) : ("server" as const),
101+
$fault: response.statusCode < 500 ? ("client" as const) : ("server" as const),
102102
};
103103

104104
const registry = TypeRegistry.for(namespace);
@@ -118,4 +118,46 @@ export class ProtocolLib {
118118
throw Object.assign(new Error(errorName), errorMetadata, dataObject);
119119
}
120120
}
121+
122+
/**
123+
* Reads the x-amzn-query-error header for awsQuery compatibility.
124+
*
125+
* @param output - values that will be assigned to an error object.
126+
* @param response - from which to read awsQueryError headers.
127+
*/
128+
public setQueryCompatError(output: Record<string, any>, response: IHttpResponse) {
129+
const queryErrorHeader = response.headers?.["x-amzn-query-error"];
130+
131+
if (output !== undefined && queryErrorHeader != null) {
132+
const [Code, Type] = queryErrorHeader.split(";");
133+
const entries = Object.entries(output);
134+
const Error = {
135+
Code,
136+
Type,
137+
} as any;
138+
Object.assign(output, Error);
139+
for (const [k, v] of entries) {
140+
Error[k] = v;
141+
}
142+
delete Error.__type;
143+
output.Error = Error;
144+
}
145+
}
146+
147+
/**
148+
* Assigns Error, Type, Code from the awsQuery error object to the output error object.
149+
* @param queryCompatErrorData - query compat error object.
150+
* @param errorData - canonical error object returned to the caller.
151+
*/
152+
public queryCompatOutput(queryCompatErrorData: any, errorData: any) {
153+
if (queryCompatErrorData.Error) {
154+
errorData.Error = queryCompatErrorData.Error;
155+
}
156+
if (queryCompatErrorData.Type) {
157+
errorData.Type = queryCompatErrorData.Type;
158+
}
159+
if (queryCompatErrorData.Code) {
160+
errorData.Code = queryCompatErrorData.Code;
161+
}
162+
}
121163
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { cbor } from "@smithy/core/cbor";
2+
import { op, SCHEMA } from "@smithy/core/schema";
3+
import { error as registerError } from "@smithy/core/schema";
4+
import { HttpResponse } from "@smithy/protocol-http";
5+
import { describe, expect, test as it } from "vitest";
6+
7+
import { AwsSmithyRpcV2CborProtocol } from "./AwsSmithyRpcV2CborProtocol";
8+
9+
describe(AwsSmithyRpcV2CborProtocol.name, () => {
10+
it("should support awsQueryCompatible", async () => {
11+
const protocol = new AwsSmithyRpcV2CborProtocol({
12+
defaultNamespace: "ns",
13+
awsQueryCompatible: true,
14+
});
15+
16+
class MyQueryError extends Error {}
17+
18+
registerError(
19+
"ns",
20+
"MyQueryError",
21+
{ error: "client" },
22+
["Message", "Prop2"],
23+
[SCHEMA.STRING, SCHEMA.NUMERIC],
24+
MyQueryError
25+
);
26+
27+
const body = cbor.serialize({
28+
Message: "oh no",
29+
Prop2: 9999,
30+
});
31+
32+
const error = await (async () => {
33+
return protocol.deserializeResponse(
34+
op("ns", "Operation", 0, "unit", "unit"),
35+
{} as any,
36+
new HttpResponse({
37+
statusCode: 400,
38+
headers: {
39+
"x-amzn-query-error": "MyQueryError;Client",
40+
},
41+
body,
42+
})
43+
);
44+
})().catch((e: any) => e);
45+
46+
expect(error.$metadata).toEqual({
47+
cfId: undefined,
48+
extendedRequestId: undefined,
49+
httpStatusCode: 400,
50+
requestId: undefined,
51+
});
52+
53+
expect(error.$response).toEqual(
54+
new HttpResponse({
55+
body,
56+
headers: {
57+
"x-amzn-query-error": "MyQueryError;Client",
58+
},
59+
reason: undefined,
60+
statusCode: 400,
61+
})
62+
);
63+
64+
expect(error.Code).toEqual(MyQueryError.name);
65+
expect(error.Error.Code).toEqual(MyQueryError.name);
66+
67+
expect(error.Message).toEqual("oh no");
68+
expect(error.Prop2).toEqual(9999);
69+
70+
expect(error.Error.Message).toEqual("oh no");
71+
expect(error.Error.Prop2).toEqual(9999);
72+
73+
expect(error).toMatchObject({
74+
$fault: "client",
75+
Message: "oh no",
76+
message: "oh no",
77+
Prop2: 9999,
78+
Error: {
79+
Code: "MyQueryError",
80+
Message: "oh no",
81+
Type: "Client",
82+
Prop2: 9999,
83+
},
84+
Type: "Client",
85+
Code: "MyQueryError",
86+
});
87+
});
88+
});
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { loadSmithyRpcV2CborErrorCode, SmithyRpcV2CborProtocol } from "@smithy/core/cbor";
2+
import { NormalizedSchema } from "@smithy/core/schema";
3+
import type {
4+
EndpointBearer,
5+
HandlerExecutionContext,
6+
HttpRequest,
7+
HttpResponse,
8+
OperationSchema,
9+
ResponseMetadata,
10+
SerdeFunctions,
11+
} from "@smithy/types";
12+
13+
import { ProtocolLib } from "../ProtocolLib";
14+
15+
/**
16+
* Extends the Smithy implementation to add AwsQueryCompatibility support.
17+
*
18+
* @alpha
19+
*/
20+
export class AwsSmithyRpcV2CborProtocol extends SmithyRpcV2CborProtocol {
21+
private readonly awsQueryCompatible: boolean;
22+
private readonly mixin = new ProtocolLib();
23+
24+
public constructor({
25+
defaultNamespace,
26+
awsQueryCompatible,
27+
}: {
28+
defaultNamespace: string;
29+
awsQueryCompatible?: boolean;
30+
}) {
31+
super({ defaultNamespace });
32+
this.awsQueryCompatible = !!awsQueryCompatible;
33+
}
34+
35+
/**
36+
* @override
37+
*/
38+
public async serializeRequest<Input extends object>(
39+
operationSchema: OperationSchema,
40+
input: Input,
41+
context: HandlerExecutionContext & SerdeFunctions & EndpointBearer
42+
): Promise<HttpRequest> {
43+
const request = await super.serializeRequest(operationSchema, input, context);
44+
if (this.awsQueryCompatible) {
45+
request.headers["x-amzn-query-mode"] = "true";
46+
}
47+
return request;
48+
}
49+
50+
/**
51+
* @override
52+
*/
53+
protected async handleError(
54+
operationSchema: OperationSchema,
55+
context: HandlerExecutionContext & SerdeFunctions,
56+
response: HttpResponse,
57+
dataObject: any,
58+
metadata: ResponseMetadata
59+
): Promise<never> {
60+
if (this.awsQueryCompatible) {
61+
this.mixin.setQueryCompatError(dataObject, response);
62+
}
63+
const errorName = loadSmithyRpcV2CborErrorCode(response, dataObject) ?? "Unknown";
64+
65+
const { errorSchema, errorMetadata } = await this.mixin.getErrorSchemaOrThrowBaseException(
66+
errorName,
67+
this.options.defaultNamespace,
68+
response,
69+
dataObject,
70+
metadata
71+
);
72+
73+
const ns = NormalizedSchema.of(errorSchema);
74+
const message = dataObject.message ?? dataObject.Message ?? "Unknown";
75+
const exception = new errorSchema.ctor(message);
76+
77+
const output = {} as any;
78+
for (const [name, member] of ns.structIterator()) {
79+
output[name] = this.deserializer.readValue(member, dataObject[name]);
80+
}
81+
82+
if (this.awsQueryCompatible) {
83+
this.mixin.queryCompatOutput(dataObject, output);
84+
}
85+
86+
throw Object.assign(
87+
exception,
88+
errorMetadata,
89+
{
90+
$fault: ns.getMergedTraits().error,
91+
message,
92+
},
93+
output
94+
);
95+
}
96+
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { collectBody } from "@smithy/smithy-client";
22
import type { SerdeFunctions } from "@smithy/types";
3+
import { toUtf8 } from "@smithy/util-utf8";
34

45
export const collectBodyString = (streamBody: any, context: SerdeFunctions): Promise<string> =>
5-
collectBody(streamBody, context).then((body) => context.utf8Encoder(body));
6+
collectBody(streamBody, context).then((body) => (context?.utf8Encoder ?? toUtf8)(body));

packages/core/src/submodules/protocols/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from "./cbor/AwsSmithyRpcV2CborProtocol";
12
export * from "./coercing-serializers";
23
export * from "./json/AwsJson1_0Protocol";
34
export * from "./json/AwsJson1_1Protocol";

packages/core/src/submodules/protocols/json/AwsJson1_0Protocol.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,19 @@ import { AwsJsonRpcProtocol } from "./AwsJsonRpcProtocol";
55
* @see https://smithy.io/2.0/aws/protocols/aws-json-1_1-protocol.html#differences-between-awsjson1-0-and-awsjson1-1
66
*/
77
export class AwsJson1_0Protocol extends AwsJsonRpcProtocol {
8-
public constructor({ defaultNamespace, serviceTarget }: { defaultNamespace: string; serviceTarget: string }) {
8+
public constructor({
9+
defaultNamespace,
10+
serviceTarget,
11+
awsQueryCompatible,
12+
}: {
13+
defaultNamespace: string;
14+
serviceTarget: string;
15+
awsQueryCompatible?: boolean;
16+
}) {
917
super({
1018
defaultNamespace,
1119
serviceTarget,
20+
awsQueryCompatible,
1221
});
1322
}
1423

packages/core/src/submodules/protocols/json/AwsJson1_1Protocol.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,19 @@ import { AwsJsonRpcProtocol } from "./AwsJsonRpcProtocol";
55
* @see https://smithy.io/2.0/aws/protocols/aws-json-1_1-protocol.html#differences-between-awsjson1-0-and-awsjson1-1
66
*/
77
export class AwsJson1_1Protocol extends AwsJsonRpcProtocol {
8-
public constructor({ defaultNamespace, serviceTarget }: { defaultNamespace: string; serviceTarget: string }) {
8+
public constructor({
9+
defaultNamespace,
10+
serviceTarget,
11+
awsQueryCompatible,
12+
}: {
13+
defaultNamespace: string;
14+
serviceTarget: string;
15+
awsQueryCompatible?: boolean;
16+
}) {
917
super({
1018
defaultNamespace,
1119
serviceTarget,
20+
awsQueryCompatible,
1221
});
1322
}
1423

0 commit comments

Comments
 (0)