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