diff --git a/.changelog/419f3a1b33d841779c9656a7d69c63e6.json b/.changelog/419f3a1b33d841779c9656a7d69c63e6.json new file mode 100644 index 00000000000..b8e633c1f3a --- /dev/null +++ b/.changelog/419f3a1b33d841779c9656a7d69c63e6.json @@ -0,0 +1,9 @@ +{ + "id": "419f3a1b-33d8-4177-9c96-56a7d69c63e6", + "type": "feature", + "description": "Add support for polly SynthesizeSpeech GET request presigner", + "modules": [ + ".", + "service/polly" + ] +} \ No newline at end of file diff --git a/aws/protocol/query/object.go b/aws/protocol/query/object.go index 6a99d4ea8f6..455b92515ca 100644 --- a/aws/protocol/query/object.go +++ b/aws/protocol/query/object.go @@ -41,6 +41,12 @@ func (o *Object) Key(name string) Value { return o.key(name, false) } +// KeyWithValues adds the given named key to the Query object. +// Returns a Value encoder that should be used to encode a Query list of values. +func (o *Object) KeyWithValues(name string) Value { + return o.keyWithValues(name, false) +} + // FlatKey adds the given named key to the Query object. // Returns a Value encoder that should be used to encode a Query value type. The // value will be flattened if it is a map or array. @@ -54,3 +60,10 @@ func (o *Object) key(name string, flatValue bool) Value { } return newValue(o.values, name, flatValue) } + +func (o *Object) keyWithValues(name string, flatValue bool) Value { + if o.prefix != "" { + return newAppendValue(o.values, fmt.Sprintf("%s.%s", o.prefix, name), flatValue) + } + return newAppendValue(o.values, name, flatValue) +} diff --git a/aws/protocol/query/value.go b/aws/protocol/query/value.go index 302525ab101..a9251521f12 100644 --- a/aws/protocol/query/value.go +++ b/aws/protocol/query/value.go @@ -27,6 +27,15 @@ func newValue(values url.Values, key string, flat bool) Value { } } +func newAppendValue(values url.Values, key string, flat bool) Value { + return Value{ + values: values, + key: key, + flat: flat, + queryValue: httpbinding.NewQueryValue(values, key, true), + } +} + func newBaseValue(values url.Values) Value { return Value{ values: values, diff --git a/codegen/smithy-aws-go-codegen/src/main/java/software/amazon/smithy/aws/go/codegen/AwsHttpPresignURLClientGenerator.java b/codegen/smithy-aws-go-codegen/src/main/java/software/amazon/smithy/aws/go/codegen/AwsHttpPresignURLClientGenerator.java index 7f3cd527c0a..0bead7b51c6 100644 --- a/codegen/smithy-aws-go-codegen/src/main/java/software/amazon/smithy/aws/go/codegen/AwsHttpPresignURLClientGenerator.java +++ b/codegen/smithy-aws-go-codegen/src/main/java/software/amazon/smithy/aws/go/codegen/AwsHttpPresignURLClientGenerator.java @@ -101,6 +101,9 @@ public class AwsHttpPresignURLClientGenerator implements GoIntegration { ShapeId.from("com.amazonaws.sts#AWSSecurityTokenServiceV20110615"), SetUtils.of( ShapeId.from("com.amazonaws.sts#GetCallerIdentity"), ShapeId.from("com.amazonaws.sts#AssumeRole") + ), + ShapeId.from("com.amazonaws.polly#Parrot_v1"), SetUtils.of( + ShapeId.from("com.amazonaws.polly#SynthesizeSpeech") ) ); @@ -381,8 +384,8 @@ private void writeConvertToPresignMiddleware( writer.write("return err"); }); - // if protocol used is ec2query or query - if (serviceShape.hasTrait(AwsQueryTrait.ID) || serviceShape.hasTrait(Ec2QueryTrait.ID)) { + // if protocol used is ec2query or query or if service is polly + if (serviceShape.hasTrait(AwsQueryTrait.ID) || serviceShape.hasTrait(Ec2QueryTrait.ID) || isPollyServiceShape(serviceShape)) { // presigned url should convert to Get request Symbol queryAsGetMiddleware = SymbolUtils.createValueSymbolBuilder("AddAsGetRequestMiddleware", AwsGoDependency.AWS_QUERY_PROTOCOL).build(); @@ -392,6 +395,15 @@ private void writeConvertToPresignMiddleware( writer.write("if err != nil { return err }"); } + // polly presigner needs to serialize input param into query string + if (isPollyServiceShape(serviceShape)) { + Symbol serializeInputMiddleware = SymbolUtils.createValueSymbolBuilder("AddPresignSynthesizeSpeechMiddleware", + AwsGoDependency.AWS_QUERY_PROTOCOL).build(); + writer.writeDocs("use query encoder to encode GET request query string"); + writer.write("err = AddPresignSynthesizeSpeechMiddleware(stack)"); + writer.write("if err != nil { return err }"); + } + // s3 service needs expires and sets unsignedPayload if input is stream if (isS3ServiceShape(model, serviceShape)) { @@ -682,5 +694,9 @@ private final boolean isS3ServiceShape(Model model, ServiceShape service) { String serviceId = service.expectTrait(ServiceTrait.class).getSdkId(); return serviceId.equalsIgnoreCase("S3"); } + + private final boolean isPollyServiceShape(ServiceShape service) { + return service.expectTrait(ServiceTrait.class).getSdkId().equalsIgnoreCase("Polly"); + } } diff --git a/service/polly/api_client.go b/service/polly/api_client.go index ab8cb65f2cb..2a9e67f8b2e 100644 --- a/service/polly/api_client.go +++ b/service/polly/api_client.go @@ -8,10 +8,12 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws/defaults" awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" + "github.com/aws/aws-sdk-go-v2/aws/protocol/query" "github.com/aws/aws-sdk-go-v2/aws/retry" "github.com/aws/aws-sdk-go-v2/aws/signer/v4" awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" internalConfig "github.com/aws/aws-sdk-go-v2/internal/configsources" + presignedurlcust "github.com/aws/aws-sdk-go-v2/service/internal/presigned-url" smithy "github.com/aws/smithy-go" smithydocument "github.com/aws/smithy-go/document" "github.com/aws/smithy-go/logging" @@ -486,6 +488,112 @@ func addResponseErrorMiddleware(stack *middleware.Stack) error { return awshttp.AddResponseErrorMiddleware(stack) } +// HTTPPresignerV4 represents presigner interface used by presign url client +type HTTPPresignerV4 interface { + PresignHTTP( + ctx context.Context, credentials aws.Credentials, r *http.Request, + payloadHash string, service string, region string, signingTime time.Time, + optFns ...func(*v4.SignerOptions), + ) (url string, signedHeader http.Header, err error) +} + +// PresignOptions represents the presign client options +type PresignOptions struct { + + // ClientOptions are list of functional options to mutate client options used by + // the presign client. + ClientOptions []func(*Options) + + // Presigner is the presigner used by the presign url client + Presigner HTTPPresignerV4 +} + +func (o PresignOptions) copy() PresignOptions { + clientOptions := make([]func(*Options), len(o.ClientOptions)) + copy(clientOptions, o.ClientOptions) + o.ClientOptions = clientOptions + return o +} + +// WithPresignClientFromClientOptions is a helper utility to retrieve a function +// that takes PresignOption as input +func WithPresignClientFromClientOptions(optFns ...func(*Options)) func(*PresignOptions) { + return withPresignClientFromClientOptions(optFns).options +} + +type withPresignClientFromClientOptions []func(*Options) + +func (w withPresignClientFromClientOptions) options(o *PresignOptions) { + o.ClientOptions = append(o.ClientOptions, w...) +} + +// PresignClient represents the presign url client +type PresignClient struct { + client *Client + options PresignOptions +} + +// NewPresignClient generates a presign client using provided API Client and +// presign options +func NewPresignClient(c *Client, optFns ...func(*PresignOptions)) *PresignClient { + var options PresignOptions + for _, fn := range optFns { + fn(&options) + } + if len(options.ClientOptions) != 0 { + c = New(c.options, options.ClientOptions...) + } + + if options.Presigner == nil { + options.Presigner = newDefaultV4Signer(c.options) + } + + return &PresignClient{ + client: c, + options: options, + } +} + +func withNopHTTPClientAPIOption(o *Options) { + o.HTTPClient = smithyhttp.NopClient{} +} + +type presignConverter PresignOptions + +func (c presignConverter) convertToPresignMiddleware(stack *middleware.Stack, options Options) (err error) { + stack.Finalize.Clear() + stack.Deserialize.Clear() + stack.Build.Remove((*awsmiddleware.ClientRequestID)(nil).ID()) + stack.Build.Remove("UserAgent") + pmw := v4.NewPresignHTTPRequestMiddleware(v4.PresignHTTPRequestMiddlewareOptions{ + CredentialsProvider: options.Credentials, + Presigner: c.Presigner, + LogSigning: options.ClientLogMode.IsSigning(), + }) + err = stack.Finalize.Add(pmw, middleware.After) + if err != nil { + return err + } + if err = smithyhttp.AddNoPayloadDefaultContentTypeRemover(stack); err != nil { + return err + } + // convert request to a GET request + err = query.AddAsGetRequestMiddleware(stack) + if err != nil { + return err + } + // use query encoder to encode GET request query string + err = AddPresignSynthesizeSpeechMiddleware(stack) + if err != nil { + return err + } + err = presignedurlcust.AddAsIsPresigingMiddleware(stack) + if err != nil { + return err + } + return nil +} + func addRequestResponseLogging(stack *middleware.Stack, o Options) error { return stack.Deserialize.Add(&smithyhttp.RequestResponseLogger{ LogRequest: o.ClientLogMode.IsRequest(), diff --git a/service/polly/api_op_SynthesizeSpeech.go b/service/polly/api_op_SynthesizeSpeech.go index 304471ba443..0f13281f8a6 100644 --- a/service/polly/api_op_SynthesizeSpeech.go +++ b/service/polly/api_op_SynthesizeSpeech.go @@ -214,6 +214,30 @@ func newServiceMetadataMiddleware_opSynthesizeSpeech(region string) *awsmiddlewa } } +// PresignSynthesizeSpeech is used to generate a presigned HTTP Request which +// contains presigned URL, signed headers and HTTP method used. +func (c *PresignClient) PresignSynthesizeSpeech(ctx context.Context, params *SynthesizeSpeechInput, optFns ...func(*PresignOptions)) (*v4.PresignedHTTPRequest, error) { + if params == nil { + params = &SynthesizeSpeechInput{} + } + options := c.options.copy() + for _, fn := range optFns { + fn(&options) + } + clientOptFns := append(options.ClientOptions, withNopHTTPClientAPIOption) + + result, _, err := c.client.invokeOperation(ctx, "SynthesizeSpeech", params, clientOptFns, + c.client.addOperationSynthesizeSpeechMiddlewares, + presignConverter(options).convertToPresignMiddleware, + ) + if err != nil { + return nil, err + } + + out := result.(*v4.PresignedHTTPRequest) + return out, nil +} + type opSynthesizeSpeechResolveEndpointMiddleware struct { EndpointResolver EndpointResolverV2 BuiltInResolver builtInParameterResolver diff --git a/service/polly/generated.json b/service/polly/generated.json index 07842156213..c99a5aad061 100644 --- a/service/polly/generated.json +++ b/service/polly/generated.json @@ -3,6 +3,7 @@ "github.com/aws/aws-sdk-go-v2": "v1.4.0", "github.com/aws/aws-sdk-go-v2/internal/configsources": "v0.0.0-00010101000000-000000000000", "github.com/aws/aws-sdk-go-v2/internal/endpoints/v2": "v2.0.0-00010101000000-000000000000", + "github.com/aws/aws-sdk-go-v2/service/internal/presigned-url": "v1.0.7", "github.com/aws/smithy-go": "v1.4.0", "github.com/google/go-cmp": "v0.5.4" }, diff --git a/service/polly/presign.go b/service/polly/presign.go new file mode 100644 index 00000000000..2e0d96277e7 --- /dev/null +++ b/service/polly/presign.go @@ -0,0 +1,100 @@ +package polly + +import ( + "bytes" + "context" + "fmt" + "github.com/aws/aws-sdk-go-v2/aws/protocol/query" + "github.com/aws/smithy-go" + "github.com/aws/smithy-go/middleware" + smithyhttp "github.com/aws/smithy-go/transport/http" +) + +// AddPresignSynthesizeSpeechMiddleware adds presignOpSynthesizeSpeechInput into middleware stack to +// parse SynthesizeSpeechInput into request stream +func AddPresignSynthesizeSpeechMiddleware(stack *middleware.Stack) error { + return stack.Serialize.Insert(&presignOpSynthesizeSpeechInput{}, "Query:AsGetRequest", middleware.Before) +} + +// presignOpSynthesizeSpeechInput encodes SynthesizeSpeechInput into url format +// query string and put that into request stream for later presign-url build +type presignOpSynthesizeSpeechInput struct { +} + +func (*presignOpSynthesizeSpeechInput) ID() string { + return "PresignSerializer" +} + +func (m *presignOpSynthesizeSpeechInput) HandleSerialize(ctx context.Context, in middleware.SerializeInput, next middleware.SerializeHandler) ( + out middleware.SerializeOutput, metadata middleware.Metadata, err error, +) { + request, ok := in.Request.(*smithyhttp.Request) + if !ok { + return out, metadata, &smithy.SerializationError{Err: fmt.Errorf("unknown transport type %T", in.Request)} + } + + input, ok := in.Parameters.(*SynthesizeSpeechInput) + _ = input + if !ok { + return out, metadata, &smithy.SerializationError{Err: fmt.Errorf("unknown input parameters type %T", in.Parameters)} + } + + bodyWriter := bytes.NewBuffer(nil) + bodyEncoder := query.NewEncoder(bodyWriter) + + if err := presignSerializeOpDocumentSynthesizeSpeechInput(input, bodyEncoder.Value); err != nil { + return out, metadata, &smithy.SerializationError{Err: err} + } + + err = bodyEncoder.Encode() + if err != nil { + return out, metadata, &smithy.SerializationError{Err: err} + } + + if request, err = request.SetStream(bytes.NewReader(bodyWriter.Bytes())); err != nil { + return out, metadata, &smithy.SerializationError{Err: err} + } + + in.Request = request + + return next.HandleSerialize(ctx, in) +} + +func presignSerializeOpDocumentSynthesizeSpeechInput(v *SynthesizeSpeechInput, value query.Value) error { + object := value.Object() + _ = object + + if v.LexiconNames != nil && len(v.LexiconNames) > 0 { + objectKey := object.KeyWithValues("LexiconNames") + for _, name := range v.LexiconNames { + objectKey.String(name) + } + } + + if len(v.OutputFormat) > 0 { + objectKey := object.Key("OutputFormat") + objectKey.String(string(v.OutputFormat)) + } + + if v.SampleRate != nil { + objectKey := object.Key("SampleRate") + objectKey.String(*v.SampleRate) + } + + if v.Text != nil { + objectKey := object.Key("Text") + objectKey.String(*v.Text) + } + + if len(v.TextType) > 0 { + objectKey := object.Key("TextType") + objectKey.String(string(v.TextType)) + } + + if len(v.VoiceId) > 0 { + objectKey := object.Key("VoiceId") + objectKey.String(string(v.VoiceId)) + } + + return nil +} diff --git a/service/polly/presign_test.go b/service/polly/presign_test.go new file mode 100644 index 00000000000..06b1f3e937f --- /dev/null +++ b/service/polly/presign_test.go @@ -0,0 +1,98 @@ +package polly + +import ( + "context" + "fmt" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/polly/types" + "github.com/aws/smithy-go/middleware" + smithyhttp "github.com/aws/smithy-go/transport/http" + "io/ioutil" + "testing" +) + +func TestPresignOpSynthesizeSpeechInput(t *testing.T) { + cases := map[string]struct { + LexiconNames []string + OutputFormat types.OutputFormat + SampleRate *string + Text *string + TextType types.TextType + VoiceID types.VoiceId + ExpectStream string + Error error + ExpectError bool + }{ + "Single LexiconNames": { + LexiconNames: []string{"abc"}, + OutputFormat: types.OutputFormatMp3, + SampleRate: aws.String("128"), + Text: aws.String("Test"), + TextType: types.TextTypeText, + VoiceID: types.VoiceIdAmy, + ExpectStream: "LexiconNames=abc&OutputFormat=mp3&SampleRate=128&Text=Test&TextType=text&VoiceId=Amy", + }, + "Multiple LexiconNames": { + LexiconNames: []string{"abc", "mno"}, + OutputFormat: types.OutputFormatMp3, + SampleRate: aws.String("128"), + Text: aws.String("Test"), + TextType: types.TextTypeText, + VoiceID: types.VoiceIdAmy, + ExpectStream: "LexiconNames=abc&LexiconNames=mno&OutputFormat=mp3&SampleRate=128&Text=Test&TextType=text&VoiceId=Amy", + }, + "Text needs parsing": { + LexiconNames: []string{"abc", "mno"}, + OutputFormat: types.OutputFormatMp3, + SampleRate: aws.String("128"), + Text: aws.String("Test /Text"), + TextType: types.TextTypeText, + VoiceID: types.VoiceIdAmy, + ExpectStream: "LexiconNames=abc&LexiconNames=mno&OutputFormat=mp3&SampleRate=128&Text=Test+%2FText&TextType=text&VoiceId=Amy", + }, + "Next serializer return error": { + Error: fmt.Errorf("next handler return error"), + ExpectError: true, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + req := smithyhttp.NewStackRequest().(*smithyhttp.Request) + var updatedRequest *smithyhttp.Request + param := &SynthesizeSpeechInput{ + LexiconNames: c.LexiconNames, + OutputFormat: c.OutputFormat, + SampleRate: c.SampleRate, + Text: c.Text, + TextType: c.TextType, + VoiceId: c.VoiceID, + } + + m := presignOpSynthesizeSpeechInput{} + _, _, err := m.HandleSerialize(context.Background(), + middleware.SerializeInput{ + Request: req, + Parameters: param, + }, + middleware.SerializeHandlerFunc(func(ctx context.Context, input middleware.SerializeInput) ( + out middleware.SerializeOutput, metadata middleware.Metadata, err error) { + updatedRequest = input.Request.(*smithyhttp.Request) + return out, metadata, c.Error + }), + ) + + if err != nil && !c.ExpectError { + t.Fatalf("expect no error, got %v", err) + } else if err != nil != c.ExpectError { + t.Fatalf("expect error but got nil") + } + + stream := updatedRequest.GetStream() + b, _ := ioutil.ReadAll(stream) + if e, a := c.ExpectStream, string(b); e != a { + t.Errorf("expect request stream value %v, got %v", e, a) + } + }) + } +}