1 package software.amazon.smithy.aws.go.codegen;
2 
3 import software.amazon.smithy.codegen.core.CodegenException;
4 import software.amazon.smithy.codegen.core.Symbol;
5 import software.amazon.smithy.go.codegen.CodegenUtils;
6 import software.amazon.smithy.go.codegen.GoWriter;
7 import software.amazon.smithy.go.codegen.SmithyGoDependency;
8 import software.amazon.smithy.go.codegen.integration.ProtocolGenerator;
9 import software.amazon.smithy.go.codegen.integration.ProtocolGenerator.GenerationContext;
10 import software.amazon.smithy.go.codegen.integration.ProtocolUtils;
11 import software.amazon.smithy.go.codegen.knowledge.GoPointableIndex;
12 import software.amazon.smithy.model.shapes.BigDecimalShape;
13 import software.amazon.smithy.model.shapes.BigIntegerShape;
14 import software.amazon.smithy.model.shapes.BlobShape;
15 import software.amazon.smithy.model.shapes.BooleanShape;
16 import software.amazon.smithy.model.shapes.ByteShape;
17 import software.amazon.smithy.model.shapes.CollectionShape;
18 import software.amazon.smithy.model.shapes.DocumentShape;
19 import software.amazon.smithy.model.shapes.DoubleShape;
20 import software.amazon.smithy.model.shapes.FloatShape;
21 import software.amazon.smithy.model.shapes.IntegerShape;
22 import software.amazon.smithy.model.shapes.ListShape;
23 import software.amazon.smithy.model.shapes.LongShape;
24 import software.amazon.smithy.model.shapes.MapShape;
25 import software.amazon.smithy.model.shapes.MemberShape;
26 import software.amazon.smithy.model.shapes.OperationShape;
27 import software.amazon.smithy.model.shapes.ResourceShape;
28 import software.amazon.smithy.model.shapes.ServiceShape;
29 import software.amazon.smithy.model.shapes.SetShape;
30 import software.amazon.smithy.model.shapes.Shape;
31 import software.amazon.smithy.model.shapes.ShapeVisitor;
32 import software.amazon.smithy.model.shapes.ShortShape;
33 import software.amazon.smithy.model.shapes.StringShape;
34 import software.amazon.smithy.model.shapes.StructureShape;
35 import software.amazon.smithy.model.shapes.TimestampShape;
36 import software.amazon.smithy.model.shapes.UnionShape;
37 import software.amazon.smithy.model.traits.EnumTrait;
38 import software.amazon.smithy.model.traits.TimestampFormatTrait.Format;
39 import software.amazon.smithy.model.traits.XmlFlattenedTrait;
40 
41 /**
42  * Visitor to generate member values for aggregate types deserialized from documents.
43  */
44 public class XmlMemberDeserVisitor implements ShapeVisitor<Void> {
45     private final GenerationContext context;
46     private final MemberShape member;
47     private final String dataDest;
48     private final Format timestampFormat;
49     private final GoPointableIndex pointableIndex;
50 
51     // isXmlAttributeMember indicates if member is deserialized from the xml start elements attribute value.
52     private final boolean isXmlAttributeMember;
53     private final boolean isFlattened;
54 
XmlMemberDeserVisitor( GenerationContext context, MemberShape member, String dataDest, Format timestampFormat, boolean isXmlAttributeMember )55     public XmlMemberDeserVisitor(
56             GenerationContext context,
57             MemberShape member,
58             String dataDest,
59             Format timestampFormat,
60             boolean isXmlAttributeMember
61     ) {
62         this.context = context;
63         this.member = member;
64         this.dataDest = dataDest;
65         this.timestampFormat = timestampFormat;
66         this.isXmlAttributeMember = isXmlAttributeMember;
67         this.isFlattened = member.hasTrait(XmlFlattenedTrait.ID);
68         this.pointableIndex = GoPointableIndex.of(context.getModel());
69     }
70 
71     @Override
blobShape(BlobShape shape)72     public Void blobShape(BlobShape shape) {
73         GoWriter writer = context.getWriter();
74         writer.write("var data string");
75         handleString(shape, () -> writer.write("data = xtv"));
76 
77         writer.addUseImports(SmithyGoDependency.BASE64);
78         writer.write("$L, err = base64.StdEncoding.DecodeString(data)", dataDest);
79         writer.write("if err != nil { return err }");
80         return null;
81     }
82 
83     @Override
booleanShape(BooleanShape shape)84     public Void booleanShape(BooleanShape shape) {
85         GoWriter writer = context.getWriter();
86         writer.addUseImports(SmithyGoDependency.FMT);
87         consumeToken(shape);
88 
89         writer.openBlock("{", "}", () -> {
90             writer.addUseImports(SmithyGoDependency.STRCONV);
91             writer.write("xtv, err := strconv.ParseBool(string(val))");
92             writer.openBlock("if err != nil {", "}", () -> {
93                 writer.write("return fmt.Errorf(\"expected $L to be of type *bool, got %T instead\", val)",
94                         shape.getId().getName());
95             });
96             writer.write("$L = $L", dataDest, CodegenUtils.getAsPointerIfPointable(context.getModel(),
97                     context.getWriter(), pointableIndex, member, "xtv"));
98         });
99         return null;
100     }
101 
102     /**
103      * Consumes a single token into the variable "val", returning on any error.
104      * If member is an xmlAttributeMember, "attr" representing xml attribute value is in scope.
105      */
consumeToken(Shape shape)106     private void consumeToken(Shape shape) {
107         GoWriter writer = context.getWriter();
108         // if the member is a modeled as an xml attribute, we do not need to
109         // get another token, instead use the attribute values from previously
110         // decoded start element.
111         if (isXmlAttributeMember) {
112             writer.write("val := []byte(attr.Value)");
113             return;
114         }
115 
116         writer.write("val, err := decoder.Value()");
117         writer.write("if err != nil { return err }");
118         writer.write("if val == nil { break }");
119     }
120 
121     @Override
byteShape(ByteShape shape)122     public Void byteShape(ByteShape shape) {
123         // Smithy's byte shape represents a signed 8-bit int, which doesn't line up with Go's unsigned byte
124         handleInteger(shape, CodegenUtils.getAsPointerIfPointable(context.getModel(), context.getWriter(),
125                 pointableIndex, member, "int8(i64)"));
126         return null;
127     }
128 
129     @Override
shortShape(ShortShape shape)130     public Void shortShape(ShortShape shape) {
131         handleInteger(shape, CodegenUtils.getAsPointerIfPointable(context.getModel(), context.getWriter(),
132                 pointableIndex, member, "int16(i64)"));
133         return null;
134     }
135 
136     @Override
integerShape(IntegerShape shape)137     public Void integerShape(IntegerShape shape) {
138         handleInteger(shape, CodegenUtils.getAsPointerIfPointable(context.getModel(), context.getWriter(),
139                 pointableIndex, member, "int32(i64)"));
140         return null;
141     }
142 
143     @Override
longShape(LongShape shape)144     public Void longShape(LongShape shape) {
145         handleInteger(shape, CodegenUtils.getAsPointerIfPointable(context.getModel(), context.getWriter(),
146                 pointableIndex, member, "i64"));
147         return null;
148     }
149 
150     /**
151      * Deserializes a string representing number without a fractional value.
152      * The 64-bit integer representation of the number is stored in the variable {@code i64}.
153      *
154      * @param shape The shape being deserialized.
155      * @param cast  A wrapping of {@code i64} to cast it to the proper type.
156      */
handleInteger(Shape shape, String cast)157     private void handleInteger(Shape shape, String cast) {
158         GoWriter writer = context.getWriter();
159         handleNumber(shape, () -> {
160             writer.addUseImports(SmithyGoDependency.STRCONV);
161             writer.write("i64, err := strconv.ParseInt(xtv, 10, 64)");
162             writer.write("if err != nil { return err }");
163             writer.write("$L = $L", dataDest, cast);
164         });
165     }
166 
167     /**
168      * Deserializes a xml number string into a xml token.
169      * The number token is stored under the variable {@code xtv}.
170      *
171      * @param shape The shape being deserialized.
172      * @param r     A runnable that runs after the value has been parsed, before the scope closes.
173      */
handleNumber(Shape shape, Runnable r)174     private void handleNumber(Shape shape, Runnable r) {
175         GoWriter writer = context.getWriter();
176         writer.addUseImports(SmithyGoDependency.FMT);
177         consumeToken(shape);
178 
179         writer.openBlock("{", "}", () -> {
180             writer.write("xtv := string(val)");
181             r.run();
182         });
183     }
184 
185     @Override
floatShape(FloatShape shape)186     public Void floatShape(FloatShape shape) {
187         handleFloat(shape, CodegenUtils.getAsPointerIfPointable(context.getModel(), context.getWriter(),
188                 pointableIndex, member, "float32(f64)"));
189         return null;
190     }
191 
192     @Override
doubleShape(DoubleShape shape)193     public Void doubleShape(DoubleShape shape) {
194         handleFloat(shape, CodegenUtils.getAsPointerIfPointable(context.getModel(), context.getWriter(),
195                 pointableIndex, member, "f64"));
196         return null;
197     }
198 
199     /**
200      * Deserializes a string representing number with a fractional value.
201      * The 64-bit float representation of the number is stored in the variable {@code f64}.
202      *
203      * @param shape The shape being deserialized.
204      * @param cast  A wrapping of {@code f64} to cast it to the proper type.
205      */
handleFloat(Shape shape, String cast)206     private void handleFloat(Shape shape, String cast) {
207         GoWriter writer = context.getWriter();
208         handleNumber(shape, () -> {
209             writer.write("f64, err := strconv.ParseFloat(xtv, 64)");
210             writer.write("if err != nil { return err }");
211             writer.write("$L = $L", dataDest, cast);
212         });
213     }
214 
215     @Override
stringShape(StringShape shape)216     public Void stringShape(StringShape shape) {
217         GoWriter writer = context.getWriter();
218         Symbol symbol = context.getSymbolProvider().toSymbol(shape);
219 
220         if (shape.hasTrait(EnumTrait.class)) {
221             handleString(shape, () -> writer.write("$L = $P(xtv)", dataDest, symbol));
222         } else {
223             handleString(shape, () -> writer.write("$L = $L", dataDest, CodegenUtils.getAsPointerIfPointable(
224                     context.getModel(), context.getWriter(), pointableIndex, member, "xtv")));
225         }
226 
227         return null;
228     }
229 
230     /**
231      * Deserializes a xml string into a xml token.
232      * The number token is stored under the variable {@code xtv}.
233      *
234      * @param shape The shape being deserialized.
235      * @param r     A runnable that runs after the value has been parsed, before the scope closes.
236      */
handleString(Shape shape, Runnable r)237     private void handleString(Shape shape, Runnable r) {
238         GoWriter writer = context.getWriter();
239         writer.addUseImports(SmithyGoDependency.FMT);
240         consumeToken(shape);
241 
242         writer.openBlock("{", "}", () -> {
243             writer.write("xtv := string(val)");
244             r.run();
245         });
246     }
247 
248     @Override
timestampShape(TimestampShape shape)249     public Void timestampShape(TimestampShape shape) {
250         GoWriter writer = context.getWriter();
251         writer.addUseImports(SmithyGoDependency.SMITHY_TIME);
252 
253         switch (timestampFormat) {
254             case DATE_TIME:
255                 handleString(shape, () -> {
256                     writer.write("t, err := smithytime.ParseDateTime(xtv)");
257                     writer.write("if err != nil { return err }");
258                     writer.write("$L = $L", dataDest, CodegenUtils.getAsPointerIfPointable(context.getModel(),
259                             context.getWriter(), pointableIndex, member, "t"));
260                 });
261                 break;
262             case HTTP_DATE:
263                 handleString(shape, () -> {
264                     writer.write("t, err := smithytime.ParseHTTPDate(xtv)");
265                     writer.write("if err != nil { return err }");
266                     writer.write("$L = $L", dataDest, CodegenUtils.getAsPointerIfPointable(context.getModel(),
267                             context.getWriter(), pointableIndex, member, "t"));
268                 });
269                 break;
270             case EPOCH_SECONDS:
271                 writer.addUseImports(SmithyGoDependency.SMITHY_PTR);
272                 handleFloat(shape, CodegenUtils.getAsPointerIfPointable(context.getModel(), writer,
273                         pointableIndex, member, "smithytime.ParseEpochSeconds(f64)"));
274                 break;
275             default:
276                 throw new CodegenException(String.format("Unknown timestamp format %s", timestampFormat));
277         }
278         return null;
279     }
280 
281     @Override
bigIntegerShape(BigIntegerShape shape)282     public Void bigIntegerShape(BigIntegerShape shape) {
283         // Fail instead of losing precision through Number.
284         unsupportedShape(shape);
285         return null;
286     }
287 
288     @Override
bigDecimalShape(BigDecimalShape shape)289     public Void bigDecimalShape(BigDecimalShape shape) {
290         // Fail instead of losing precision through Number.
291         unsupportedShape(shape);
292         return null;
293     }
294 
unsupportedShape(Shape shape)295     private void unsupportedShape(Shape shape) {
296         throw new CodegenException(String.format("Cannot deserialize shape type %s on protocol, shape: %s.",
297                 shape.getType(), shape.getId()));
298     }
299 
300     @Override
operationShape(OperationShape shape)301     public Void operationShape(OperationShape shape) {
302         throw new CodegenException("Operation shapes cannot be bound to documents.");
303     }
304 
305     @Override
resourceShape(ResourceShape shape)306     public Void resourceShape(ResourceShape shape) {
307         throw new CodegenException("Resource shapes cannot be bound to documents.");
308     }
309 
310     @Override
serviceShape(ServiceShape shape)311     public Void serviceShape(ServiceShape shape) {
312         throw new CodegenException("Service shapes cannot be bound to documents.");
313     }
314 
315     @Override
memberShape(MemberShape shape)316     public Void memberShape(MemberShape shape) {
317         throw new CodegenException("Member shapes cannot be bound to documents.");
318     }
319 
320     @Override
documentShape(DocumentShape shape)321     public Void documentShape(DocumentShape shape) {
322         writeDelegateFunction(shape);
323         return null;
324     }
325 
326     @Override
structureShape(StructureShape shape)327     public Void structureShape(StructureShape shape) {
328         writeDelegateFunction(shape);
329         return null;
330     }
331 
332     @Override
unionShape(UnionShape shape)333     public Void unionShape(UnionShape shape) {
334         writeDelegateFunction(shape);
335         return null;
336     }
337 
338     @Override
listShape(ListShape shape)339     public Void listShape(ListShape shape) {
340         return collectionShape(shape);
341     }
342 
343     @Override
setShape(SetShape shape)344     public Void setShape(SetShape shape) {
345         return collectionShape(shape);
346     }
347 
collectionShape(CollectionShape shape)348     private Void collectionShape(CollectionShape shape) {
349         if (isFlattened) {
350             writeUnwrappedDelegateFunction(shape);
351         } else {
352             writeDelegateFunction(shape);
353         }
354         return null;
355     }
356 
357     @Override
mapShape(MapShape shape)358     public Void mapShape(MapShape shape) {
359         if (isFlattened) {
360             writeUnwrappedDelegateFunction(shape);
361         } else {
362             writeDelegateFunction(shape);
363         }
364         return null;
365     }
366 
writeDelegateFunction(Shape shape)367     private void writeDelegateFunction(Shape shape) {
368         String functionName = ProtocolGenerator.getDocumentDeserializerFunctionName(shape, context.getProtocolName());
369         GoWriter writer = context.getWriter();
370         writer.write("nodeDecoder := smithyxml.WrapNodeDecoder(decoder.Decoder, t)");
371 
372         ProtocolUtils.writeDeserDelegateFunction(context, writer, member, dataDest, (destVar) -> {
373             writer.openBlock("if err := $L(&$L, nodeDecoder); err != nil {", "}", functionName, destVar, () -> {
374                 writer.write("return err");
375             });
376         });
377     }
378 
getUnwrappedDelegateFunctionName(Shape shape)379     private String getUnwrappedDelegateFunctionName(Shape shape) {
380         return ProtocolGenerator.getDocumentDeserializerFunctionName(shape, context.getProtocolName()) + "Unwrapped";
381     }
382 
writeUnwrappedDelegateFunction(Shape shape)383     private void writeUnwrappedDelegateFunction(Shape shape) {
384         final String functionName = getUnwrappedDelegateFunctionName(shape);
385         final GoWriter writer = context.getWriter();
386 
387         writer.write("nodeDecoder := smithyxml.WrapNodeDecoder(decoder.Decoder, t)");
388 
389         ProtocolUtils.writeDeserDelegateFunction(context, writer, member, dataDest, (destVar) -> {
390             writer.openBlock("if err := $L(&$L, nodeDecoder); err != nil {", "}", functionName, destVar, () -> {
391                 writer.write("return err");
392             });
393         });
394     }
395 }
396