diff --git a/README.md b/README.md index 92dc2a0d4..154e3751e 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,7 @@ bundle exec smithy build --debug local build using smithy-ruby executable: ``` -export SMITHY_PLUGIN_DIR=build/smithy/source/smithy-ruby -bundle exec smithy-ruby smith client --gem-name weather --gem-version 1.0.0 --destination-root projections/weather <<< $(smithy ast model/weather.smithy) +SMITHY_PLUGIN_DIR=build/smithy/source/smithy-ruby bundle exec smithy-ruby smith client --gem-name weather --gem-version 1.0.0 --destination-root projections/weather <<< $(smithy ast model/weather.smithy) ``` ### IRB diff --git a/gems/smithy-cbor/lib/smithy-cbor/deserializer.rb b/gems/smithy-cbor/lib/smithy-cbor/deserializer.rb index 614e42872..9a1c5a76e 100644 --- a/gems/smithy-cbor/lib/smithy-cbor/deserializer.rb +++ b/gems/smithy-cbor/lib/smithy-cbor/deserializer.rb @@ -38,7 +38,7 @@ def list(ref, values, target = nil) values.each do |value| next if value.nil? && !sparse?(ref.shape) - target << (value.nil? ? nil : shape(ref.shape.member, value)) + target << shape(ref.shape.member, value) end target end @@ -48,37 +48,31 @@ def map(ref, values, target = nil) values.each do |key, value| next if value.nil? && !sparse?(ref.shape) - target[key] = value.nil? ? nil : shape(ref.shape.value, value) + target[key] = shape(ref.shape.value, value) end target end def structure(ref, values, target = nil) - return Schema::EmptyStructure.new if ref.shape == Prelude::Unit - target = ref.shape.type.new if target.nil? ref.shape.members.each do |member_name, member_ref| - key = member_ref.member_name - next unless values.key?(key) - - target[member_name] = shape(member_ref, values[key]) + value = values[member_ref.member_name] + target[member_name] = shape(member_ref, value) unless value.nil? end target end def union(ref, values, target = nil) # rubocop:disable Metrics/AbcSize - raise ArgumentError, "union value includes more than one key, received: #{values.keys}" if values.size > 1 - - key, value = values.first - return nil if key.nil? - ref.shape.members.each do |member_name, member_ref| - name = member_ref.member_name - next unless values.key?(name) + value = values[member_ref.member_name] + next if value.nil? target = ref.shape.member_type(member_name) if target.nil? - return target.new(shape(member_ref, values[name])) + return target.new(shape(member_ref, value)) end + + values.delete('__type') + key, value = values.first ref.shape.member_type(:unknown).new(key, value) end diff --git a/gems/smithy-cbor/lib/smithy-cbor/serializer.rb b/gems/smithy-cbor/lib/smithy-cbor/serializer.rb index 464387f53..c0d09f34d 100644 --- a/gems/smithy-cbor/lib/smithy-cbor/serializer.rb +++ b/gems/smithy-cbor/lib/smithy-cbor/serializer.rb @@ -14,7 +14,7 @@ def initialize(options = {}) def serialize(shape, data) ref = shape.is_a?(ShapeRef) ? shape : ShapeRef.new(shape: shape) - return nil if ref.shape == Prelude::Unit + return if ref.shape == Prelude::Unit CBOR.encode(shape(ref, data)) end @@ -33,38 +33,40 @@ def shape(ref, value) end def blob(value) - value.is_a?(String) ? value : value.read + value.respond_to?(:read) ? value.read : value end def list(ref, values) + shape = ref.shape values.collect do |value| - next if value.nil? && !sparse?(ref.shape) + next if value.nil? && !sparse?(shape.traits) - value.nil? ? nil : shape(ref.shape.member, value) + value.nil? ? nil : shape(shape.member, value) end end def map(ref, values) + shape = ref.shape values.each.with_object({}) do |(key, value), data| - next if value.nil? && !sparse?(ref.shape) + next if value.nil? && !sparse?(shape.traits) - data[key] = value.nil? ? nil : shape(ref.shape.value, value) + data[key] = value.nil? ? nil : shape(shape.value, value) end end def structure(ref, values) - values.each_pair.with_object({}) do |(key, value), data| - if ref.shape.member?(key) && !value.nil? - member_ref = ref.shape.member(key) - data[member_ref.member_name] = shape(member_ref, value) - end + ref.shape.members.each_with_object({}) do |(member_name, member_ref), data| + value = values[member_name] + next if value.nil? + + data[member_ref.member_name] = shape(member_ref, value) end end def union(ref, values) # rubocop:disable Metrics/AbcSize data = {} if values.is_a?(Schema::Union) - member_ref = ref.shape.member_by_type(values.class) + _name, member_ref = ref.shape.member_by_type(values.class) data[member_ref.member_name] = shape(member_ref, values).value else key, value = values.first @@ -76,8 +78,8 @@ def union(ref, values) # rubocop:disable Metrics/AbcSize data end - def sparse?(shape) - shape.traits.include?('smithy.api#sparse') + def sparse?(traits) + traits.include?('smithy.api#sparse') end end end diff --git a/gems/smithy-cbor/spec/smithy-cbor/codec_spec.rb b/gems/smithy-cbor/spec/smithy-cbor/codec_spec.rb index 29f84fe8e..e859a9f1f 100644 --- a/gems/smithy-cbor/spec/smithy-cbor/codec_spec.rb +++ b/gems/smithy-cbor/spec/smithy-cbor/codec_spec.rb @@ -4,6 +4,7 @@ module Smithy module CBOR + # TODO: test all codec cases describe Codec do let(:shape) { SchemaHelper.sample_schema.const_get(:Structure) } @@ -83,18 +84,6 @@ module CBOR expect(subject.deserialize(shape, bytes).union).to eq(nil) end - it 'serializes an empty union' do - data = { union: {} } - bytes = subject.serialize(shape, data) - expect(subject.deserialize(shape, bytes).union).to eq(nil) - end - - it 'serializes nil union values' do - data = { union: { string: nil } } - bytes = subject.serialize(shape, data) - expect(subject.deserialize(shape, bytes).to_h).to eq(data) - end - it 'deserializes unknown union members' do unknown_union_type = shape.member(:union).shape.member_type(:unknown) data = { union: { 'someThing' => 'someValue' } } @@ -102,12 +91,6 @@ module CBOR expect(deserialized.union).to be_a(unknown_union_type) expect(deserialized.union.to_h).to eq(unknown: { name: 'someThing', value: 'someValue' }) end - - it 'raises when deserializing unions with more than one member' do - data = { union: { string: 'string', integer: 1 } } - expect { subject.deserialize(shape, CBOR.encode(data)) } - .to raise_error(ArgumentError, /union value includes more than one key/) - end end context 'lists' do diff --git a/gems/smithy-client/lib/smithy-client.rb b/gems/smithy-client/lib/smithy-client.rb index a92a23ab4..c1ad06714 100644 --- a/gems/smithy-client/lib/smithy-client.rb +++ b/gems/smithy-client/lib/smithy-client.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'base64' +require 'bigdecimal' require 'jmespath' require 'smithy-cbor' diff --git a/gems/smithy-client/lib/smithy-client/handler_context.rb b/gems/smithy-client/lib/smithy-client/handler_context.rb index de829c6d2..022fef3cb 100644 --- a/gems/smithy-client/lib/smithy-client/handler_context.rb +++ b/gems/smithy-client/lib/smithy-client/handler_context.rb @@ -7,7 +7,7 @@ class HandlerContext # @option options [Symbol] :operation_name (nil) # @option options [OperationShape] :operation (nil) # @option options [Base] :client (nil) - # @option options [Hash] :params ({}) + # @option options [Hash, Struct] :params ({}) # @option options [Configuration] :config (nil) # @option options [Request] :request (HTTP::Request.new) # @option options [Response] :response (HTTP::Response.new) @@ -33,7 +33,7 @@ def initialize(options = {}) # @return [Base] attr_accessor :client - # @return [Hash] The hash of request parameters. + # @return [Hash, Struct] The request parameters as a Hash or a Struct. attr_accessor :params # @return [Struct] The client configuration. diff --git a/gems/smithy-client/lib/smithy-client/param_converter.rb b/gems/smithy-client/lib/smithy-client/param_converter.rb index 9cebb71ed..c2615f208 100644 --- a/gems/smithy-client/lib/smithy-client/param_converter.rb +++ b/gems/smithy-client/lib/smithy-client/param_converter.rb @@ -15,8 +15,9 @@ class ParamConverter @mutex = Mutex.new @converters = Hash.new { |h, k| h[k] = {} } - def initialize(schema) + def initialize(schema, convert_structures: true) @schema = schema + @convert_structures = convert_structures @opened_files = [] end @@ -39,67 +40,67 @@ def c(ref, value) self.class.c(ref.shape.class, value, self) end + def shape(ref, value) + case ref.shape + when ListShape then list(ref, value) + when MapShape then map(ref, value) + when StructureShape then structure(ref, value) + when UnionShape then union(ref, value) + else c(ref, value) + end + end + def list(ref, values) values = c(ref, values) - if values.is_a?(Array) - values.map { |v| member(ref.shape.member, v) } - else - values - end + return values unless values.is_a?(Array) + + values.collect { |v| shape(ref.shape.member, v) } end def map(ref, values) values = c(ref, values) - if values.is_a?(Hash) - values.each.with_object({}) do |(key, value), hash| - hash[member(ref.shape.key, key)] = member(ref.shape.value, value) - end - else - values - end - end + return values unless values.is_a?(Hash) - def member(ref, value) - case ref.shape - when StructureShape then structure(ref, value) - when UnionShape then union(ref, value) - when ListShape then list(ref, value) - when MapShape then map(ref, value) - else c(ref, value) + values.each.with_object({}) do |(key, value), hash| + hash[shape(ref.shape.key, key)] = shape(ref.shape.value, value) end end def structure(ref, values) values = c(ref, values) - if values.respond_to?(:each_pair) - values.each_pair do |k, v| - next if v.nil? + return if values.nil? - next unless ref.shape.member?(k) + type = @convert_structures ? ref.shape.type.new : values + return type unless values.respond_to?(:each_pair) - values[k] = member(ref.shape.member(k), v) - end + values.each_pair do |k, v| + next if v.nil? + next unless ref.shape.member?(k) + + type[k] = shape(ref.shape.member(k), v) end - values + type end - def union(ref, values) + def union(ref, values) # rubocop:disable Metrics/AbcSize values = c(ref, values) + return if values.nil? + if values.is_a?(Schema::Union) - member_ref = ref.shape.member_by_type(values.class) - member(member_ref, values) + name, member_ref = ref.shape.member_by_type(values.class) + member_type = ref.shape.member_type(name) + member_type.new(shape(member_ref, values.value)) else key, value = values.first - values[key] = member(ref.shape.member(key), value) + return { key => shape(ref.shape.member(key), value) } unless @convert_structures + return unless ref.shape.member?(key) + + member_type = ref.shape.member_type(key) + member_type.new(shape(ref.shape.member(key), value)) end - values end class << self - def convert(shape, params) - new(shape).convert(params) - end - # Registers a new value converter. Converters run in the context # of a shape and value class. # @@ -245,7 +246,6 @@ def each_base_class(shape_class, &) end add(UnionShape, Hash) { |h, _| h.dup } - add(UnionShape, Schema::Union) end end end diff --git a/gems/smithy-client/lib/smithy-client/param_validator.rb b/gems/smithy-client/lib/smithy-client/param_validator.rb index 705abc2a7..93b8754a4 100644 --- a/gems/smithy-client/lib/smithy-client/param_validator.rb +++ b/gems/smithy-client/lib/smithy-client/param_validator.rb @@ -148,7 +148,7 @@ def union(ref, values, errors, context) return unless valid_union?(ref, values, errors, context) if values.is_a?(Schema::Union) - member_ref = ref.shape.member_by_type(values.class) + _name, member_ref = ref.shape.member_by_type(values.class) shape(member_ref, values.value, errors, context) else values.each_pair do |name, value| @@ -176,7 +176,8 @@ def valid_union?(ref, values, errors, context) def validate_required_members(ref, values, errors, context) ref.shape.members.each do |name, member_ref| - next unless member_ref.traits.include?('smithy.api#required') + traits = member_ref.traits + next unless traits.include?('smithy.api#required') && !traits.include?('smithy.api#clientOptional') if values[name].nil? param = "#{context}[#{name.inspect}]" diff --git a/gems/smithy-client/lib/smithy-client/rpc_v2_cbor/protocol.rb b/gems/smithy-client/lib/smithy-client/rpc_v2_cbor/protocol.rb index 64e26d098..9857a72d9 100644 --- a/gems/smithy-client/lib/smithy-client/rpc_v2_cbor/protocol.rb +++ b/gems/smithy-client/lib/smithy-client/rpc_v2_cbor/protocol.rb @@ -29,8 +29,8 @@ def stub_data(service, operation, data) ResponseStubber.new(@options).stub_data(service, operation, data) end - def stub_error(error_code) - ResponseStubber.new(@options).stub_error(error_code) + def stub_error(service, error_code) + ResponseStubber.new(@options).stub_error(service, error_code) end end end diff --git a/gems/smithy-client/lib/smithy-client/rpc_v2_cbor/response_stubber.rb b/gems/smithy-client/lib/smithy-client/rpc_v2_cbor/response_stubber.rb index 0a978d4c4..64a2562d4 100644 --- a/gems/smithy-client/lib/smithy-client/rpc_v2_cbor/response_stubber.rb +++ b/gems/smithy-client/lib/smithy-client/rpc_v2_cbor/response_stubber.rb @@ -18,7 +18,7 @@ def stub_data(_service, operation, data) resp end - def stub_error(error_code) + def stub_error(_service, error_code) resp = HTTP::Response.new resp.status_code = 400 resp.headers['Smithy-Protocol'] = 'rpc-v2-cbor' diff --git a/gems/smithy-client/lib/smithy-client/signers/http_basic.rb b/gems/smithy-client/lib/smithy-client/signers/http_basic.rb index 8887ad973..475e76b29 100644 --- a/gems/smithy-client/lib/smithy-client/signers/http_basic.rb +++ b/gems/smithy-client/lib/smithy-client/signers/http_basic.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'base64' + module Smithy module Client module Signers diff --git a/gems/smithy-client/lib/smithy-client/stubbing/data_applicator.rb b/gems/smithy-client/lib/smithy-client/stubbing/data_applicator.rb index 51108fbdc..b8cacfc35 100644 --- a/gems/smithy-client/lib/smithy-client/stubbing/data_applicator.rb +++ b/gems/smithy-client/lib/smithy-client/stubbing/data_applicator.rb @@ -18,30 +18,40 @@ def apply(data, stub) private + def shape(ref, value, stub = nil) + case ref.shape + when StructureShape then structure(ref, value, stub) + when ListShape then list(ref, value) + when MapShape then map(ref, value) + else value + end + end + def list(ref, value) + return if value.nil? + + shape = ref.shape value.each_with_object([]) do |v, list| - list << member(ref.shape.member, v) + list << shape(shape.member, v) end end def map(ref, value) - value.each_with_object({}) do |(k, v), map| - map[k.to_s] = member(ref.shape.value, v) - end - end + return if value.nil? - def member(ref, value) - case ref.shape - when StructureShape then structure(ref.shape, value, ref.shape.type.new) - when ListShape then list(ref, value) - when MapShape then map(ref, value) - else value + shape = ref.shape + value.each_with_object({}) do |(k, v), map| + map[k.to_s] = shape(shape.value, v) end end def structure(ref, data, stub) - data.each do |key, value| - stub[key] = member(ref.shape.member(key), value) + return if data.nil? + + stub = ref.shape.type.new if stub.nil? + shape = ref.shape + data.each_pair do |key, value| + stub[key] = shape(shape.member(key), value) end stub end diff --git a/gems/smithy-client/lib/smithy-client/stubbing/empty_stub.rb b/gems/smithy-client/lib/smithy-client/stubbing/empty_stub.rb index adce582ad..170cdbafe 100644 --- a/gems/smithy-client/lib/smithy-client/stubbing/empty_stub.rb +++ b/gems/smithy-client/lib/smithy-client/stubbing/empty_stub.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'bigdecimal' + module Smithy module Client module Stubbing @@ -20,11 +22,12 @@ def stub private def shape(ref, visited) - return nil if visited.include?(ref.shape) + shape = ref.shape + return nil if visited.include?(shape) - visited += [ref.shape] + visited += [shape] - case ref.shape + case shape when ListShape then [] when MapShape then {} when StructureShape then structure(ref, visited) @@ -34,24 +37,23 @@ def shape(ref, visited) end def structure(ref, visited) - return Schema::EmptyStructure.new if ref.shape == Prelude::Unit - - ref.shape.members.each_with_object(ref.shape.type.new) do |(member_name, member_ref), struct| + shape = ref.shape + shape.members.each_with_object(shape.type.new) do |(member_name, member_ref), struct| struct[member_name] = shape(member_ref, visited) end end def union(ref, visited) - member_name, member_ref = ref.shape.members.first + shape = ref.shape + member_name, member_ref = shape.members.first return unless member_name value = shape(member_ref, visited) - klass = ref.shape.member_type(member_name) + klass = shape.member_type(member_name) klass.new(value) end - # rubocop:disable Metrics/CyclomaticComplexity - def scalar(ref) + def scalar(ref) # rubocop:disable Metrics/CyclomaticComplexity case ref.shape when BigDecimalShape then BigDecimal(0) when BlobShape then 'blob' @@ -63,7 +65,6 @@ def scalar(ref) when TimestampShape then Time.now end end - # rubocop:enable Metrics/CyclomaticComplexity end end end diff --git a/gems/smithy-client/lib/smithy-client/stubbing/protocol.rb b/gems/smithy-client/lib/smithy-client/stubbing/protocol.rb index 50a7cb295..f679257d4 100644 --- a/gems/smithy-client/lib/smithy-client/stubbing/protocol.rb +++ b/gems/smithy-client/lib/smithy-client/stubbing/protocol.rb @@ -17,7 +17,7 @@ def stub_data(_service, _operation, data) resp end - def stub_error(error_code) + def stub_error(_service, error_code) resp = HTTP::Response.new resp.status_code = 500 resp.body = StringIO.new(error_code.to_json) diff --git a/gems/smithy-client/lib/smithy-client/stubbing/stub_data.rb b/gems/smithy-client/lib/smithy-client/stubbing/stub_data.rb index ef97e5eea..e532b43ea 100644 --- a/gems/smithy-client/lib/smithy-client/stubbing/stub_data.rb +++ b/gems/smithy-client/lib/smithy-client/stubbing/stub_data.rb @@ -10,20 +10,14 @@ def initialize(operation) @schema = operation.output end - # @param [Hash] data + # @param [Hash] params # @return [Structure] - def stub(data = {}) + def stub(params = {}) stub = EmptyStub.new(@schema).stub - apply_data(data, stub) - stub - end - - private - - def apply_data(data, stub) - data = ParamConverter.new(@schema).convert(data) + data = ParamConverter.new(@schema, convert_structures: false).convert(params) ParamValidator.new(@schema, validate_required: false).validate!(data, context: 'stub') DataApplicator.new(@schema).apply(data, stub) + stub end end end diff --git a/gems/smithy-client/lib/smithy-client/stubs.rb b/gems/smithy-client/lib/smithy-client/stubs.rb index 13adab92e..8cd72be4f 100644 --- a/gems/smithy-client/lib/smithy-client/stubs.rb +++ b/gems/smithy-client/lib/smithy-client/stubs.rb @@ -182,7 +182,7 @@ def convert_stub(operation_name, stub, context) end def service_error_stub(error_code) - { http: @config.protocol.stub_error(error_code) } + { http: @config.protocol.stub_error(@config.service, error_code) } end def http_response_stub(operation_name, data) @@ -203,8 +203,8 @@ def hash_to_http_resp(data) def data_to_http_resp(operation_name, data) operation = @config.service.operation(operation_name) - data = ParamConverter.new(operation.output).convert(data) - ParamValidator.new(operation.output, validate_required: false).validate!(data) + data = ParamConverter.new(operation.output, convert_structures: false).convert(data) + ParamValidator.new(operation.output, validate_required: false).validate!(data, context: 'stub') @config.protocol.stub_data(@config.service, operation, data) end end diff --git a/gems/smithy-client/sig/smithy-client/handler_context.rbs b/gems/smithy-client/sig/smithy-client/handler_context.rbs index e7e519f44..6156706c6 100644 --- a/gems/smithy-client/sig/smithy-client/handler_context.rbs +++ b/gems/smithy-client/sig/smithy-client/handler_context.rbs @@ -5,7 +5,7 @@ module Smithy attr_accessor operation_name: (Symbol | String)? attr_accessor operation: Schema::Shapes::OperationShape? attr_accessor client: Base? - attr_accessor params: Hash[Symbol | String, untyped] + attr_accessor params: (Hash[Symbol | String, untyped] | Schema::Structure) attr_accessor config: Struct[untyped]? attr_accessor request: HTTP::Request | untyped attr_accessor response: HTTP::Response | untyped diff --git a/gems/smithy-client/smithy-client.gemspec b/gems/smithy-client/smithy-client.gemspec index 2d64d12f2..790069df9 100644 --- a/gems/smithy-client/smithy-client.gemspec +++ b/gems/smithy-client/smithy-client.gemspec @@ -10,6 +10,7 @@ Gem::Specification.new do |spec| spec.license = 'Apache-2.0' spec.files = Dir['CHANGELOG.md', 'VERSION', 'lib/**/*'] + spec.add_dependency('base64') spec.add_dependency('jmespath', '~> 1', '>= 1.6.1') # necessary for secure jmespath JSON parsing spec.add_dependency('smithy-cbor', '1.0.0.pre0') diff --git a/gems/smithy-client/spec/smithy-client/param_converter_spec.rb b/gems/smithy-client/spec/smithy-client/param_converter_spec.rb index df343b8c4..54a4ef2b2 100644 --- a/gems/smithy-client/spec/smithy-client/param_converter_spec.rb +++ b/gems/smithy-client/spec/smithy-client/param_converter_spec.rb @@ -9,13 +9,47 @@ module Smithy module Client describe ParamConverter do describe '#convert' do - it 'performs a deeply nested conversion of values' do - client = ClientHelper.sample_client.const_get(:Client).new - service = client.config.service - input = service.operation(:operation).input + let(:client) { ClientHelper.sample_client.const_get(:Client).new } + let(:input) { client.config.service.operation(:operation).input } + let(:expected) do + { + structure: { boolean: true }, + map: 'not a map', + structure_map: { + 'key' => { map: { 'color' => 'blue' } } + }, + list: 'not a list', + structure_list: [ + { integer: 1 }, + { integer: 2 }, + { integer: 3 } + ], + union: { structure: { string: 'abc' } } + } + end + + it 'performs a deeply nested conversion of values when using hashes' do + params = { + structure: { boolean: 'true' }, + map: 'not a map', + structure_map: { + 'key' => { map: { color: :blue } } + }, + list: 'not a list', + structure_list: [ + { integer: 1 }, + { integer: 2.0 }, + { integer: '3' } + ], + union: { structure: { string: :abc } } + } + converted = ParamConverter.new(input, convert_structures: false).convert(params) + expect(converted).to eq(expected) + end + + it 'performs a deeply nested conversion of values when using types' do structure_type = input.shape.type union_type = input.shape.member(:union).shape.member_type(:structure) - params = structure_type.new( structure: structure_type.new(boolean: 'true'), map: 'not a map', @@ -30,22 +64,52 @@ module Client ], union: union_type.new({ string: :abc }) ) + converted = ParamConverter.new(input, convert_structures: false).convert(params) + expect(converted.to_h).to eq(expected) + end - converted = ParamConverter.convert(input, params) - expect(converted.to_h).to eq( - structure: { boolean: true }, + it 'performs a deeply nested conversion of hash values into types' do + params = { + structure: { boolean: 'true' }, map: 'not a map', structure_map: { - 'key' => { map: { 'color' => 'blue' } } + 'key' => { map: { color: :blue } } }, list: 'not a list', structure_list: [ { integer: 1 }, - { integer: 2 }, - { integer: 3 } + { integer: 2.0 }, + { integer: '3' } ], - union: { structure: { string: 'abc' } } + union: { structure: { string: :abc } } + } + converted = ParamConverter.new(input).convert(params) + expect(converted).to be_a(input.shape.type) + expect(converted.union).to be_a(input.shape.member(:union).shape.member_type(:structure)) + expect(converted.to_h).to eq(expected) + end + + it 'performs a deeply nested conversion of type values into types' do + structure_type = input.shape.type + union_type = input.shape.member(:union).shape.member_type(:structure) + params = structure_type.new( + structure: structure_type.new(boolean: 'true'), + map: 'not a map', + structure_map: { + 'key' => structure_type.new(map: { color: :blue }) + }, + list: 'not a list', + structure_list: [ + { integer: 1 }, + { integer: 2.0 }, + { integer: '3' } + ], + union: union_type.new({ string: :abc }) ) + converted = ParamConverter.new(input).convert(params) + expect(converted).to be_a(structure_type) + expect(converted.union).to be_a(union_type) + expect(converted.to_h).to eq(expected) end end diff --git a/gems/smithy-client/spec/smithy-client/param_validator_spec.rb b/gems/smithy-client/spec/smithy-client/param_validator_spec.rb index a605145d6..ad160956a 100644 --- a/gems/smithy-client/spec/smithy-client/param_validator_spec.rb +++ b/gems/smithy-client/spec/smithy-client/param_validator_spec.rb @@ -271,7 +271,7 @@ def match_errors(error, expected_errors) end it 'accepts a modeled type' do - structure = sample_client.const_get(:Types).const_get(:Structure).new({}) + structure = sample_client.const_get(:Types).const_get(:Structure).new validate({ structure: structure }) end end diff --git a/gems/smithy-client/spec/smithy-client/plugins/stub_responses_spec.rb b/gems/smithy-client/spec/smithy-client/plugins/stub_responses_spec.rb index cec8ef7ac..a7e3c0dfa 100644 --- a/gems/smithy-client/spec/smithy-client/plugins/stub_responses_spec.rb +++ b/gems/smithy-client/spec/smithy-client/plugins/stub_responses_spec.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'bigdecimal' + require_relative '../../spec_helper' require 'smithy-client/plugins/stub_responses' @@ -13,6 +15,7 @@ module Plugins client_class = sample_client.const_get(:Client) client_class.clear_plugins client_class.add_plugin(sample_client::Plugins::Endpoint) + client_class.add_plugin(ParamConverter) client_class.add_plugin(Protocol) client_class.add_plugin(RaiseResponseErrors) client_class.add_plugin(StubResponses) @@ -103,7 +106,7 @@ module Plugins let(:now) { Time.now } let(:default_stub_data) do { - big_decimal: 0.0, + big_decimal: BigDecimal(0), big_integer: 0, blob: String.new('blob'), boolean: false, @@ -134,7 +137,14 @@ module Plugins it 'returns the correct type' do client.stub_responses(:operation) output = client.operation - expect(output.data).to be_a(sample_client::Types::Structure) + expect(output.data).to be_a(sample_client::Types::OperationOutput) + end + + it 'validates stubs at request time' do + data = { not_a_member: 'foo' } + client.stub_responses(:operation, data) + expect { client.operation } + .to raise_error(ArgumentError, /unexpected value at stub\[:not_a_member\]/) end it 'can stub default data' do @@ -144,13 +154,6 @@ module Plugins expect(output.data.structure.to_h).to include(default_stub_data) end - it 'validates stubs at request time' do - data = { not_a_member: 'foo' } - client.stub_responses(:operation, data) - expect { client.operation } - .to raise_error(ArgumentError, /unexpected value at params\[:not_a_member\]/) - end - it 'can stub procs' do client.stub_responses(:operation, ->(ctx) { { string: ctx.params[:string] } }) output = client.operation(string: 'new string') diff --git a/gems/smithy-client/spec/support/client_helper.rb b/gems/smithy-client/spec/support/client_helper.rb index 6d417a517..07c40682a 100644 --- a/gems/smithy-client/spec/support/client_helper.rb +++ b/gems/smithy-client/spec/support/client_helper.rb @@ -88,7 +88,10 @@ def sample_shapes 'short' => { 'target' => 'smithy.api#Short' }, 'streamingBlob' => { 'target' => 'smithy.ruby.tests#StreamingBlob', - 'traits' => { 'smithy.api#default' => 'streamingBlob' } + 'traits' => { + 'smithy.api#required' => {}, + 'smithy.api#clientOptional' => {} + } }, 'string' => { 'target' => 'smithy.api#String' }, 'structure' => { 'target' => 'smithy.ruby.tests#Structure' }, diff --git a/gems/smithy-json/lib/smithy-json/deserializer.rb b/gems/smithy-json/lib/smithy-json/deserializer.rb index 18f34cf31..d8c6aab2f 100644 --- a/gems/smithy-json/lib/smithy-json/deserializer.rb +++ b/gems/smithy-json/lib/smithy-json/deserializer.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'base64' + module Smithy module JSON # @api private @@ -7,7 +9,7 @@ class Deserializer include Smithy::Schema::Shapes def initialize(options = {}) - @options = options + @json_name = options[:json_name] || false end def deserialize(shape, bytes, target) @@ -22,7 +24,6 @@ def deserialize(shape, bytes, target) def shape(ref, value, target = nil) # rubocop:disable Metrics/CyclomaticComplexity case ref.shape when BlobShape then Base64.decode64(value) - when BooleanShape then value.to_s == 'true' when FloatShape then float(value) when ListShape then list(ref, value, target) when MapShape then map(ref, value, target) @@ -44,11 +45,13 @@ def float(value) end def list(ref, values, target = nil) + return if values.nil? + target = [] if target.nil? values.each do |value| next if value.nil? && !sparse?(ref.shape) - target << (value.nil? ? nil : shape(ref.shape.member, value)) + target << shape(ref.shape.member, value) end target end @@ -58,20 +61,18 @@ def map(ref, values, target = nil) values.each do |key, value| next if value.nil? && !sparse?(ref.shape) - target[key] = value.nil? ? nil : shape(ref.shape.value, value) + target[key] = shape(ref.shape.value, value) end target end - def structure(ref, values, target = nil) # rubocop:disable Metrics/AbcSize - return Smithy::Schema::EmptyStructure.new if ref.shape == Prelude::Unit + def structure(ref, values, target = nil) + return if values.nil? target = ref.shape.type.new if target.nil? ref.shape.members.each do |member_name, member_ref| - key = member_ref.traits['smithy.api#jsonName'] || member_ref.member_name - next unless values.key?(key) - - target[member_name] = shape(member_ref, values[key]) + value = values[location_name(member_ref)] + target[member_name] = shape(member_ref, value) unless value.nil? end target end @@ -86,39 +87,29 @@ def timestamp(value) fractional_time = Time.parse(value).to_f Time.at(fractional_time).utc rescue ArgumentError - raise "unhandled timestamp format `#{value}'" + raise "unhandled timestamp format: #{value}" end end end def union(ref, values, target = nil) # rubocop:disable Metrics/AbcSize - sanitize_union!(ref, values) - - key, value = values.first - return nil if key.nil? - ref.shape.members.each do |member_name, member_ref| - name = member_ref.traits['smithy.api#jsonName'] || member_ref.member_name - next unless values.key?(name) + value = values[location_name(member_ref)] + next if value.nil? target = ref.shape.member_type(member_name) if target.nil? - return target.new(shape(member_ref, values[name])) + return target.new(shape(member_ref, value)) end + + values.delete('__type') + key, value = values.first ref.shape.member_type(:unknown).new(key, value) end - def sanitize_union!(ref, values) # rubocop:disable Metrics/CyclomaticComplexity - return unless values.size > 1 - - # __type should be ignored unless it's a jsonName for a member - type_as_name = false - ref.shape.members.each_value do |member_ref| - name = member_ref.traits['smithy.api#jsonName'] || member_ref.member_name - type_as_name = true if name == '__type' - end + def location_name(ref) + return ref.member_name unless @json_name - values.delete('__type') if values.key?('__type') && !type_as_name - raise ArgumentError, "union value includes more than one key, received: #{values.keys}" if values.size > 1 + ref.traits['smithy.api#jsonName'] || ref.member_name end def sparse?(shape) diff --git a/gems/smithy-json/lib/smithy-json/serializer.rb b/gems/smithy-json/lib/smithy-json/serializer.rb index 2292427fd..163add533 100644 --- a/gems/smithy-json/lib/smithy-json/serializer.rb +++ b/gems/smithy-json/lib/smithy-json/serializer.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'base64' + module Smithy module JSON # @api private @@ -7,13 +9,11 @@ class Serializer include Smithy::Schema::Shapes def initialize(options = {}) - @options = options + @json_name = options[:json_name] || false end def serialize(shape, data) ref = shape.is_a?(ShapeRef) ? shape : ShapeRef.new(shape: shape) - return nil if ref.shape == Prelude::Unit - Smithy::JSON.dump(shape(ref, data)) end @@ -34,7 +34,7 @@ def shape(ref, value) # rubocop:disable Metrics/CyclomaticComplexity end def blob(value) - Base64.strict_encode64(value.is_a?(String) ? value : value.read) + Base64.strict_encode64(value.respond_to?(:read) ? value.read : value) end def float(value) @@ -50,10 +50,12 @@ def float(value) end def list(ref, values) + return if values.nil? + values.collect do |value| next if value.nil? && !sparse?(ref.shape) - value.nil? ? nil : shape(ref.shape.member, value) + shape(ref.shape.member, value) end end @@ -61,19 +63,16 @@ def map(ref, values) values.each.with_object({}) do |(key, value), data| next if value.nil? && !sparse?(ref.shape) - data[key] = value.nil? ? nil : shape(ref.shape.value, value) + data[key] = shape(ref.shape.value, value) end end def structure(ref, values) - return nil if values.nil? + return if values.nil? - values.each_pair.with_object({}) do |(key, value), data| - if ref.shape.member?(key) && !value.nil? - member_ref = ref.shape.member(key) - member_name = member_ref.traits['smithy.api#jsonName'] || member_ref.member_name - data[member_name] = shape(member_ref, value) - end + ref.shape.members.each_with_object({}) do |(member_name, member_ref), data| + value = values[member_name] + data[location_name(member_ref)] = shape(member_ref, value) unless value.nil? end end @@ -88,18 +87,16 @@ def timestamp(ref, value) end end - def union(ref, values) # rubocop:disable Metrics/AbcSize + def union(ref, values) data = {} if values.is_a?(Smithy::Schema::Union) - member_ref = ref.shape.member_by_type(values.class) - member_name = member_ref.traits['smithy.api#jsonName'] || member_ref.member_name - data[member_name] = shape(member_ref, values) + _name, member_ref = ref.shape.member_by_type(values.class) + data[location_name(member_ref)] = shape(member_ref, values) else key, value = values.first if ref.shape.member?(key) member_ref = ref.shape.member(key) - member_name = member_ref.traits['smithy.api#jsonName'] || member_ref.member_name - data[member_name] = shape(member_ref, value) + data[location_name(member_ref)] = shape(member_ref, value) end end data @@ -108,6 +105,12 @@ def union(ref, values) # rubocop:disable Metrics/AbcSize def sparse?(shape) shape.traits.include?('smithy.api#sparse') end + + def location_name(ref) + return ref.member_name unless @json_name + + ref.traits['smithy.api#jsonName'] || ref.member_name + end end end end diff --git a/gems/smithy-json/smithy-json.gemspec b/gems/smithy-json/smithy-json.gemspec index 891256c78..a720fa843 100644 --- a/gems/smithy-json/smithy-json.gemspec +++ b/gems/smithy-json/smithy-json.gemspec @@ -10,6 +10,8 @@ Gem::Specification.new do |spec| spec.license = 'Apache-2.0' spec.files = Dir['CHANGELOG.md', 'VERSION', 'lib/**/*'] + spec.add_dependency('base64') + spec.add_dependency('smithy-schema', '1.0.0.pre0') spec.required_ruby_version = '>= 3.3' diff --git a/gems/smithy-json/spec/smithy-json/codec_spec.rb b/gems/smithy-json/spec/smithy-json/codec_spec.rb index 5aac09c97..2b7d5bb50 100644 --- a/gems/smithy-json/spec/smithy-json/codec_spec.rb +++ b/gems/smithy-json/spec/smithy-json/codec_spec.rb @@ -4,21 +4,18 @@ module Smithy module JSON + # TODO: test all codec cases describe Codec do let(:shapes) { SchemaHelper.sample_shapes } let(:sample_schema) { SchemaHelper.sample_schema(shapes: shapes) } let(:structure_shape) { sample_schema.const_get(:Structure) } - it 'serialize returns nil when given a unit shape' do - expect(subject.serialize(Schema::Shapes::Prelude::Unit, '')).to be_nil - end - - it 'deserializes returns an empty hash when given bytes are empty' do - expect(subject.deserialize(Schema::Shapes::Prelude::String, '')).to be_empty + it 'deserializes returns an empty hash when given json is empty' do + expect(subject.deserialize(Schema::Shapes::Prelude::String, '')).to eq({}) end it 'deserializes returns an empty hash when given a unit shape' do - expect(subject.deserialize(Schema::Shapes::Prelude::Unit, '')).to be_empty + expect(subject.deserialize(Schema::Shapes::Prelude::Unit, '')).to eq({}) end it 'serializes and deserializes data' do @@ -47,21 +44,33 @@ module JSON union: { string: 'string' } } data = data.merge(structure: data) - bytes = subject.serialize(structure_shape, data) - expect(subject.deserialize(structure_shape, bytes).to_h).to eq(data) + json = subject.serialize(structure_shape, data) + expect(subject.deserialize(structure_shape, json).to_h).to eq(data) end context 'structures' do it 'serializes and deserializes structures as a type' do type = structure_shape.type.new(string: 'string') - bytes = subject.serialize(structure_shape, type) - expect(subject.deserialize(structure_shape, bytes).string).to eq('string') + json = subject.serialize(structure_shape, type) + expect(subject.deserialize(structure_shape, json).string).to eq('string') end it 'serializes and deserializes structures as a hash' do data = { string: 'string' } - bytes = subject.serialize(structure_shape, data) - expect(subject.deserialize(structure_shape, bytes).to_h).to eq(data) + json = subject.serialize(structure_shape, data) + expect(subject.deserialize(structure_shape, json).to_h).to eq(data) + end + + it 'serializes and deserializes structures with jsonName' do + subject = described_class.new(json_name: true) + shapes['smithy.ruby.tests#Structure']['members']['string'] = { + 'target' => 'smithy.api#String', + 'traits' => { 'smithy.api#jsonName' => 'NewString' } + } + data = { string: 'string' } + json = subject.serialize(structure_shape, data) + expect(json).to include('"NewString":"string"') + expect(subject.deserialize(structure_shape, json).to_h).to eq(data) end end @@ -69,38 +78,26 @@ module JSON it 'serializes and deserializes union as a type' do union = structure_shape.member(:union).shape.member_type(:string).new('string') type = structure_shape.type.new(union: union) - bytes = subject.serialize(structure_shape, type) - expect(subject.deserialize(structure_shape, bytes).union).to eq(union) + json = subject.serialize(structure_shape, type) + expect(subject.deserialize(structure_shape, json).union).to eq(union) end it 'serializes and deserializes unions as a hash' do data = { union: { string: 'string' } } - bytes = subject.serialize(structure_shape, data) - expect(subject.deserialize(structure_shape, bytes).to_h).to eq(data) + json = subject.serialize(structure_shape, data) + expect(subject.deserialize(structure_shape, json).to_h).to eq(data) end it 'serializes and deserializes a nil union' do data = { union: nil } - bytes = subject.serialize(structure_shape, data) - expect(subject.deserialize(structure_shape, bytes).union).to eq(nil) - end - - it 'serializes and deserializes an empty union' do - data = { union: {} } - bytes = subject.serialize(structure_shape, data) - expect(subject.deserialize(structure_shape, bytes).union).to eq(nil) - end - - it 'serializes and deserializes nil union values' do - data = { union: { string: nil } } - bytes = subject.serialize(structure_shape, data) - expect(subject.deserialize(structure_shape, bytes).to_h).to eq(data) + json = subject.serialize(structure_shape, data) + expect(subject.deserialize(structure_shape, json).union).to eq(nil) end it 'serializes and deserializes unit shape members' do data = { union: { unit: {} } } - bytes = subject.serialize(structure_shape, data) - expect(subject.deserialize(structure_shape, bytes).to_h).to eq(data) + json = subject.serialize(structure_shape, data) + expect(subject.deserialize(structure_shape, json).to_h).to eq(data) end it 'deserializes unknown union members' do @@ -111,16 +108,6 @@ module JSON expect(deserialized.union.to_h).to eq(unknown: { name: 'someThing', value: 'someValue' }) end - it 'raises when deserializing unions with more than one member' do - data = { 'union' => { 'string' => 'string', 'structure' => {} } }.to_json - expect { subject.deserialize(structure_shape, data) } - .to raise_error(ArgumentError, /union value includes more than one key/) - - data = { 'union' => { 'string' => 'string', 'someThing' => 'someValue' } }.to_json - expect { subject.deserialize(structure_shape, data) } - .to raise_error(ArgumentError, /union value includes more than one key/) - end - it 'ignores extra __type key when deserializing' do data = { 'union' => { '__type' => 'ignored', 'string' => 'string' } }.to_json deserialized = subject.deserialize(structure_shape, data) @@ -129,6 +116,7 @@ module JSON end it 'does not ignore __type if it is a jsonName member' do + subject = described_class.new(json_name: true) shapes['smithy.ruby.tests#Union']['members']['string'] = { 'target' => 'smithy.api#String', 'traits' => { 'smithy.api#jsonName' => '__type' } @@ -139,46 +127,35 @@ module JSON expect(deserialized.union).to be_a(structure_shape.member(:union).shape.member_type(:string)) expect(deserialized.union.to_h).to eq(string: 'string') end - - it 'raises when deserializing unions with more than one member with __type as a jsonName' do - shapes['smithy.ruby.tests#Union']['members']['string'] = { - 'target' => 'smithy.api#String', - 'traits' => { 'smithy.api#jsonName' => '__type' } - } - structure_shape = sample_schema.const_get(:Structure) - data = { 'union' => { '__type' => 'string', 'someThing' => 'someValue' } }.to_json - expect { subject.deserialize(structure_shape, data) } - .to raise_error(ArgumentError, /union value includes more than one key/) - end end context 'lists' do it 'serializes and deserializes lists' do data = { list: ['string'] } - bytes = subject.serialize(structure_shape, data) - expect(subject.deserialize(structure_shape, bytes).to_h).to eq(data) + json = subject.serialize(structure_shape, data) + expect(subject.deserialize(structure_shape, json).to_h).to eq(data) end it 'serializes and deserializes sparse lists' do shapes['smithy.ruby.tests#List']['traits'] = { 'smithy.api#sparse' => {} } data = { list: [nil] } - bytes = subject.serialize(structure_shape, data) - expect(subject.deserialize(structure_shape, bytes).to_h).to eq(data) + json = subject.serialize(structure_shape, data) + expect(subject.deserialize(structure_shape, json).to_h).to eq(data) end end context 'maps' do it 'serializes and deserializes maps' do data = { map: { 'key' => 'value' } } - bytes = subject.serialize(structure_shape, data) - expect(subject.deserialize(structure_shape, bytes).to_h).to eq(data) + json = subject.serialize(structure_shape, data) + expect(subject.deserialize(structure_shape, json).to_h).to eq(data) end it 'serializes and deserializes sparse maps' do shapes['smithy.ruby.tests#Map']['traits'] = { 'smithy.api#sparse' => {} } data = { map: { 'key' => nil } } - bytes = subject.serialize(structure_shape, data) - expect(subject.deserialize(structure_shape, bytes).to_h).to eq(data) + json = subject.serialize(structure_shape, data) + expect(subject.deserialize(structure_shape, json).to_h).to eq(data) end end end diff --git a/gems/smithy-schema/lib/smithy-schema.rb b/gems/smithy-schema/lib/smithy-schema.rb index 75093be33..0a03a75a2 100644 --- a/gems/smithy-schema/lib/smithy-schema.rb +++ b/gems/smithy-schema/lib/smithy-schema.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require_relative 'smithy-schema/shapes' require_relative 'smithy-schema/structure' require_relative 'smithy-schema/union' +require_relative 'smithy-schema/shapes' module Smithy # Base module for Smithy schema classes. diff --git a/gems/smithy-schema/lib/smithy-schema/shapes.rb b/gems/smithy-schema/lib/smithy-schema/shapes.rb index aed364b67..bbcfed330 100644 --- a/gems/smithy-schema/lib/smithy-schema/shapes.rb +++ b/gems/smithy-schema/lib/smithy-schema/shapes.rb @@ -201,6 +201,11 @@ class StringShape < Shape; end # Represents a Structure shape. class StructureShape < Structure + def initialize(options = {}) + super + @type = options[:type] + end + # @return [Class] attr_accessor :type end @@ -228,7 +233,7 @@ def initialize(options = {}) # @return [ShapeRef] def add_member(name, type, shape_ref) @member_types[name] = type - @members_by_type[type] = shape_ref + @members_by_type[type] = [name, shape_ref] super(name, shape_ref) end @@ -302,7 +307,8 @@ module Prelude Timestamp = TimestampShape.new(id: 'smithy.api#Timestamp') Unit = StructureShape.new( id: 'smithy.api#Unit', - traits: { 'smithy.api#unitType' => {} } + traits: { 'smithy.api#unitType' => {} }, + type: Schema::EmptyStructure ) end end diff --git a/gems/smithy-schema/lib/smithy-schema/structure.rb b/gems/smithy-schema/lib/smithy-schema/structure.rb index 8cb5fee97..47e803736 100644 --- a/gems/smithy-schema/lib/smithy-schema/structure.rb +++ b/gems/smithy-schema/lib/smithy-schema/structure.rb @@ -44,7 +44,7 @@ def _to_h_array(obj) end # An empty Struct that includes the {Client::Structure} module. - EmptyStructure = Struct.new do + class EmptyStructure < Struct.new # rubocop:disable Style/StructInheritance include Smithy::Schema::Structure end end diff --git a/gems/smithy-schema/sig/smithy-schema/shapes.rbs b/gems/smithy-schema/sig/smithy-schema/shapes.rbs index c89ba6251..1436ab126 100644 --- a/gems/smithy-schema/sig/smithy-schema/shapes.rbs +++ b/gems/smithy-schema/sig/smithy-schema/shapes.rbs @@ -90,13 +90,13 @@ module Smithy class UnionShape < Structure attr_accessor type: Class attr_accessor member_types: Hash[Symbol, Class] - attr_accessor members_by_type: Hash[Class, ShapeRef] + attr_accessor members_by_type: Hash[Class, [Symbol, ShapeRef]] def add_member: (Symbol, Class, ShapeRef) -> ShapeRef def member_type?: (Symbol) -> bool def member_type: (Symbol) -> Class? def member_by_type?: (Class) -> bool - def member_by_type: (Class) -> ShapeRef? + def member_by_type: (Class) -> [Symbol, ShapeRef]? end module Prelude diff --git a/gems/smithy-schema/spec/smithy-schema/shapes_spec.rb b/gems/smithy-schema/spec/smithy-schema/shapes_spec.rb index 235618677..9128d4066 100644 --- a/gems/smithy-schema/spec/smithy-schema/shapes_spec.rb +++ b/gems/smithy-schema/spec/smithy-schema/shapes_spec.rb @@ -412,7 +412,7 @@ module Shapes shape_ref = ShapeRef.new(shape: StringShape.new) subject.add_member(:foo, union_type, shape_ref) expect(subject.member_types[:foo]).to be(union_type) - expect(subject.members_by_type[union_type]).to eq(shape_ref) + expect(subject.members_by_type[union_type]).to eq([:foo, shape_ref]) end end @@ -444,7 +444,7 @@ module Shapes it 'returns the member by type' do shape_ref = ShapeRef.new(shape: StringShape.new) subject.add_member(:foo, union_type, shape_ref) - expect(subject.member_by_type(union_type)).to eq(shape_ref) + expect(subject.member_by_type(union_type)).to eq([:foo, shape_ref]) end end end diff --git a/gems/smithy-schema/spec/support/schema_helper.rb b/gems/smithy-schema/spec/support/schema_helper.rb index e9a69609e..114b20f3c 100644 --- a/gems/smithy-schema/spec/support/schema_helper.rb +++ b/gems/smithy-schema/spec/support/schema_helper.rb @@ -85,7 +85,10 @@ def sample_shapes 'short' => { 'target' => 'smithy.api#Short' }, 'streamingBlob' => { 'target' => 'smithy.ruby.tests#StreamingBlob', - 'traits' => { 'smithy.api#default' => 'streamingBlob' } + 'traits' => { + 'smithy.api#required' => {}, + 'smithy.api#clientOptional' => {} + } }, 'string' => { 'target' => 'smithy.api#String' }, 'structure' => { 'target' => 'smithy.ruby.tests#Structure' }, diff --git a/gems/smithy/lib/smithy/model/rbs.rb b/gems/smithy/lib/smithy/model/rbs.rb index 2da36229a..d99163fa2 100644 --- a/gems/smithy/lib/smithy/model/rbs.rb +++ b/gems/smithy/lib/smithy/model/rbs.rb @@ -10,7 +10,8 @@ def type(model, id, shape) case shape['type'] when 'blob', 'string', 'enum' then 'String' when 'boolean' then 'bool' - when 'byte', 'short', 'integer', 'long', 'intEnum' then 'Integer' + when 'byte', 'short', 'integer', 'bigInteger', 'long', 'intEnum' then 'Integer' + when 'bigDecimal' then 'BigDecimal' when 'float', 'double' then 'Float' when 'timestamp' then 'Time' when 'document' then 'Smithy::Schema::document' diff --git a/gems/smithy/lib/smithy/templates/client/protocol_spec.erb b/gems/smithy/lib/smithy/templates/client/protocol_spec.erb index 9f9c2c49e..e11b8a6d3 100644 --- a/gems/smithy/lib/smithy/templates/client/protocol_spec.erb +++ b/gems/smithy/lib/smithy/templates/client/protocol_spec.erb @@ -96,7 +96,7 @@ module <%= module_name %> response[:body] = nil <% end -%> client.stub_responses(:<%= operation_tests.name %>, response) - output = client.<%= operation_tests.name %> + output = client.<%= operation_tests.name %>(<%= test.params %>) <% if (member_name, _shape = test.streaming_member) -%> output.data.<%= member_name.underscore %>.rewind output.data.<%= member_name.underscore %> = output.data.<%= member_name.underscore %>.read diff --git a/gems/smithy/lib/smithy/templates/client/types.erb b/gems/smithy/lib/smithy/templates/client/types.erb index 60a5301f4..1675c52f8 100644 --- a/gems/smithy/lib/smithy/templates/client/types.erb +++ b/gems/smithy/lib/smithy/templates/client/types.erb @@ -15,11 +15,20 @@ module <%= module_name %> # <%= docstring %> <% end -%> class <%= type.name %> < Struct.new( -<% type.member_names.each do |member| -%> - :<%= member %>, +<% type.members.each do |member| -%> + :<%= member.name.underscore %>, <% end -%> keyword_init: true) include Smithy::Schema::Structure +<% if !type.input? && type.defaults.any? -%> + + def initialize(options = {}) +<% type.defaults.each do |member| -%> + options[:<%= member.name.underscore %>] = <%= member.default %> unless options.key?(:<%= member.name.underscore %>) +<% end -%> + super + end +<% end -%> end <% elsif type.type == 'union' -%> class <%= type.name %> < Smithy::Schema::Union diff --git a/gems/smithy/lib/smithy/views/client/protocol_spec.rb b/gems/smithy/lib/smithy/views/client/protocol_spec.rb index 342d4b162..8d208e4a5 100644 --- a/gems/smithy/lib/smithy/views/client/protocol_spec.rb +++ b/gems/smithy/lib/smithy/views/client/protocol_spec.rb @@ -152,7 +152,7 @@ def params end def endpoint - "#{test_case.fetch('authScheme', 'http')}://#{test_case.fetch('host', '127.0.0.1')}" + "https://#{test_case.fetch('host', '127.0.0.1')}" end def body_expect @@ -182,7 +182,28 @@ def idempotency_token_trait? # @api private class ResponseTest < TestCase def params - ShapeToHash.transform_value(@model, test_case.fetch('params', {}), @output_shape) + # Finds all required members to pass operation validation + ShapeToHash.transform_value(@model, structure(@input_shape, {}), @input_shape) + end + + def shape(shape, value) + case shape['type'] + when 'structure' then structure(shape, value) + else value + end + end + + def structure(shape, values) + shape['members'].each_with_object({}) do |(member_name, member_shape), data| + next unless required?(member_shape.fetch('traits', {})) + + target = Model.shape(@model, member_shape['target']) + data[member_name] = shape(target, values) + end + end + + def required?(traits) + traits.include?('smithy.api#required') && !traits.include?('smithy.api#clientOptional') end def stub_body @@ -195,7 +216,8 @@ def stub_body end def data_expect - "expect(output.data.to_h).to match_data(#{params})" + output = ShapeToHash.transform_value(@model, test_case.fetch('params', {}), @output_shape) + "expect(output.data.to_h).to match_data(#{output})" end def streaming_member diff --git a/gems/smithy/lib/smithy/views/client/schema.rb b/gems/smithy/lib/smithy/views/client/schema.rb index 7078c80d5..a3e795068 100644 --- a/gems/smithy/lib/smithy/views/client/schema.rb +++ b/gems/smithy/lib/smithy/views/client/schema.rb @@ -123,6 +123,11 @@ def build_errors(errors) # @api private class Shape + OMITTED_TRAITS = %w[ + smithy.api#default + smithy.api#documentation + ].freeze + SHAPE_CLASS_MAP = { 'bigDecimal' => 'BigDecimalShape', 'bigInteger' => 'IntegerShape', @@ -150,7 +155,7 @@ def initialize(service, id, shape) @id = id @shape = shape @type = shape['type'] - @traits = shape['traits'] || {} + @traits = shape.fetch('traits', {}).except(*OMITTED_TRAITS) end attr_reader :type @@ -175,7 +180,7 @@ def initialize(service, id, shape) attr_reader :members def type_class - "Types::#{Model::Shape.name(@id).camelize}" + "Types::#{@service.dig('rename', @id) || Model::Shape.name(@id).camelize}" end def http_payload? @@ -256,7 +261,7 @@ def initialize(service, id, shape) attr_reader :members def type_class - "Types::#{Model::Shape.name(@id).camelize}" + "Types::#{@service.dig('rename', @id) || Model::Shape.name(@id).camelize}" end def union_type(shape_ref) @@ -272,6 +277,11 @@ def build_shape_refs(members) # @api private class ShapeRef + OMITTED_TRAITS = %w[ + smithy.api#default + smithy.api#documentation + ].freeze + PRELUDE_SHAPES_MAP = { 'smithy.api#BigInteger' => 'Prelude::BigInteger', 'smithy.api#BigDecimal' => 'Prelude::BigDecimal', @@ -301,7 +311,7 @@ def initialize(service, member_name, shape_ref) @name = member_name.underscore if member_name @member_name = member_name @target = target(shape_ref['target']) - @traits = shape_ref['traits'] || {} + @traits = shape_ref.fetch('traits', {}).except(*OMITTED_TRAITS) end attr_reader :name diff --git a/gems/smithy/lib/smithy/views/client/types.rb b/gems/smithy/lib/smithy/views/client/types.rb index eeba1d77e..eebffb74c 100644 --- a/gems/smithy/lib/smithy/views/client/types.rb +++ b/gems/smithy/lib/smithy/views/client/types.rb @@ -26,10 +26,21 @@ def types # @api private class Type def initialize(service, model, id, shape) - _, @service = service.first - @model = model - @id = id + _, service = service.first @shape = shape + @type = shape['type'] + @name = service.fetch('rename', {})[id] || Model::Shape.name(id).camelize + @members = shape['members'].map { |name, member| Member.new(model, name, member) } + end + + attr_reader :type, :name, :members + + def input? + @shape.fetch('traits', {}).key?('smithy.api#input') + end + + def defaults + @members.select { |member| member if member.default? } end def docstrings @@ -46,32 +57,16 @@ def attribute_docstrings end lines end - - def name - @service.fetch('rename', {})[@id] || Model::Shape.name(@id).camelize - end - - def member_names - @shape['members'].keys.map(&:underscore) - end - - def members - @members ||= @shape['members'].map { |name, member| Member.new(@model, name, member) } - end - - def type - @shape['type'] - end end # @api private class Member def initialize(model, name, member) - @model = model @name = name @member = member - @id = member['target'] - @target = Model.shape(model, @id) + @member_traits = member.fetch('traits', {}) + @target = Model.shape(model, member['target']) + @doc_type = Model::YARD.type(model, member['target'], @target) end attr_reader :name @@ -88,9 +83,42 @@ def attribute_docstrings docstrings.each do |docstring| lines << " #{docstring}" end - lines << " @return [#{Model::YARD.type(@model, @id, @target)}]" + lines << " @return [#{@doc_type}]" lines end + + def default? + traits = @member.fetch('traits', {}) + traits.key?('smithy.api#default') && !traits.key?('smithy.api#clientOptional') + end + + def default + default = @member.dig('traits', 'smithy.api#default') + case @target['type'] + when 'blob' then "Base64.strict_decode64('#{default}')" + when 'bigDecimal' then "BigDecimal('#{default}')" + when 'document' then document(default) + when 'enum', 'string' then "'#{default}'" + when 'timestamp' then timestamp(default) + else default + end + end + + def document(default) + case default + when nil then 'nil' + when String then "'#{default}'" + else default + end + end + + def timestamp(default) + case default + when Integer then "Time.at(#{default})" + when String then "Time.parse('#{default}')" + else default + end + end end end end diff --git a/gems/smithy/lib/smithy/welds.rb b/gems/smithy/lib/smithy/welds.rb index 5a8b3ec55..481d44874 100644 --- a/gems/smithy/lib/smithy/welds.rb +++ b/gems/smithy/lib/smithy/welds.rb @@ -9,6 +9,7 @@ require_relative 'welds/endpoints' require_relative 'welds/plugins' require_relative 'welds/rpc_v2_cbor' +require_relative 'welds/synthetic_input_output' # require_relative 'welds/rubocop' module Smithy diff --git a/gems/smithy/lib/smithy/welds/synthetic_input_output.rb b/gems/smithy/lib/smithy/welds/synthetic_input_output.rb new file mode 100644 index 000000000..41786ad80 --- /dev/null +++ b/gems/smithy/lib/smithy/welds/synthetic_input_output.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'active_support/core_ext/object/deep_dup' + +module Smithy + module Welds + # Creates synthetic input and output shapes for operations that do not have them. + class SyntheticInputOutput < Weld + def pre_process(model) + say_status :insert, 'Creating synthetic input and output shapes', @plan.quiet + create_synthetic_input_output_shapes(model) + end + + private + + def create_synthetic_input_output_shapes(model) + Model::ServiceIndex.new(model).operations_for(@plan.service).each do |operation_id, operation| + create_synthetic_input_shape(model, operation_id, operation) if operation['input'] + create_synthetic_output_shape(model, operation_id, operation) if operation['output'] + end + end + + def create_synthetic_input_shape(model, operation_id, operation) + input_target = operation['input']['target'] + target = Model.shape(model, input_target) + return if target.fetch('traits', {}).include?('smithy.api#input') || input_target == 'smithy.api#Unit' + + input_target = new_shape_id(model, operation_id, 'Input') + operation['input']['target'] = input_target + input_shape = target.deep_dup + model['shapes'][input_target] = input_shape + input_shape['traits'] = input_shape.fetch('traits', {}).merge({ 'smithy.api#input' => {} }) + end + + def create_synthetic_output_shape(model, operation_id, operation) + output_target = operation['output']['target'] + target = Model.shape(model, output_target) + return if target.fetch('traits', {}).include?('smithy.api#output') || output_target == 'smithy.api#Unit' + + output_target = new_shape_id(model, operation_id, 'Output') + operation['output']['target'] = output_target + output_shape = target.deep_dup + model['shapes'][output_target] = output_shape + output_shape['traits'] = output_shape.fetch('traits', {}).merge({ 'smithy.api#output' => {} }) + end + + def new_shape_id(model, operation_id, suffix) + namespace = Model::Shape.namespace(operation_id) + operation_name = Model::Shape.name(operation_id) + id = "#{namespace}##{operation_name}#{suffix}" + return id unless model['shapes'].key?(id) + + id = "#{namespace}##{operation_name}Operation#{suffix}" + return id unless model['shapes'].key?(id) + + raise "unable to generate a unique synthetic #{suffix} shape ID for #{operation_id}" + end + end + end +end diff --git a/gems/smithy/spec/fixtures/protocol_tests/rpcv2_cbor/model.json b/gems/smithy/spec/fixtures/protocol_tests/rpcv2_cbor/model.json index e2d86c6cb..f3e719cf4 100644 --- a/gems/smithy/spec/fixtures/protocol_tests/rpcv2_cbor/model.json +++ b/gems/smithy/spec/fixtures/protocol_tests/rpcv2_cbor/model.json @@ -789,18 +789,6 @@ } ], "traits": { - "smithy.ruby#skipTests": [ - { - "id": "RpcV2CborClientPopulatesDefaultValuesInInput", - "reason": "Defaults not Implemented yet.", - "type": "request" - }, - { - "id": "RpcV2CborClientPopulatesDefaultsValuesWhenMissingInResponse", - "reason": "Defaults not Implemented yet.", - "type": "response" - } - ], "smithy.test#httpRequestTests": [ { "id": "RpcV2CborClientPopulatesDefaultValuesInInput", diff --git a/gems/smithy/spec/fixtures/protocol_tests/rpcv2_cbor/skipped_tests.smithy b/gems/smithy/spec/fixtures/protocol_tests/rpcv2_cbor/skipped_tests.smithy index 17c161a86..1dab5287b 100644 --- a/gems/smithy/spec/fixtures/protocol_tests/rpcv2_cbor/skipped_tests.smithy +++ b/gems/smithy/spec/fixtures/protocol_tests/rpcv2_cbor/skipped_tests.smithy @@ -2,9 +2,4 @@ $version: "2.0" namespace smithy.protocoltests.rpcv2Cbor -use smithy.ruby#skipTests - -apply OperationWithDefaults @skipTests([ - { id: "RpcV2CborClientPopulatesDefaultValuesInInput", reason: "Defaults not Implemented yet.", type: "request" } - { id: "RpcV2CborClientPopulatesDefaultsValuesWhenMissingInResponse", reason: "Defaults not Implemented yet.", type: "response" } -]) +// This file is used to skip tests. diff --git a/gems/smithy/spec/fixtures/protocol_tests/skip_tests.smithy b/gems/smithy/spec/fixtures/protocol_tests/skip_tests.smithy index 3ba8d3593..539e66e0f 100644 --- a/gems/smithy/spec/fixtures/protocol_tests/skip_tests.smithy +++ b/gems/smithy/spec/fixtures/protocol_tests/skip_tests.smithy @@ -21,7 +21,3 @@ enum TestType { @enumValue("response") RESPONSE } - - - - diff --git a/gems/smithy/spec/fixtures/synthetic_input_output/model.json b/gems/smithy/spec/fixtures/synthetic_input_output/model.json new file mode 100644 index 000000000..9c22a77a4 --- /dev/null +++ b/gems/smithy/spec/fixtures/synthetic_input_output/model.json @@ -0,0 +1,92 @@ +{ + "smithy": "2.0", + "shapes": { + "smithy.ruby.tests#Operation": { + "type": "operation", + "input": { + "target": "smithy.ruby.tests#Structure" + }, + "output": { + "target": "smithy.ruby.tests#Structure" + } + }, + "smithy.ruby.tests#OperationWithInputAndOutputTraits": { + "type": "operation", + "input": { + "target": "smithy.ruby.tests#OperationWithInputAndOutputTraitsInput" + }, + "output": { + "target": "smithy.ruby.tests#OperationWithInputAndOutputTraitsOutput" + } + }, + "smithy.ruby.tests#OperationWithInputAndOutputTraitsInput": { + "type": "structure", + "members": { + "member": { + "target": "smithy.api#String" + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "smithy.ruby.tests#OperationWithInputAndOutputTraitsOutput": { + "type": "structure", + "members": { + "member": { + "target": "smithy.api#String" + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "smithy.ruby.tests#OperationWithNamingConflict": { + "type": "operation", + "input": { + "target": "smithy.ruby.tests#OperationWithNamingConflictInput" + }, + "output": { + "target": "smithy.ruby.tests#OperationWithNamingConflictOutput" + } + }, + "smithy.ruby.tests#OperationWithNamingConflictInput": { + "type": "structure", + "members": { + "member": { + "target": "smithy.api#String" + } + } + }, + "smithy.ruby.tests#OperationWithNamingConflictOutput": { + "type": "structure", + "members": { + "member": { + "target": "smithy.api#String" + } + } + }, + "smithy.ruby.tests#Structure": { + "type": "structure", + "members": { + "member": { + "target": "smithy.api#String" + } + } + }, + "smithy.ruby.tests#SyntheticInputOutput": { + "type": "service", + "operations": [ + { + "target": "smithy.ruby.tests#Operation" + }, + { + "target": "smithy.ruby.tests#OperationWithInputAndOutputTraits" + }, + { + "target": "smithy.ruby.tests#OperationWithNamingConflict" + } + ] + } + } +} diff --git a/gems/smithy/spec/fixtures/synthetic_input_output/model.smithy b/gems/smithy/spec/fixtures/synthetic_input_output/model.smithy new file mode 100644 index 000000000..78e419023 --- /dev/null +++ b/gems/smithy/spec/fixtures/synthetic_input_output/model.smithy @@ -0,0 +1,48 @@ +$version: "2" + +namespace smithy.ruby.tests + +service SyntheticInputOutput { + operations: [ + Operation, + OperationWithInputAndOutputTraits, + OperationWithNamingConflict + ] +} + +operation Operation { + input: Structure + output: Structure +} + +structure Structure { + member: String +} + +operation OperationWithInputAndOutputTraits { + input: OperationWithInputAndOutputTraitsInput + output: OperationWithInputAndOutputTraitsOutput +} + +@input +structure OperationWithInputAndOutputTraitsInput { + member: String +} + +@output +structure OperationWithInputAndOutputTraitsOutput { + member: String +} + +operation OperationWithNamingConflict { + input: OperationWithNamingConflictInput + output: OperationWithNamingConflictOutput +} + +structure OperationWithNamingConflictInput { + member: String +} + +structure OperationWithNamingConflictOutput { + member: String +} diff --git a/gems/smithy/spec/interfaces/client/client_spec.rb b/gems/smithy/spec/interfaces/client/client_spec.rb index f6c2fb7eb..69de29d43 100644 --- a/gems/smithy/spec/interfaces/client/client_spec.rb +++ b/gems/smithy/spec/interfaces/client/client_spec.rb @@ -58,7 +58,7 @@ @option params [String] :bar Shape documentation @option params [String] :qux - @return [Types::Foo] + @return [Types::OperationOutput] DOC client_file = File.join(@plan.destination_root, 'lib', 'documentation_trait', 'client.rb') expect(expected).to be_in_documentation(client_file, 'DocumentationTrait::Client', 'operation') diff --git a/gems/smithy/spec/interfaces/client/stub_responses_spec.rb b/gems/smithy/spec/interfaces/client/stub_responses_spec.rb index 1d978bf90..487d69bbe 100644 --- a/gems/smithy/spec/interfaces/client/stub_responses_spec.rb +++ b/gems/smithy/spec/interfaces/client/stub_responses_spec.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true +require 'bigdecimal' + require_relative '../../spec_helper' -describe 'Client: rpcv2Cbor Protocol; Stub Responses' do +describe 'Client: Stub Responses' do ['generated client gem', 'generated client from source code'].each do |context| next if ENV['SMITHY_RUBY_RBS_TEST'] && context != 'generated client gem' @@ -12,22 +14,23 @@ let(:now) { Time.now } let(:default_stub_data) do { - big_decimal: 0.0, - big_integer: 0, blob: String.new('blob'), boolean: false, + string: 'string', byte: 0, + short: 0, + integer: 0, + long: 0, + float: 0.0, double: 0.0, + big_integer: 0, + big_decimal: BigDecimal(0), + timestamp: now, enum: 'enum', - float: 0.0, int_enum: 0, - integer: 0, list: [], - long: 0, map: {}, - short: 0, - string: 'string', - timestamp: now, + structure: { member: 'string' }, union: { string: 'string' } } end @@ -37,12 +40,12 @@ allow(Time).to receive(:at).and_return(now) end - subject { Shapes::Client.new(stub_responses: true) } + subject { Shapes::Client.new(stub_responses: true, protocol: Smithy::Client::RPCv2CBOR::Protocol.new) } describe '#stub_data' do it 'returns the correct type' do stub = subject.stub_data(:operation) - expect(stub).to be_a(Shapes::Types::OperationInputOutput) + expect(stub).to be_a(Shapes::Types::OperationOutput) end it 'can return default stubbed data' do @@ -53,7 +56,7 @@ it 'can set stubbed data mixed with defaults' do data = default_stub_data.merge(string: 'new string') stub = subject.stub_data(:operation, { string: 'new string' }) - expect(stub.to_h).to include(data) + expect(stub.to_h).to eq(data) end end @@ -74,6 +77,13 @@ subject.stub_responses(:operation, { string: 'value' }) expect(subject.config.stubs[:operation].size).to eq(1) end + + it 'does not mix stub data with defaults' do + data = { string: 'new string' } + subject.stub_responses(:operation, data) + stub = subject.operation + expect(stub.to_h).to eq(data) + end end context '#api_requests' do diff --git a/gems/smithy/spec/interfaces/client/types_spec.rb b/gems/smithy/spec/interfaces/client/types_spec.rb index 245ae217e..1e5d96100 100644 --- a/gems/smithy/spec/interfaces/client/types_spec.rb +++ b/gems/smithy/spec/interfaces/client/types_spec.rb @@ -27,7 +27,7 @@ @return [String] DOC client_file = File.join(@plan.destination_root, 'lib', 'documentation_trait', 'types.rb') - expect(expected).to be_in_documentation(client_file, 'DocumentationTrait::Types::Foo') + expect(expected).to be_in_documentation(client_file, 'DocumentationTrait::Types::OperationOutput') end end end diff --git a/gems/smithy/spec/interfaces/welds/synthetic_input_output_spec.rb b/gems/smithy/spec/interfaces/welds/synthetic_input_output_spec.rb new file mode 100644 index 000000000..4e0d17ef5 --- /dev/null +++ b/gems/smithy/spec/interfaces/welds/synthetic_input_output_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require_relative '../../spec_helper' + +describe 'Welds: Synthetic Input and Output' do + ['generated client gem', + 'generated schema gem', + 'generated client from source code', + 'generated schema from source code'].each do |context| + context context do + include_context context, 'SyntheticInputOutput' + + it 'creates synthetic input and output types' do + expect(defined?(SyntheticInputOutput::Types::OperationInput)).to_not be nil + expect(defined?(SyntheticInputOutput::Types::OperationOutput)).to_not be nil + expect(defined?(SyntheticInputOutput::Types::Structure)).to be nil + end + + it 'preserves input and output types on the operation with the input and output trait' do + expect(defined?(SyntheticInputOutput::Types::OperationWithInputAndOutputTraitsInput)).to_not be nil + expect(defined?(SyntheticInputOutput::Types::OperationWithInputAndOutputTraitsOutput)).to_not be nil + expect(defined?(SyntheticInputOutput::Types::Structure)).to be nil + end + + it 'handles naming conflicts by inserting Operation between the operation name and the suffix' do + expect(defined?(SyntheticInputOutput::Types::OperationWithNamingConflictOperationInput)).to_not be nil + expect(defined?(SyntheticInputOutput::Types::OperationWithNamingConflictOperationOutput)).to_not be nil + expect(defined?(SyntheticInputOutput::Types::Structure)).to be nil + end + end + end + + ['generated client gem', 'generated client from source code'].each do |context| + context context do + include_context context, 'SyntheticInputOutput' + + it 'assigns synthetic input and output shapes to the operation' do + client = SyntheticInputOutput::Client.new + operation = client.config.service.operation(:operation) + expect(operation.input.shape.type).to eq(SyntheticInputOutput::Types::OperationInput) + expect(operation.output.shape.type).to eq(SyntheticInputOutput::Types::OperationOutput) + end + + it 'preserves input and output shapes on the operation with the input and output trait' do + client = SyntheticInputOutput::Client.new + operation = client.config.service.operation(:operation_with_input_and_output_traits) + expect(operation.input.shape.type) + .to eq(SyntheticInputOutput::Types::OperationWithInputAndOutputTraitsInput) + expect(operation.output.shape.type) + .to eq(SyntheticInputOutput::Types::OperationWithInputAndOutputTraitsOutput) + end + + it 'handles naming conflicts by inserting Operation between the operation name and the suffix' do + client = SyntheticInputOutput::Client.new + operation = client.config.service.operation(:operation_with_naming_conflict) + expect(operation.input.shape.type) + .to eq(SyntheticInputOutput::Types::OperationWithNamingConflictOperationInput) + expect(operation.output.shape.type) + .to eq(SyntheticInputOutput::Types::OperationWithNamingConflictOperationOutput) + end + end + end +end diff --git a/gems/smithy/spec/support/examples/types_examples.rb b/gems/smithy/spec/support/examples/types_examples.rb index 6a41a008f..5bd67edda 100644 --- a/gems/smithy/spec/support/examples/types_examples.rb +++ b/gems/smithy/spec/support/examples/types_examples.rb @@ -19,15 +19,16 @@ it 'supports nested to_h' do structure = ShapeService::Types::Structure.new(member: 'member') - union = ShapeService::Types::Union::Structure.new(structure) - input_output = ShapeService::Types::OperationInputOutput.new( + input = ShapeService::Types::OperationInput.new( + list: ['item'], + map: { 'key' => 'value' }, string: 'string', - union: union + union: ShapeService::Types::Union::Structure.new(structure) ) expected = { string: 'string', union: { structure: { member: 'member' } } } - expect(input_output.to_h).to eq(expected) + expect(input.to_h).to include(expected) end end diff --git a/projections/shapes/lib/shapes/client.rb b/projections/shapes/lib/shapes/client.rb index 1a03c3862..a6c83ca52 100644 --- a/projections/shapes/lib/shapes/client.rb +++ b/projections/shapes/lib/shapes/client.rb @@ -187,7 +187,7 @@ def initialize(*options) # @option params [Hash] :map # @option params [Types::Structure] :structure # @option params [Types::Union] :union - # @return [Types::OperationInputOutput] + # @return [Types::OperationOutput] # @example Request syntax with placeholder values # params = { # blob: "data", diff --git a/projections/shapes/lib/shapes/schema.rb b/projections/shapes/lib/shapes/schema.rb index 35149c105..ab92b3e7d 100644 --- a/projections/shapes/lib/shapes/schema.rb +++ b/projections/shapes/lib/shapes/schema.rb @@ -21,7 +21,8 @@ module Schema List = ListShape.new(id: 'smithy.ruby.tests#List', traits: {"smithy.ruby.tests#shape" => {}}) Long = IntegerShape.new(id: 'smithy.ruby.tests#Long', traits: {"smithy.ruby.tests#shape" => {}}) Map = MapShape.new(id: 'smithy.ruby.tests#Map', traits: {"smithy.ruby.tests#shape" => {}}) - OperationInputOutput = StructureShape.new(id: 'smithy.ruby.tests#OperationInputOutput') + OperationInput = StructureShape.new(id: 'smithy.ruby.tests#OperationInput', traits: {"smithy.api#input" => {}}) + OperationOutput = StructureShape.new(id: 'smithy.ruby.tests#OperationOutput', traits: {"smithy.api#output" => {}}) Short = IntegerShape.new(id: 'smithy.ruby.tests#Short', traits: {"smithy.ruby.tests#shape" => {}}) String = StringShape.new(id: 'smithy.ruby.tests#String', traits: {"smithy.ruby.tests#shape" => {}}) Structure = StructureShape.new(id: 'smithy.ruby.tests#Structure', traits: {"smithy.ruby.tests#shape" => {}}) @@ -33,26 +34,46 @@ module Schema List.member = ShapeRef.new(shape: String, traits: {"smithy.ruby.tests#shape" => {}}) Map.key = ShapeRef.new(shape: String, traits: {"smithy.ruby.tests#shape" => {}}) Map.value = ShapeRef.new(shape: String, traits: {"smithy.ruby.tests#shape" => {}}) - OperationInputOutput.add_member(:blob, ShapeRef.new(shape: Blob, member_name: 'blob')) - OperationInputOutput.add_member(:boolean, ShapeRef.new(shape: Boolean, member_name: 'boolean')) - OperationInputOutput.add_member(:string, ShapeRef.new(shape: String, member_name: 'string')) - OperationInputOutput.add_member(:byte, ShapeRef.new(shape: Byte, member_name: 'byte')) - OperationInputOutput.add_member(:short, ShapeRef.new(shape: Short, member_name: 'short')) - OperationInputOutput.add_member(:integer, ShapeRef.new(shape: Integer, member_name: 'integer')) - OperationInputOutput.add_member(:long, ShapeRef.new(shape: Long, member_name: 'long')) - OperationInputOutput.add_member(:float, ShapeRef.new(shape: Float, member_name: 'float')) - OperationInputOutput.add_member(:double, ShapeRef.new(shape: Double, member_name: 'double')) - OperationInputOutput.add_member(:big_integer, ShapeRef.new(shape: BigInteger, member_name: 'bigInteger')) - OperationInputOutput.add_member(:big_decimal, ShapeRef.new(shape: BigDecimal, member_name: 'bigDecimal')) - OperationInputOutput.add_member(:timestamp, ShapeRef.new(shape: Timestamp, member_name: 'timestamp')) - OperationInputOutput.add_member(:document, ShapeRef.new(shape: Document, member_name: 'document')) - OperationInputOutput.add_member(:enum, ShapeRef.new(shape: Enum, member_name: 'enum')) - OperationInputOutput.add_member(:int_enum, ShapeRef.new(shape: IntEnum, member_name: 'intEnum')) - OperationInputOutput.add_member(:list, ShapeRef.new(shape: List, member_name: 'list')) - OperationInputOutput.add_member(:map, ShapeRef.new(shape: Map, member_name: 'map')) - OperationInputOutput.add_member(:structure, ShapeRef.new(shape: Structure, member_name: 'structure')) - OperationInputOutput.add_member(:union, ShapeRef.new(shape: Union, member_name: 'union')) - OperationInputOutput.type = Types::OperationInputOutput + OperationInput.add_member(:blob, ShapeRef.new(shape: Blob, member_name: 'blob')) + OperationInput.add_member(:boolean, ShapeRef.new(shape: Boolean, member_name: 'boolean')) + OperationInput.add_member(:string, ShapeRef.new(shape: String, member_name: 'string')) + OperationInput.add_member(:byte, ShapeRef.new(shape: Byte, member_name: 'byte')) + OperationInput.add_member(:short, ShapeRef.new(shape: Short, member_name: 'short')) + OperationInput.add_member(:integer, ShapeRef.new(shape: Integer, member_name: 'integer')) + OperationInput.add_member(:long, ShapeRef.new(shape: Long, member_name: 'long')) + OperationInput.add_member(:float, ShapeRef.new(shape: Float, member_name: 'float')) + OperationInput.add_member(:double, ShapeRef.new(shape: Double, member_name: 'double')) + OperationInput.add_member(:big_integer, ShapeRef.new(shape: BigInteger, member_name: 'bigInteger')) + OperationInput.add_member(:big_decimal, ShapeRef.new(shape: BigDecimal, member_name: 'bigDecimal')) + OperationInput.add_member(:timestamp, ShapeRef.new(shape: Timestamp, member_name: 'timestamp')) + OperationInput.add_member(:document, ShapeRef.new(shape: Document, member_name: 'document')) + OperationInput.add_member(:enum, ShapeRef.new(shape: Enum, member_name: 'enum')) + OperationInput.add_member(:int_enum, ShapeRef.new(shape: IntEnum, member_name: 'intEnum')) + OperationInput.add_member(:list, ShapeRef.new(shape: List, member_name: 'list')) + OperationInput.add_member(:map, ShapeRef.new(shape: Map, member_name: 'map')) + OperationInput.add_member(:structure, ShapeRef.new(shape: Structure, member_name: 'structure')) + OperationInput.add_member(:union, ShapeRef.new(shape: Union, member_name: 'union')) + OperationInput.type = Types::OperationInput + OperationOutput.add_member(:blob, ShapeRef.new(shape: Blob, member_name: 'blob')) + OperationOutput.add_member(:boolean, ShapeRef.new(shape: Boolean, member_name: 'boolean')) + OperationOutput.add_member(:string, ShapeRef.new(shape: String, member_name: 'string')) + OperationOutput.add_member(:byte, ShapeRef.new(shape: Byte, member_name: 'byte')) + OperationOutput.add_member(:short, ShapeRef.new(shape: Short, member_name: 'short')) + OperationOutput.add_member(:integer, ShapeRef.new(shape: Integer, member_name: 'integer')) + OperationOutput.add_member(:long, ShapeRef.new(shape: Long, member_name: 'long')) + OperationOutput.add_member(:float, ShapeRef.new(shape: Float, member_name: 'float')) + OperationOutput.add_member(:double, ShapeRef.new(shape: Double, member_name: 'double')) + OperationOutput.add_member(:big_integer, ShapeRef.new(shape: BigInteger, member_name: 'bigInteger')) + OperationOutput.add_member(:big_decimal, ShapeRef.new(shape: BigDecimal, member_name: 'bigDecimal')) + OperationOutput.add_member(:timestamp, ShapeRef.new(shape: Timestamp, member_name: 'timestamp')) + OperationOutput.add_member(:document, ShapeRef.new(shape: Document, member_name: 'document')) + OperationOutput.add_member(:enum, ShapeRef.new(shape: Enum, member_name: 'enum')) + OperationOutput.add_member(:int_enum, ShapeRef.new(shape: IntEnum, member_name: 'intEnum')) + OperationOutput.add_member(:list, ShapeRef.new(shape: List, member_name: 'list')) + OperationOutput.add_member(:map, ShapeRef.new(shape: Map, member_name: 'map')) + OperationOutput.add_member(:structure, ShapeRef.new(shape: Structure, member_name: 'structure')) + OperationOutput.add_member(:union, ShapeRef.new(shape: Union, member_name: 'union')) + OperationOutput.type = Types::OperationOutput Structure.add_member(:member, ShapeRef.new(shape: String, member_name: 'member', traits: {"smithy.ruby.tests#shape" => {}})) Structure.type = Types::Structure Union.add_member(:string, Types::Union::String, ShapeRef.new(shape: String, member_name: 'string', traits: {"smithy.ruby.tests#shape" => {}})) @@ -69,9 +90,8 @@ module Schema service.add_operation(:operation, OperationShape.new do |operation| operation.id = "smithy.ruby.tests#Operation" operation.name = "Operation" - operation.input = ShapeRef.new(shape: OperationInputOutput) - operation.output = ShapeRef.new(shape: OperationInputOutput) - # TODO: support parsing errors defined at the service level + operation.input = ShapeRef.new(shape: OperationInput) + operation.output = ShapeRef.new(shape: OperationOutput) operation.traits = {"smithy.ruby.tests#shape" => {}} end) end diff --git a/projections/shapes/lib/shapes/types.rb b/projections/shapes/lib/shapes/types.rb index f7fd863bc..b45e504d6 100644 --- a/projections/shapes/lib/shapes/types.rb +++ b/projections/shapes/lib/shapes/types.rb @@ -44,7 +44,69 @@ module Types # @return [Types::Structure] # @!attribute union # @return [Types::Union] - class OperationInputOutput < Struct.new( + class OperationInput < Struct.new( + :blob, + :boolean, + :string, + :byte, + :short, + :integer, + :long, + :float, + :double, + :big_integer, + :big_decimal, + :timestamp, + :document, + :enum, + :int_enum, + :list, + :map, + :structure, + :union, + keyword_init: true) + include Smithy::Schema::Structure + end + + # @!attribute blob + # @return [String] + # @!attribute boolean + # @return [Boolean] + # @!attribute string + # @return [String] + # @!attribute byte + # @return [Integer] + # @!attribute short + # @return [Integer] + # @!attribute integer + # @return [Integer] + # @!attribute long + # @return [Integer] + # @!attribute float + # @return [Float] + # @!attribute double + # @return [Float] + # @!attribute big_integer + # @return [Object] + # @!attribute big_decimal + # @return [Object] + # @!attribute timestamp + # @return [Time] + # @!attribute document + # @return [JSON] + # @!attribute enum + # @return [String] + # @!attribute int_enum + # @return [Integer] + # @!attribute list + # @return [Array] + # @!attribute map + # @return [Hash] + # @!attribute structure + # @return [Types::Structure] + # @!attribute union + # @return [Types::Union] + class OperationOutput < Struct.new( :blob, :boolean, :string, diff --git a/projections/shapes/sig/shapes/client.rbs b/projections/shapes/sig/shapes/client.rbs index 958968a88..1e96a16ee 100644 --- a/projections/shapes/sig/shapes/client.rbs +++ b/projections/shapes/sig/shapes/client.rbs @@ -38,7 +38,7 @@ module ShapeService ) -> void | (?Hash[Symbol, untyped]) -> void interface _OperationResponse - include Smithy::Client::_Output[Types::OperationInputOutput] + include Smithy::Client::_Output[Types::OperationOutput] def blob: () -> String? def boolean: () -> bool? def string: () -> String? @@ -48,8 +48,8 @@ module ShapeService def long: () -> Integer? def float: () -> Float? def double: () -> Float? - def big_integer: () -> untyped? - def big_decimal: () -> untyped? + def big_integer: () -> Integer? + def big_decimal: () -> BigDecimal? def timestamp: () -> Time? def document: () -> Smithy::Schema::document? def enum: () -> String? @@ -69,8 +69,8 @@ module ShapeService ?long: Integer, ?float: Float, ?double: Float, - ?big_integer: untyped, - ?big_decimal: untyped, + ?big_integer: Integer, + ?big_decimal: BigDecimal, ?timestamp: Time, ?document: Smithy::Schema::document, ?enum: String, diff --git a/projections/shapes/sig/shapes/schema.rbs b/projections/shapes/sig/shapes/schema.rbs index 31c4c8036..318814c33 100644 --- a/projections/shapes/sig/shapes/schema.rbs +++ b/projections/shapes/sig/shapes/schema.rbs @@ -16,7 +16,8 @@ module ShapeService List: ListShape Long: IntegerShape Map: MapShape - OperationInputOutput: StructureShape + OperationInput: StructureShape + OperationOutput: StructureShape Short: IntegerShape String: StringShape Structure: StructureShape diff --git a/projections/shapes/sig/shapes/types.rbs b/projections/shapes/sig/shapes/types.rbs index 6984563e5..fcd053f1e 100644 --- a/projections/shapes/sig/shapes/types.rbs +++ b/projections/shapes/sig/shapes/types.rbs @@ -1,7 +1,7 @@ module ShapeService module Types - class OperationInputOutput + class OperationInput include Smithy::Schema::Structure def initialize: ( @@ -14,8 +14,8 @@ module ShapeService ?long: Integer, ?float: Float, ?double: Float, - ?big_integer: untyped, - ?big_decimal: untyped, + ?big_integer: Integer, + ?big_decimal: BigDecimal, ?timestamp: Time, ?document: Smithy::Schema::document, ?enum: String, @@ -36,8 +36,55 @@ module ShapeService attr_accessor long: Integer? attr_accessor float: Float? attr_accessor double: Float? - attr_accessor big_integer: untyped? - attr_accessor big_decimal: untyped? + attr_accessor big_integer: Integer? + attr_accessor big_decimal: BigDecimal? + attr_accessor timestamp: Time? + attr_accessor document: Smithy::Schema::document? + attr_accessor enum: String? + attr_accessor int_enum: Integer? + attr_accessor list: Array[String]? + attr_accessor map: Hash[String, String]? + attr_accessor structure: Types::Structure? + attr_accessor union: Types::Union? + end + + class OperationOutput + include Smithy::Schema::Structure + + def initialize: ( + ?blob: String, + ?boolean: bool, + ?string: String, + ?byte: Integer, + ?short: Integer, + ?integer: Integer, + ?long: Integer, + ?float: Float, + ?double: Float, + ?big_integer: Integer, + ?big_decimal: BigDecimal, + ?timestamp: Time, + ?document: Smithy::Schema::document, + ?enum: String, + ?int_enum: Integer, + ?list: Array[String], + ?map: Hash[String, String], + ?structure: Types::Structure, + ?union: Types::Union, + ) -> void + | (?Hash[Symbol, untyped]) -> void + + attr_accessor blob: String? + attr_accessor boolean: bool? + attr_accessor string: String? + attr_accessor byte: Integer? + attr_accessor short: Integer? + attr_accessor integer: Integer? + attr_accessor long: Integer? + attr_accessor float: Float? + attr_accessor double: Float? + attr_accessor big_integer: Integer? + attr_accessor big_decimal: BigDecimal? attr_accessor timestamp: Time? attr_accessor document: Smithy::Schema::document? attr_accessor enum: String? diff --git a/projections/weather/lib/weather/schema.rb b/projections/weather/lib/weather/schema.rb index 683bfe12d..1e2fce736 100644 --- a/projections/weather/lib/weather/schema.rb +++ b/projections/weather/lib/weather/schema.rb @@ -57,7 +57,6 @@ module Schema operation.name = "GetCity" operation.input = ShapeRef.new(shape: GetCityInput) operation.output = ShapeRef.new(shape: GetCityOutput) - # TODO: support parsing errors defined at the service level operation.errors << ShapeRef.new(shape: NoSuchResource) operation.traits = {"smithy.api#readonly" => {}} end) @@ -66,7 +65,6 @@ module Schema operation.name = "GetCurrentTime" operation.input = ShapeRef.new(shape: Prelude::Unit) operation.output = ShapeRef.new(shape: GetCurrentTimeOutput) - # TODO: support parsing errors defined at the service level operation.traits = {"smithy.api#readonly" => {}} end) service.add_operation(:get_forecast, OperationShape.new do |operation| @@ -74,7 +72,6 @@ module Schema operation.name = "GetForecast" operation.input = ShapeRef.new(shape: GetForecastInput) operation.output = ShapeRef.new(shape: GetForecastOutput) - # TODO: support parsing errors defined at the service level operation.traits = {"smithy.api#readonly" => {}} end) service.add_operation(:list_cities, OperationShape.new do |operation| @@ -82,7 +79,6 @@ module Schema operation.name = "ListCities" operation.input = ShapeRef.new(shape: ListCitiesInput) operation.output = ShapeRef.new(shape: ListCitiesOutput) - # TODO: support parsing errors defined at the service level operation.traits = {"smithy.api#readonly" => {}} operation[:paginator] = Paginators::ListCities.new end)