diff --git a/build_tools/aws-sdk-code-generator/spec/interfaces/client/jsonvalue_converter_spec.rb b/build_tools/aws-sdk-code-generator/spec/interfaces/client/jsonvalue_converter_spec.rb deleted file mode 100644 index 99e5e13b960..00000000000 --- a/build_tools/aws-sdk-code-generator/spec/interfaces/client/jsonvalue_converter_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -require_relative '../../spec_helper' - -describe 'Client Interface:' do - describe 'Jsonvalue Converter' do - - before(:all) do - SpecHelper.generate_service(['JsonvalueConverter'], multiple_files: false) - end - - let(:client) { - JsonvalueConverter::Client.new( - region: 'us-west-2', - access_key_id: 'akid', - secret_access_key: 'secret', - stub_responses: true, - endpoint: 'http://svc.us-west-2.amazonaws.com' - ) - } - - it 'converts ruby object to json string before validation' do - hash = {'bar' => 'baz'} - array = ['bar', 'baz'] - num = 12345 - str = 'foobarbaz' - bool = true - time = Time.now - resp = client.foo( - foo_hash: hash, - foo_array: array, - foo_num: num, - foo_str: str, - foo_bool: bool, - foo_time: time - ) - expect(resp.context.params[:foo_hash]).to eql(hash.to_json) - expect(resp.context.params[:foo_array]).to eql(array.to_json) - expect(resp.context.params[:foo_num]).to eql(num.to_json) - expect(resp.context.params[:foo_str]).to eql(str.to_json) - expect(resp.context.params[:foo_bool]).to eql(bool.to_json) - expect(resp.context.params[:foo_time]).to eql(time.to_json) - end - - it 'raises exception when an object is not JSON serializable' do - hash = {'bar' => 'baz'} - hash.instance_eval('undef :to_json') - expect { - client.foo(foo_hash: hash) - }.to raise_error(ArgumentError) - end - end -end diff --git a/build_tools/aws-sdk-code-generator/spec/protocols/input/rest-json.json b/build_tools/aws-sdk-code-generator/spec/protocols/input/rest-json.json index 8c914c2b321..7ee8a6a246d 100644 --- a/build_tools/aws-sdk-code-generator/spec/protocols/input/rest-json.json +++ b/build_tools/aws-sdk-code-generator/spec/protocols/input/rest-json.json @@ -1229,18 +1229,60 @@ }, "shapes": { "InputShape": { - "type":"structure", + "type": "structure", + "payload": "Body", "members": { - "Attr": { + "HeaderField": { "shape": "StringType", "jsonvalue": true, "location": "header", "locationName": "X-Amz-Foo" + }, + "QueryField": { + "shape": "StringType", + "jsonvalue": true, + "location": "querystring", + "locationName": "Bar" + }, + "Body": { + "shape": "BodyStructure" } } }, "StringType": { "type": "string" + }, + "ListType": { + "type": "list", + "member": { + "shape": "StringType", + "jsonvalue": true + } + }, + "MapType": { + "type": "map", + "key": { + "shape": "StringType" + }, + "value": { + "shape": "StringType", + "jsonvalue": true + } + }, + "BodyStructure": { + "type": "structure", + "members": { + "BodyField": { + "shape": "StringType", + "jsonvalue": true + }, + "BodyListField": { + "shape": "ListType" + }, + "BodyMapField": { + "shape": "MapType" + } + } } }, "cases": [ @@ -1256,12 +1298,43 @@ "name": "OperationName" }, "params": { - "Attr": {"Foo":"Bar"} + "HeaderField": {"Foo":"Bar"}, + "QueryField": {"Foo":"Bar"}, + "Body": { + "BodyField": {"Foo":"Bar"} + } + }, + "serialized": { + "uri": "/?Bar=%7B%22Foo%22%3A%22Bar%22%7D", + "headers": { + "X-Amz-Foo": "eyJGb28iOiJCYXIifQ==", + "Content-Type": "application/json" + }, + "body": "{\"BodyField\":\"{\\\"Foo\\\":\\\"Bar\\\"}\"}" + } + }, + { + "given": { + "input": { + "shape": "InputShape" + }, + "http": { + "method": "POST", + "requestUri": "/" + }, + "name": "OperationName" + }, + "params": { + "Body": { + "BodyListField": [{"Foo":"Bar"}] + } }, "serialized": { "uri": "/", - "headers": {"X-Amz-Foo": "eyJGb28iOiJCYXIifQ=="}, - "body": "" + "headers": { + "Content-Type": "application/json" + }, + "body": "{\"BodyListField\":[\"{\\\"Foo\\\":\\\"Bar\\\"}\"]}" } }, { @@ -1276,11 +1349,37 @@ "name": "OperationName" }, "params": { + "Body": { + "BodyMapField": {"FooBar": {"Foo":"Bar"}, "BarFoo": {"Bar": "Foo"}} + } }, "serialized": { "uri": "/", - "headers": {}, - "body": "" + "headers": { + "Content-Type": "application/json" + }, + "body": "{\"BodyMapField\": {\"FooBar\": \"{\\\"Foo\\\":\\\"Bar\\\"}\", \"BarFoo\": \"{\\\"Bar\\\":\\\"Foo\\\"}\"}}" + } + }, + { + "given": { + "input": { + "shape": "InputShape" + }, + "http": { + "method": "POST", + "requestUri": "/" + }, + "name": "OperationName" + }, + "params": { + }, + "serialized": { + "uri": "/", + "headers": { + "Content-Type": "application/json" + }, + "body": "{}" } } ] diff --git a/gems/aws-sdk-core/CHANGELOG.md b/gems/aws-sdk-core/CHANGELOG.md index 5db240b9d30..f6dd491ede7 100644 --- a/gems/aws-sdk-core/CHANGELOG.md +++ b/gems/aws-sdk-core/CHANGELOG.md @@ -1,6 +1,8 @@ Unreleased Changes ------------------ +* Issue - Add support for serializing shapes on the body with `jsonvalue` members. + 3.131.2 (2022-06-20) ------------------ diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/plugins/jsonvalue_converter.rb b/gems/aws-sdk-core/lib/aws-sdk-core/plugins/jsonvalue_converter.rb index f07fc9581ee..d82150570c0 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/plugins/jsonvalue_converter.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/plugins/jsonvalue_converter.rb @@ -11,15 +11,43 @@ class Handler < Seahorse::Client::Handler def call(context) context.operation.input.shape.members.each do |m, ref| - if ref['jsonvalue'] - param_value = context.params[m] - unless param_value.respond_to?(:to_json) - raise ArgumentError, "The value of params[#{m}] is not JSON serializable." + convert_jsonvalue(m, ref, context.params, 'params') + end + @handler.call(context) + end + + def convert_jsonvalue(m, ref, params, context) + return if params.nil? || !params.key?(m) + + if ref['jsonvalue'] + params[m] = serialize_jsonvalue(params[m], "#{context}[#{m}]") + else + case ref.shape + when Seahorse::Model::Shapes::StructureShape + ref.shape.members.each do |member_m, ref| + convert_jsonvalue(member_m, ref, params[m], "#{context}[#{m}]") + end + when Seahorse::Model::Shapes::ListShape + if ref.shape.member['jsonvalue'] + params[m] = params[m].each_with_index.map do |v, i| + serialize_jsonvalue(v, "#{context}[#{m}][#{i}]") + end + end + when Seahorse::Model::Shapes::MapShape + if ref.shape.value['jsonvalue'] + params[m].each do |k, v| + params[m][k] = serialize_jsonvalue(v, "#{context}[#{m}][#{k}]") + end end - context.params[m] = param_value.to_json end end - @handler.call(context) + end + + def serialize_jsonvalue(v, context) + unless v.respond_to?(:to_json) + raise ArgumentError, "The value of #{context} is not JSON serializable." + end + v.to_json end end diff --git a/gems/aws-sdk-core/spec/aws/plugins/jsonvalue_converter_spec.rb b/gems/aws-sdk-core/spec/aws/plugins/jsonvalue_converter_spec.rb new file mode 100644 index 00000000000..d84bdd0a979 --- /dev/null +++ b/gems/aws-sdk-core/spec/aws/plugins/jsonvalue_converter_spec.rb @@ -0,0 +1,212 @@ +# frozen_string_literal: true + +require_relative '../../spec_helper' + +module Aws + module Plugins + describe JsonvalueConverter do + JsonValueClient = ApiHelper.sample_service(api: JSON.parse(<<~JSON + { + "version":"2.0", + "metadata":{ + "endpointPrefix":"svc", + "protocol": "rest-json", + "signatureVersion":"v4" + }, + "operations":{ + "Foo" : { + "name" : "Foo", + "http" : { + "method" : "POST", + "requestUri" : "/" + }, + "input" : { + "shape" : "FooRequest" + } + } + }, + "shapes": { + "FooRequest": { + "type": "structure", + "payload": "Body", + "members": { + "HeaderField": { + "shape": "StringType", + "jsonvalue": true, + "location": "header", + "locationName": "X-Amz-Foo" + }, + "QueryField": { + "shape": "StringType", + "jsonvalue": true, + "location": "querystring", + "locationName": "Bar" + }, + "Body": { + "shape": "BodyStructure" + } + } + }, + "StringType": { + "type": "string" + }, + "ListType": { + "type": "list", + "member": { + "shape": "StringType", + "jsonvalue": true + } + }, + "MapType": { + "type": "map", + "key": { + "shape": "StringType" + }, + "value": { + "shape": "StringType", + "jsonvalue": true + } + }, + "BodyStructure": { + "type": "structure", + "members": { + "BodyField": { + "shape": "StringType", + "jsonvalue": true + }, + "BodyStructureField": { + "shape": "BodyStructure" + }, + "BodyListField": { + "shape": "ListType" + }, + "BodyMapField": { + "shape": "MapType" + } + } + } + } + } + JSON + )).const_get(:Client) + + let(:client) { JsonValueClient.new(stub_responses: true) } + let(:hash) { { 'bar' => 'baz' } } + let(:hash_no_json) do + hash = { 'bar' => 'baz' } + hash.instance_eval('undef :to_json', __FILE__, __LINE__) + hash + end + + it 'converts ruby object to json string before validation' do + ruby_types = { + hash: { 'bar' => 'baz' }, + array: %w[bar baz], + num: 12_345, + str: 'foobarbaz', + bool: true, + time: Time.now + } + ruby_types.each do |_k, v| + resp = client.foo(header_field: v) + expect(resp.context.params[:header_field]).to eq(v.to_json) + end + end + + it 'does not serialize unset jsonvalue members' do + resp = client.foo(body: {}) + expect(resp.context.params[:body]).to eq({}) + end + + it 'serializes jsonvalue members set to nil' do + resp = client.foo(body: { body_field: nil }) + expect(resp.context.params[:body]).to eq({ body_field: 'null' }) + end + + it 'serializes jsonvalue members of nested structures' do + resp = client.foo( + body: { + body_structure_field: { + body_field: hash + } + } + ) + expect(resp.context.params[:body][:body_structure_field][:body_field]) + .to eq(hash.to_json) + end + + it 'serializes jsonvalue members of lists' do + resp = client.foo( + body: { + body_list_field: [hash, hash] + } + ) + expect(resp.context.params[:body][:body_list_field]) + .to eq([hash.to_json, hash.to_json]) + end + + it 'serializes jsonvalue values of maps' do + resp = client.foo( + body: { + body_map_field: { key1: hash, key2: hash } + } + ) + expect(resp.context.params[:body][:body_map_field]) + .to eq({ 'key1' => hash.to_json, 'key2' => hash.to_json }) + end + + it 'raises exception when an object is not JSON serializable' do + expect do + client.foo(header_field: hash_no_json) + end.to raise_error( + ArgumentError, + 'The value of params[header_field] is not JSON serializable.' + ) + end + + it 'raises exceptions when structure member is not JSON serializable' do + expect do + client.foo( + body: { + body_structure_field: { + body_field: hash_no_json + } + } + ) + end.to raise_error( + ArgumentError, + 'The value of params[body][body_structure_field][body_field] '\ + 'is not JSON serializable.' + ) + end + + it 'raises exceptions when a list member is not JSON serializable' do + expect do + client.foo( + body: { + body_list_field: [hash, hash_no_json] + } + ) + end.to raise_error( + ArgumentError, + 'The value of params[body][body_list_field][1] '\ + 'is not JSON serializable.' + ) + end + + it 'raises exceptions when a map value is not JSON serializable' do + expect do + client.foo( + body: { + body_map_field: { key1: hash, key2: hash_no_json } + } + ) + end.to raise_error( + ArgumentError, + 'The value of params[body][body_map_field][key2] '\ + 'is not JSON serializable.' + ) + end + end + end +end diff --git a/gems/aws-sdk-s3/spec/presigner_spec.rb b/gems/aws-sdk-s3/spec/presigner_spec.rb index bd13789d54e..fdd6fb8352f 100644 --- a/gems/aws-sdk-s3/spec/presigner_spec.rb +++ b/gems/aws-sdk-s3/spec/presigner_spec.rb @@ -90,7 +90,6 @@ module S3 expires_in: 86_400, whitelist_headers: ['user-agent'] ) - puts CGI.parse(actual_url) expect(actual_url).to include( '&X-Amz-SignedHeaders=host%3Buser-agent' )