-
Notifications
You must be signed in to change notification settings - Fork 548
/
QueryShapeSerVisitor.java
297 lines (254 loc) · 12.9 KB
/
QueryShapeSerVisitor.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package software.amazon.smithy.aws.typescript.codegen;
import software.amazon.smithy.codegen.core.CodegenException;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.CollectionShape;
import software.amazon.smithy.model.shapes.DocumentShape;
import software.amazon.smithy.model.shapes.MapShape;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.StructureShape;
import software.amazon.smithy.model.shapes.UnionShape;
import software.amazon.smithy.model.traits.SparseTrait;
import software.amazon.smithy.model.traits.TimestampFormatTrait;
import software.amazon.smithy.model.traits.TimestampFormatTrait.Format;
import software.amazon.smithy.model.traits.XmlFlattenedTrait;
import software.amazon.smithy.model.traits.XmlNameTrait;
import software.amazon.smithy.typescript.codegen.TypeScriptWriter;
import software.amazon.smithy.typescript.codegen.integration.DocumentMemberSerVisitor;
import software.amazon.smithy.typescript.codegen.integration.DocumentShapeSerVisitor;
import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator.GenerationContext;
/**
* Visitor to generate serialization functions for shapes in form-urlencoded
* based document bodies.
*
* This class handles function body generation for all types expected by the {@code
* DocumentShapeSerVisitor}. No other shape type serialization is overridden.
*
* Timestamps are serialized to {@link Format}.DATE_TIME by default.
*/
class QueryShapeSerVisitor extends DocumentShapeSerVisitor {
private static final Format TIMESTAMP_FORMAT = Format.DATE_TIME;
QueryShapeSerVisitor(GenerationContext context) {
super(context);
}
private QueryMemberSerVisitor getMemberVisitor(String dataSource) {
return new QueryMemberSerVisitor(getContext(), dataSource, TIMESTAMP_FORMAT);
}
@Override
protected void serializeCollection(GenerationContext context, CollectionShape shape) {
TypeScriptWriter writer = context.getWriter();
MemberShape memberShape = shape.getMember();
Shape target = context.getModel().expectShape(memberShape.getTarget());
// Use the @xmlName trait if present on the member, otherwise use "member".
String locationName = getMemberSerializedLocationName(memberShape, "member");
// Set up a location to store all of the entry pairs.
writer.write("const entries: any = {};");
// Set up a counter to increment the member entries.
writer.write("let counter = 1;");
writer.openBlock("for (let entry of input) {", "}", () -> {
writer.openBlock("if (entry === null) {", "}", () -> {
// Handle the sparse trait by short circuiting null values
// from serialization, and not including them if encountered
// when not sparse.
if (shape.hasTrait(SparseTrait.ID)) {
writer.write("entries[`$L.$${counter}`] = null as any }", locationName);
}
writer.write("continue;");
});
QueryMemberSerVisitor inputVisitor = getMemberVisitor("entry");
if (inputVisitor.visitSuppliesEntryList(target)) {
// Dispatch to the input value provider for any additional handling.
serializeUnnamedMemberEntryList(context, target, "entry", locationName + ".${counter}");
} else {
writer.write("entries[`$L.$${counter}`] = $L;", locationName, target.accept(inputVisitor));
}
writer.write("counter++;");
});
writer.write("return entries;");
}
@Override
protected void serializeDocument(GenerationContext context, DocumentShape shape) {
throw new CodegenException(String.format(
"Cannot serialize Document types in the aws.query protocol, shape: %s.", shape.getId()));
}
@Override
protected void serializeMap(GenerationContext context, MapShape shape) {
TypeScriptWriter writer = context.getWriter();
Model model = context.getModel();
// Filter out null entries if we don't have the sparse trait.
String potentialFilter = "";
if (!shape.hasTrait(SparseTrait.ID)) {
potentialFilter = ".filter((key) => input[key] != null)";
}
// Set up a location to store all of the entry pairs.
writer.write("const entries: any = {};");
// Set up a counter to increment the member entries.
writer.write("let counter = 1;");
// Use the keys as an iteration point to dispatch to the input value providers.
writer.openBlock("Object.keys(input)$L.forEach(key => {", "});", potentialFilter, () -> {
// Prepare the key's entry.
// Use the @xmlName trait if present on the member, otherwise use "key".
MemberShape keyMember = shape.getKey();
Shape keyTarget = model.expectShape(keyMember.getTarget());
String keyName = getMemberSerializedLocationName(keyMember, "key");
writer.write("entries[`entry.$${counter}.$L`] = $L;", keyName, keyTarget.accept(getMemberVisitor("key")));
// Prepare the value's entry.
// Use the @xmlName trait if present on the member, otherwise use "value".
MemberShape valueMember = shape.getValue();
Shape valueTarget = model.expectShape(valueMember.getTarget());
String valueName = getMemberSerializedLocationName(valueMember, "value");
QueryMemberSerVisitor inputVisitor = getMemberVisitor("input[key]");
String valueLocation = "entry.${counter}." + valueName;
if (inputVisitor.visitSuppliesEntryList(valueTarget)) {
serializeUnnamedMemberEntryList(context, valueTarget, "input[key]", valueLocation);
} else {
writer.write("entries[`$L`] = $L;", valueLocation, valueTarget.accept(inputVisitor));
}
writer.write("counter++;");
});
writer.write("return entries;");
}
private void serializeUnnamedMemberEntryList(
GenerationContext context,
Shape target,
String inputLocation,
String entryWrapper
) {
TypeScriptWriter writer = context.getWriter();
QueryMemberSerVisitor inputVisitor = getMemberVisitor(inputLocation);
// Map entries that supply entry lists need to have them joined properly.
writer.write("const memberEntries = $L;", target.accept(inputVisitor));
writer.openBlock("Object.entries(memberEntries).forEach(([key, value]) => {", "});",
() -> writer.write("entries[`$L.$${key}`] = value;", entryWrapper));
}
@Override
protected void serializeStructure(GenerationContext context, StructureShape shape) {
TypeScriptWriter writer = context.getWriter();
// Set up a location to store all of the entry pairs.
writer.write("const entries: any = {};");
// Serialize every member of the structure if present.
shape.getAllMembers().forEach((memberName, memberShape) -> {
String inputLocation = "input." + memberName;
// Handle if the member is an idempotency token that should be auto-filled.
AwsProtocolUtils.writeIdempotencyAutofill(context, memberShape, inputLocation);
writer.openBlock("if ($1L !== undefined && $1L !== null) {", "}", inputLocation,
() -> serializeNamedMember(context, memberName, memberShape, inputLocation));
});
writer.write("return entries");
}
private void serializeNamedMember(
GenerationContext context,
String memberName,
MemberShape memberShape,
String inputLocation
) {
// Grab the target shape so we can use a member serializer on it.
Shape target = context.getModel().expectShape(memberShape.getTarget());
String locationName = getMemberSerializedLocationName(memberShape, memberName);
QueryMemberSerVisitor inputVisitor = getMemberVisitor(inputLocation);
if (inputVisitor.visitSuppliesEntryList(target)) {
serializeNamedMemberEntryList(context, locationName, memberShape, target, inputVisitor);
} else {
serializeNamedMemberValue(context, locationName, "input." + memberName, memberShape, target);
}
}
private void serializeNamedMemberValue(
GenerationContext context,
String locationName,
String dataSource,
MemberShape memberShape,
Shape target
) {
TypeScriptWriter writer = context.getWriter();
// Handle @timestampFormat on members not just the targeted shape.
String valueProvider = memberShape.hasTrait(TimestampFormatTrait.class)
? AwsProtocolUtils.getInputTimestampValueProvider(context, memberShape,
TIMESTAMP_FORMAT, dataSource)
: target.accept(getMemberVisitor(dataSource));
writer.write("entries[$S] = $L;", locationName, valueProvider);
}
/**
* Retrieves the correct serialization location based on the member's
* xmlName trait or uses the default value.
*
* @param memberShape The member being serialized.
* @param defaultValue A default value for the location.
* @return The location where the member will be serialized.
*/
protected String getMemberSerializedLocationName(MemberShape memberShape, String defaultValue) {
// Use the @xmlName trait if present on the member, otherwise use the member name.
return memberShape.getTrait(XmlNameTrait.class)
.map(XmlNameTrait::getValue)
.orElse(defaultValue);
}
private void serializeNamedMemberEntryList(
GenerationContext context,
String locationName,
MemberShape memberShape,
Shape target,
DocumentMemberSerVisitor inputVisitor
) {
TypeScriptWriter writer = context.getWriter();
// Handle flattening for collections and maps.
boolean isFlattened = isFlattenedMember(context, memberShape);
// Set up a location to store all of the entry pairs.
writer.write("const memberEntries = $L;", target.accept(inputVisitor));
// Consolidate every entry in the list.
writer.openBlock("Object.entries(memberEntries).forEach(([key, value]) => {", "});", () -> {
// Remove the last segment for any flattened entities.
if (isFlattened) {
writer.write("const loc = `$L.$${key.substring(key.indexOf('.') + 1)}`;", locationName);
} else {
writer.write("const loc = `$L.$${key}`;", locationName);
}
writer.write("entries[loc] = value;");
});
}
/**
* Tells whether the contents of the member should be flattened
* when serialized.
*
* @param context The generation context.
* @param memberShape The member being serialized.
* @return If the member's contents should be flattened when serialized.
*/
protected boolean isFlattenedMember(GenerationContext context, MemberShape memberShape) {
// The @xmlFlattened trait determines the flattening of members in aws.query.
return memberShape.hasTrait(XmlFlattenedTrait.class);
}
@Override
protected void serializeUnion(GenerationContext context, UnionShape shape) {
TypeScriptWriter writer = context.getWriter();
ServiceShape serviceShape = context.getService();
// Set up a location to store the entry pair.
writer.write("const entries: any = {};");
// Visit over the union type, then get the right serialization for the member.
writer.openBlock("$L.visit(input, {", "});", shape.getId().getName(serviceShape), () -> {
shape.getAllMembers().forEach((memberName, memberShape) -> {
writer.openBlock("$L: value => {", "},", memberName, () -> {
serializeNamedMember(context, memberName, memberShape, "value");
});
});
// Handle the unknown property.
writer.openBlock("_: (name: string, value: any) => {", "}", () -> {
writer.write("entries[name] = value;");
});
});
writer.write("return entries;");
}
}