Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Route53 deserializer customization #792

Merged
merged 7 commits into from
Oct 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,6 @@ protected static GoDependency module(
}

private static final class Versions {
private static final String AWS_SDK = "v0.0.0-20201001231852-1fc1ab173989";
private static final String AWS_SDK = "v0.0.0-20201002231452-4f578e93925d";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public final class AwsCustomGoDependency extends AwsGoDependency {
"service/kinesis/internal/customizations", "kinesiscust");
public static final GoDependency MACHINE_LEARNING_CUSTOMIZATION = aws(
"service/machinelearning/internal/customizations", "mlcust");
public static final GoDependency ROUTE53_CUSTOMIZATION = aws(
"service/route53/internal/customizations", "route53cust");

private AwsCustomGoDependency() {
super();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package software.amazon.smithy.aws.go.codegen.customization;

import java.util.List;
import software.amazon.smithy.aws.traits.ServiceTrait;
import software.amazon.smithy.go.codegen.SymbolUtils;
import software.amazon.smithy.go.codegen.integration.GoIntegration;
import software.amazon.smithy.go.codegen.integration.MiddlewareRegistrar;
import software.amazon.smithy.go.codegen.integration.RuntimeClientPlugin;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.OperationShape;
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.utils.ListUtils;

public class Route53ErrorCustomizations implements GoIntegration {
private static String ADD_ERROR_HANDLER_INTERNAL = "HandleCustomErrorDeserialization";

@Override
public byte getOrder() {
// The associated customization ordering is relative to operation deserializers
// and thus the integration should be added at the end.
return 127;
}

@Override
public List<RuntimeClientPlugin> getClientPlugins() {
return ListUtils.of(
RuntimeClientPlugin.builder()
.operationPredicate(Route53ErrorCustomizations::supportsCustomError)
.registerMiddleware(MiddlewareRegistrar.builder()
.resolvedFunction(SymbolUtils.createValueSymbolBuilder(ADD_ERROR_HANDLER_INTERNAL,
AwsCustomGoDependency.ROUTE53_CUSTOMIZATION).build())
.build())
.build()
);
}

// returns true if the operation supports custom route53 error response
private static boolean supportsCustomError(Model model, ServiceShape service, OperationShape operation){
if (!isRoute53Service(model, service)) {
return false;
}

return operation.getId().getName().equalsIgnoreCase("ChangeResourceRecordSets");
}

// returns true if service is route53
private static boolean isRoute53Service(Model model, ServiceShape service) {
String serviceId= service.expectTrait(ServiceTrait.class).getSdkId();
return serviceId.equalsIgnoreCase("Route 53");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ software.amazon.smithy.aws.go.codegen.customization.MachineLearningCustomization
software.amazon.smithy.aws.go.codegen.customization.S3AcceptEncodingGzip
software.amazon.smithy.aws.go.codegen.customization.KinesisCustomizations
software.amazon.smithy.aws.go.codegen.customization.S3ErrorWith200Status
software.amazon.smithy.aws.go.codegen.customization.Route53ErrorCustomizations
2 changes: 2 additions & 0 deletions service/route53/api_op_ChangeResourceRecordSets.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion service/route53/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/aws/aws-sdk-go-v2/service/route53
go 1.15

require (
github.com/aws/aws-sdk-go-v2 v0.0.0-20201001231852-1fc1ab173989
github.com/aws/aws-sdk-go-v2 v0.0.0-20201005175632-36f8dc6bcc52
github.com/awslabs/smithy-go v0.1.1
)

Expand Down
93 changes: 93 additions & 0 deletions service/route53/internal/customizations/custom_error_deser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package customizations

import (
"bytes"
"context"
"encoding/xml"
"fmt"
"io"
"io/ioutil"
"strings"

"github.com/awslabs/smithy-go"
"github.com/awslabs/smithy-go/middleware"
"github.com/awslabs/smithy-go/ptr"
smithyhttp "github.com/awslabs/smithy-go/transport/http"
smithyxml "github.com/awslabs/smithy-go/xml"

awsmiddle "github.com/aws/aws-sdk-go-v2/aws/middleware"
"github.com/aws/aws-sdk-go-v2/service/route53/types"
)

// HandleCustomErrorDeserialization check if Route53 response is an error and needs
// custom error deserialization.
//
func HandleCustomErrorDeserialization(stack *middleware.Stack) {
stack.Deserialize.Insert(&processResponseMiddleware{}, "OperationDeserializer", middleware.After)
}

// middleware to process raw response and look for error response with InvalidChangeBatch error tag
type processResponseMiddleware struct{}

// ID returns the middleware ID.
func (*processResponseMiddleware) ID() string { return "Route53:ProcessResponseForCustomErrorResponse" }

func (m *processResponseMiddleware) HandleDeserialize(
ctx context.Context, in middleware.DeserializeInput, next middleware.DeserializeHandler) (
out middleware.DeserializeOutput, metadata middleware.Metadata, err error,
) {
out, metadata, err = next.HandleDeserialize(ctx, in)
if err != nil {
return out, metadata, err
}

response, ok := out.RawResponse.(*smithyhttp.Response)
if !ok {
return out, metadata, &smithy.DeserializationError{Err: fmt.Errorf("unknown transport type %T", out.RawResponse)}
}

// check if success response
if response.StatusCode >= 200 && response.StatusCode < 300 {
return
}

var readBuff bytes.Buffer
body := io.TeeReader(response.Body, &readBuff)

rootDecoder := xml.NewDecoder(body)
t, err := smithyxml.FetchRootElement(rootDecoder)
if err == io.EOF {
return out, metadata, nil
}

// rewind response body
response.Body = ioutil.NopCloser(io.MultiReader(&readBuff, response.Body))

// if start tag is "InvalidChangeBatch", the error response needs custom unmarshaling.
if strings.EqualFold(t.Name.Local, "InvalidChangeBatch") {
return out, metadata, route53CustomErrorDeser(&metadata, response)
}

return out, metadata, err
}

// error type for invalidChangeBatchError
type invalidChangeBatchError struct {
Messages []string `xml:"Messages>Message"`
RequestID string `xml:"RequestId"`
}

func route53CustomErrorDeser(metadata *middleware.Metadata, response *smithyhttp.Response) error {
err := invalidChangeBatchError{}
xml.NewDecoder(response.Body).Decode(&err)

// set request id in metadata
if len(err.RequestID) != 0 {
awsmiddle.SetRequestIDMetadata(metadata, err.RequestID)
}

return &types.InvalidChangeBatch{
Message: ptr.String("ChangeBatch errors occurred"),
Messages: ptr.StringSlice(err.Messages),
}
skotambkar marked this conversation as resolved.
Show resolved Hide resolved
}
123 changes: 123 additions & 0 deletions service/route53/internal/customizations/custom_error_deser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package customizations_test

import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/route53"
"github.com/aws/aws-sdk-go-v2/service/route53/types"
)

func TestCustomErrorDeserialization(t *testing.T) {
cases := map[string]struct {
responseStatus int
responseBody []byte
expectedError string
expectedRequestID string
expectedResponseID string
}{
"invalidChangeBatchError": {
skotambkar marked this conversation as resolved.
Show resolved Hide resolved
responseStatus: 500,
responseBody: []byte(`<?xml version="1.0" encoding="UTF-8"?>
<InvalidChangeBatch xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
<Messages>
<Message>Tried to create resource record set duplicate.example.com. type A, but it already exists</Message>
</Messages>
<RequestId>b25f48e8-84fd-11e6-80d9-574e0c4664cb</RequestId>
</InvalidChangeBatch>`),
expectedError: "InvalidChangeBatch: ChangeBatch errors occurred",
expectedRequestID: "b25f48e8-84fd-11e6-80d9-574e0c4664cb",
},

"standardRestXMLError": {
responseStatus: 500,
responseBody: []byte(`<?xml version="1.0"?>
<ErrorResponse xmlns="http://route53.amazonaws.com/doc/2016-09-07/">
<Error>
<Type>Sender</Type>
<Code>MalformedXML</Code>
<Message>1 validation error detected: Value null at 'route53#ChangeSet' failed to satisfy constraint: Member must not be null</Message>
</Error>
<RequestId>b25f48e8-84fd-11e6-80d9-574e0c4664cb</RequestId>
</ErrorResponse>
`),
expectedError: "1 validation error detected:",
expectedRequestID: "b25f48e8-84fd-11e6-80d9-574e0c4664cb",
},

"Success response": {
responseStatus: 200,
responseBody: []byte(`<?xml version="1.0" encoding="UTF-8"?>
<ChangeResourceRecordSetsResponse>
<ChangeInfo>
<Comment>mockComment</Comment>
<Id>mockID</Id>
</ChangeInfo>
</ChangeResourceRecordSetsResponse>`),
expectedResponseID: "mockID",
},
}

for name, c := range cases {
server := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(c.responseStatus)
w.Write(c.responseBody)
}))
defer server.Close()

t.Run(name, func(t *testing.T) {
svc := route53.NewFromConfig(aws.Config{
Region: "us-east-1",
EndpointResolver: aws.EndpointResolverFunc(func(service, region string) (aws.Endpoint, error) {
return aws.Endpoint{
URL: server.URL,
SigningName: "route53",
}, nil
}),
Retryer: aws.NoOpRetryer{},
})
resp, err := svc.ChangeResourceRecordSets(context.Background(), &route53.ChangeResourceRecordSetsInput{
ChangeBatch: &types.ChangeBatch{
Changes: []*types.Change{},
Comment: aws.String("mock"),
},
HostedZoneId: aws.String("zone"),
})

if err == nil && len(c.expectedError) != 0 {
t.Fatalf("expected err, got none")
}

if len(c.expectedError) != 0 {
if e, a := c.expectedError, err.Error(); !strings.Contains(a, e) {
t.Fatalf("expected error to be %s, got %s", e, a)
}

var responseError interface {
ServiceRequestID() string
}

if !errors.As(err, &responseError) {
t.Fatalf("expected error to be of type %T, was not", responseError)
}

if e, a := c.expectedRequestID, responseError.ServiceRequestID(); !strings.EqualFold(e, a) {
t.Fatalf("expected request id to be %s, got %s", e, a)
}
}

if len(c.expectedResponseID) != 0 {
if e, a := c.expectedResponseID, *resp.ChangeInfo.Id; !strings.EqualFold(e, a) {
t.Fatalf("expected response to have id %v, got %v", e, a)
}
}

})
}
}
41 changes: 41 additions & 0 deletions service/route53/internal/customizations/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Package customizations provides customizations for the Amazon Route53 API client.
//
// This package provides support for following customizations
//
// Process Response Middleware: used for custom error deserializing
skotambkar marked this conversation as resolved.
Show resolved Hide resolved
//
//
// Process Response Middleware
//
// Route53 operation "ChangeResourceRecordSets" can have an error response returned in
// a slightly different format. This customization is only applicable to
// ChangeResourceRecordSets operation of Route53.
//
// Here's a sample error response:
//
// <?xml version="1.0" encoding="UTF-8"?>
// <InvalidChangeBatch xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
// <Messages>
// <Message>Tried to create resource record set duplicate.example.com. type A, but it already exists</Message>
// </Messages>
// </InvalidChangeBatch>
//
//
// The processResponse middleware customizations enables SDK to check for an error
// response starting with "InvalidChangeBatch" tag prior to deserialization.
//
// As this check in error response needs to be performed earlier than response
// deserialization. Since the behavior of Deserialization is in
// reverse order to the other stack steps its easier to consider that "after" means
// "before".
//
// Middleware layering:
//
// HTTP Response -> process response error -> deserialize
//
//
// In case the returned error response has `InvalidChangeBatch` format, the error is
// deserialized and returned. The operation deserializer does not attempt to deserialize
// as an error is returned by the process response error middleware.
//
package customizations