Skip to content

Commit fd9c923

Browse files
committed
Add start of HTTP binding protocol serde
1 parent 716cc1a commit fd9c923

File tree

1 file changed

+332
-0
lines changed

1 file changed

+332
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
/*
2+
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.smithy.typescript.codegen.integration;
17+
18+
import static software.amazon.smithy.model.knowledge.HttpBinding.Location;
19+
20+
import java.util.List;
21+
import java.util.logging.Logger;
22+
import software.amazon.smithy.codegen.core.CodegenException;
23+
import software.amazon.smithy.codegen.core.Symbol;
24+
import software.amazon.smithy.codegen.core.SymbolProvider;
25+
import software.amazon.smithy.codegen.core.SymbolReference;
26+
import software.amazon.smithy.model.knowledge.HttpBinding;
27+
import software.amazon.smithy.model.knowledge.HttpBindingIndex;
28+
import software.amazon.smithy.model.knowledge.TopDownIndex;
29+
import software.amazon.smithy.model.shapes.BlobShape;
30+
import software.amazon.smithy.model.shapes.BooleanShape;
31+
import software.amazon.smithy.model.shapes.CollectionShape;
32+
import software.amazon.smithy.model.shapes.MemberShape;
33+
import software.amazon.smithy.model.shapes.NumberShape;
34+
import software.amazon.smithy.model.shapes.OperationShape;
35+
import software.amazon.smithy.model.shapes.Shape;
36+
import software.amazon.smithy.model.shapes.ShapeIndex;
37+
import software.amazon.smithy.model.shapes.StringShape;
38+
import software.amazon.smithy.model.shapes.TimestampShape;
39+
import software.amazon.smithy.model.traits.HttpTrait;
40+
import software.amazon.smithy.model.traits.TimestampFormatTrait;
41+
import software.amazon.smithy.typescript.codegen.ApplicationProtocol;
42+
import software.amazon.smithy.typescript.codegen.TypeScriptWriter;
43+
import software.amazon.smithy.utils.OptionalUtils;
44+
45+
/**
46+
* Abstract implementation useful for all protocols that use HTTP bindings.
47+
*/
48+
public abstract class HttpBindingProtocolGenerator implements ProtocolGenerator {
49+
50+
private static final Logger LOGGER = Logger.getLogger(HttpBindingProtocolGenerator.class.getName());
51+
52+
@Override
53+
public ApplicationProtocol getApplicationProtocol() {
54+
return ApplicationProtocol.createDefaultHttpApplicationProtocol();
55+
}
56+
57+
@Override
58+
public void generateRequestSerializers(GenerationContext context) {
59+
TopDownIndex topDownIndex = context.getModel().getKnowledge(TopDownIndex.class);
60+
for (OperationShape operation : topDownIndex.getContainedOperations(context.getService())) {
61+
OptionalUtils.ifPresentOrElse(
62+
operation.getTrait(HttpTrait.class),
63+
httpTrait -> generateOperationSerializer(context, operation, httpTrait),
64+
() -> LOGGER.warning(String.format(
65+
"Unable to generate %s protocol request bindings for %s because it does not have an "
66+
+ "http binding trait", getName(), operation.getId())));
67+
}
68+
}
69+
70+
@Override
71+
public void generateResponseDeserializers(GenerationContext context) {
72+
TopDownIndex topDownIndex = context.getModel().getKnowledge(TopDownIndex.class);
73+
for (OperationShape operation : topDownIndex.getContainedOperations(context.getService())) {
74+
OptionalUtils.ifPresentOrElse(
75+
operation.getTrait(HttpTrait.class),
76+
httpTrait -> generateOperationDeserializer(context, operation, httpTrait),
77+
() -> LOGGER.warning(String.format(
78+
"Unable to generate %s protocol response bindings for %s because it does not have an "
79+
+ "http binding trait", getName(), operation.getId())));
80+
}
81+
}
82+
83+
private void generateOperationSerializer(GenerationContext context, OperationShape operation, HttpTrait trait) {
84+
SymbolProvider symbolProvider = context.getSymbolProvider();
85+
Symbol symbol = symbolProvider.toSymbol(operation);
86+
SymbolReference requestType = getApplicationProtocol().getRequestType();
87+
HttpBindingIndex bindingIndex = context.getModel().getKnowledge(HttpBindingIndex.class);
88+
TypeScriptWriter writer = context.getWriter();
89+
90+
// Ensure that the request type is imported.
91+
writer.addUseImports(requestType);
92+
writer.addImport("SerializerUtils", "SerializerUtils", "@aws-sdk/types");
93+
writer.addImport("Endpoint", "__Endpoint", "@aws-sdk/types");
94+
// e.g., serializeAws_restJson1_1ExecuteStatement
95+
String serializerMethodName = "serialize" + ProtocolGenerator.getSanitizedName(getName()) + symbol.getName();
96+
// Add the normalized input type.
97+
String inputType = symbol.getName() + "Input";
98+
writer.addImport(inputType, inputType, symbol.getNamespace());
99+
100+
writer.openBlock("export function $L(\n"
101+
+ " input: $L,\n"
102+
+ " utils: SerializerUtils\n"
103+
+ "): $T {", "}", serializerMethodName, inputType, requestType, () -> {
104+
List<HttpBinding> labelBindings = writeRequestLabels(context, operation, bindingIndex, trait);
105+
List<HttpBinding> queryBindings = writeRequestQueryString(context, operation, bindingIndex);
106+
writeHeaders(context, operation, bindingIndex);
107+
List<HttpBinding> documentBindings = writeRequestBody(context, operation, bindingIndex);
108+
109+
writer.openBlock("return new $T({", "});", requestType, () -> {
110+
writer.write("...utils.endpoint,");
111+
writer.write("protocol: \"https\",");
112+
writer.write("method: $S,", trait.getMethod());
113+
if (labelBindings.isEmpty()) {
114+
writer.write("path: $S,", trait.getUri());
115+
} else {
116+
writer.write("path: resolvedPath,");
117+
}
118+
writer.write("headers: headers,");
119+
if (!documentBindings.isEmpty()) {
120+
writer.write("body: body,");
121+
}
122+
if (!queryBindings.isEmpty()) {
123+
writer.write("query: query,");
124+
}
125+
});
126+
});
127+
128+
writer.write("");
129+
}
130+
131+
private List<HttpBinding> writeRequestLabels(
132+
GenerationContext context,
133+
OperationShape operation,
134+
HttpBindingIndex bindingIndex,
135+
HttpTrait trait
136+
) {
137+
TypeScriptWriter writer = context.getWriter();
138+
SymbolProvider symbolProvider = context.getSymbolProvider();
139+
List<HttpBinding> labelBindings = bindingIndex.getRequestBindings(operation, Location.LABEL);
140+
141+
if (!labelBindings.isEmpty()) {
142+
ShapeIndex index = context.getModel().getShapeIndex();
143+
writer.write("let resolvedPath = $S;", trait.getUri());
144+
for (HttpBinding binding : labelBindings) {
145+
String memberName = symbolProvider.toMemberName(binding.getMember());
146+
writer.openBlock("if (input.$L !== undefined) {", "}", memberName, () -> {
147+
Shape target = index.getShape(binding.getMember().getTarget()).get();
148+
String labelValue = getInputValue(binding.getLocation(), operation, binding.getMember(), target);
149+
writer.write("resolvedPath = resolvedPath.replace('{$1S}', $L);", labelValue);
150+
});
151+
}
152+
}
153+
154+
return labelBindings;
155+
}
156+
157+
private List<HttpBinding> writeRequestQueryString(
158+
GenerationContext context,
159+
OperationShape operation,
160+
HttpBindingIndex bindingIndex
161+
) {
162+
TypeScriptWriter writer = context.getWriter();
163+
SymbolProvider symbolProvider = context.getSymbolProvider();
164+
List<HttpBinding> queryBindings = bindingIndex.getRequestBindings(operation, Location.QUERY);
165+
166+
if (!queryBindings.isEmpty()) {
167+
ShapeIndex index = context.getModel().getShapeIndex();
168+
writer.write("let query = {};");
169+
for (HttpBinding binding : queryBindings) {
170+
String memberName = symbolProvider.toMemberName(binding.getMember());
171+
writer.openBlock("if (input.$L !== undefined) {", "}", memberName, () -> {
172+
Shape target = index.getShape(binding.getMember().getTarget()).get();
173+
String queryValue = getInputValue(binding.getLocation(), operation, binding.getMember(), target);
174+
writer.write("query['$L'] = $L;", binding.getLocationName(), queryValue);
175+
});
176+
}
177+
}
178+
179+
return queryBindings;
180+
}
181+
182+
private String getInputValue(
183+
Location bindingType,
184+
OperationShape operation,
185+
MemberShape member,
186+
Shape target
187+
) {
188+
String memberName = member.getMemberName();
189+
190+
if (target instanceof StringShape) {
191+
return "input." + memberName;
192+
} else if (target instanceof BooleanShape || target instanceof NumberShape) {
193+
// Just toString on the value.
194+
return "input." + memberName + ".toString()";
195+
} else if (target instanceof TimestampShape) {
196+
return getTimestampInputParam(member, bindingType);
197+
} else if (target instanceof BlobShape) {
198+
// base64 encode
199+
// TODO: fixme (how do we base64 encode?)
200+
throw new UnsupportedOperationException("Not yet implemented");
201+
} else if (target instanceof CollectionShape) {
202+
// TODO: fixme
203+
throw new UnsupportedOperationException("Not yet implemented");
204+
}
205+
206+
throw new CodegenException(String.format(
207+
"Unsupported %s string binding of %s to %s in %s using the %s protocol",
208+
bindingType, memberName, target.getType(), operation, getName()));
209+
}
210+
211+
private static String getTimestampInputParam(MemberShape member, Location bindingType) {
212+
String value = resolveTimestampFormat(member, bindingType);
213+
switch (value) {
214+
case TimestampFormatTrait.DATE_TIME:
215+
return "input." + member.getMemberName() + ".toISOString()";
216+
case TimestampFormatTrait.EPOCH_SECONDS:
217+
return "Math.round(input." + member.getMemberName() + ".getTime() / 1000)";
218+
case TimestampFormatTrait.HTTP_DATE:
219+
return "input." + member.getMemberName() + ".toUTCString()";
220+
default:
221+
throw new CodegenException("Unexpected timestamp format `" + value + "` on " + member);
222+
}
223+
}
224+
225+
// TODO: make this a generic feature of HTTP bindings somehow.
226+
private static String resolveTimestampFormat(MemberShape member, Location bindingType) {
227+
return member.getTrait(TimestampFormatTrait.class).map(TimestampFormatTrait::getValue).orElseGet(() -> {
228+
switch (bindingType) {
229+
case LABEL:
230+
case QUERY:
231+
return TimestampFormatTrait.DATE_TIME;
232+
case HEADER:
233+
return TimestampFormatTrait.HTTP_DATE;
234+
default:
235+
throw new CodegenException("Unexpected timestamp binding location: " + bindingType);
236+
}
237+
});
238+
}
239+
240+
private void writeHeaders(
241+
GenerationContext context,
242+
OperationShape operation,
243+
HttpBindingIndex bindingIndex
244+
) {
245+
TypeScriptWriter writer = context.getWriter();
246+
SymbolProvider symbolProvider = context.getSymbolProvider();
247+
248+
// Headers are always present either from the default document or the payload.
249+
writer.write("let headers: any = {};");
250+
writer.write("headers['Content-Type'] = $S;", bindingIndex.determineRequestContentType(
251+
operation, getDocumentContentType()));
252+
253+
ShapeIndex index = context.getModel().getShapeIndex();
254+
for (HttpBinding binding : bindingIndex.getRequestBindings(operation, Location.HEADER)) {
255+
String memberName = symbolProvider.toMemberName(binding.getMember());
256+
writer.openBlock("if (input.$L !== undefined) {", "}", memberName, () -> {
257+
Shape target = index.getShape(binding.getMember().getTarget()).get();
258+
String headerValue = getInputValue(binding.getLocation(), operation, binding.getMember(), target);
259+
writer.write("headers['$L'] = $L;", binding.getLocationName(), headerValue);
260+
});
261+
}
262+
263+
for (HttpBinding binding : bindingIndex.getRequestBindings(operation, Location.PREFIX_HEADERS)) {
264+
// TODO: httpPrefixHeader params. fixme
265+
throw new UnsupportedOperationException("Not yet implemented: " + binding);
266+
}
267+
}
268+
269+
private List<HttpBinding> writeRequestBody(
270+
GenerationContext context,
271+
OperationShape operation,
272+
HttpBindingIndex bindingIndex
273+
) {
274+
// Write the default `body` property.
275+
context.getWriter().write("let body: any = undefined;");
276+
List<HttpBinding> documentBindings = bindingIndex.getRequestBindings(operation, Location.DOCUMENT);
277+
if (!documentBindings.isEmpty()) {
278+
serializeDocument(context, operation, bindingIndex.getRequestBindings(operation, Location.DOCUMENT));
279+
}
280+
281+
return documentBindings;
282+
}
283+
284+
/**
285+
* Gets the default content-type when a document is synthesized in the body.
286+
*
287+
* @return Returns the default content-type.
288+
*/
289+
protected abstract String getDocumentContentType();
290+
291+
/**
292+
* Writes the code needed to serialize the input document of a request.
293+
*
294+
* @param context The generation context.
295+
* @param operation The operation being generated.
296+
* @param documentBindings The bindings to place in the document.
297+
*/
298+
protected abstract void serializeDocument(
299+
GenerationContext context,
300+
OperationShape operation,
301+
List<HttpBinding> documentBindings
302+
);
303+
304+
private void generateOperationDeserializer(GenerationContext context, OperationShape operation, HttpTrait trait) {
305+
SymbolProvider symbolProvider = context.getSymbolProvider();
306+
Symbol symbol = symbolProvider.toSymbol(operation);
307+
SymbolReference responseType = getApplicationProtocol().getResponseType();
308+
HttpBindingIndex bindingIndex = context.getModel().getKnowledge(HttpBindingIndex.class);
309+
TypeScriptWriter writer = context.getWriter();
310+
311+
// Ensure that the response type is imported.
312+
writer.addUseImports(responseType);
313+
writer.addImport("DeserializerUtils", "DeserializerUtils", "@aws-sdk/types");
314+
// e.g., deserializeAws_restJson1_1ExecuteStatement
315+
String methodName = "deserialize" + ProtocolGenerator.getSanitizedName(getName()) + symbol.getName();
316+
317+
// Add the normalized output type.
318+
String outputType = symbol.getName() + "Output";
319+
writer.addImport(outputType, outputType, symbol.getNamespace());
320+
321+
writer.openBlock("export function $L(\n"
322+
+ " output: $T,\n"
323+
+ " utils: DeserializerUtils\n"
324+
+ "): Promise<$L> {", "}", methodName, responseType, outputType, () -> {
325+
// TODO: Check status code to create appropriate error type or response type.
326+
writeHeaders(context, operation, bindingIndex);
327+
// TODO: response body deserialization.
328+
});
329+
330+
writer.write("");
331+
}
332+
}

0 commit comments

Comments
 (0)