diff --git a/gems/smithy-client/lib/smithy-client/default_params.rb b/gems/smithy-client/lib/smithy-client/default_params.rb index ea4105b32..00170731f 100644 --- a/gems/smithy-client/lib/smithy-client/default_params.rb +++ b/gems/smithy-client/lib/smithy-client/default_params.rb @@ -7,8 +7,6 @@ module Smithy module Client # @api private class DefaultParams - include Schema::Shapes - def initialize(shape) @shape = shape end @@ -22,10 +20,10 @@ def apply(params) private def apply_shape(shape, value) - case shape.target - when ListShape then list(shape, value) - when MapShape then map(shape, value) - when StructureShape then structure(shape, value) + case Schema::Extension.target_shape(shape) + when Schema::Extension::SHAPE_LIST then list(shape, value) + when Schema::Extension::SHAPE_MAP then map(shape, value) + when Schema::Extension::SHAPE_STRUCTURE then structure(shape, value) else value end end @@ -33,7 +31,7 @@ def apply_shape(shape, value) def list(shape, values) return if values.nil? - member = shape.target.member + member, = Schema::Extension.list_member(shape.target) values.each do |value| apply_shape(member, value) end @@ -43,7 +41,7 @@ def list(shape, values) def map(shape, values) return if values.nil? - value_shape = shape.target.value + value_shape, = Schema::Extension.map_value_member(shape.target) values.each_pair do |_key, value| apply_shape(value_shape, value) end @@ -72,9 +70,9 @@ def default?(shape, traits) def default(member_shape) default = member_shape.traits['smithy.api#default'] - case member_shape.target - when BlobShape then Base64.strict_decode64(default) - when TimestampShape then timestamp_default(default) + case Schema::Extension.target_shape(member_shape) + when Schema::Extension::SHAPE_BLOB then Base64.strict_decode64(default) + when Schema::Extension::SHAPE_TIMESTAMP then timestamp_default(default) else default end end diff --git a/gems/smithy-client/lib/smithy-client/param_converter.rb b/gems/smithy-client/lib/smithy-client/param_converter.rb index ea2d71cb3..6cca6ac3d 100644 --- a/gems/smithy-client/lib/smithy-client/param_converter.rb +++ b/gems/smithy-client/lib/smithy-client/param_converter.rb @@ -40,11 +40,11 @@ def c(shape, value) end def convert_shape(shape, value) - case shape.target - when ListShape then list(shape, value) - when MapShape then map(shape, value) - when StructureShape then structure(shape, value) - when UnionShape then union(shape, value) + case Schema::Extension.target_shape(shape) + when Schema::Extension::SHAPE_LIST then list(shape, value) + when Schema::Extension::SHAPE_MAP then map(shape, value) + when Schema::Extension::SHAPE_STRUCTURE then structure(shape, value) + when Schema::Extension::SHAPE_UNION then union(shape, value) else c(shape, value) end end @@ -53,15 +53,18 @@ def list(shape, values) values = c(shape, values) return values unless values.is_a?(Array) - values.collect { |v| convert_shape(shape.target.member, v) } + member, = Schema::Extension.list_member(shape.target) + values.collect { |v| convert_shape(member, v) } end def map(shape, values) values = c(shape, values) return values unless values.is_a?(Hash) + key_member, = Schema::Extension.map_key_member(shape.target) + value_member, = Schema::Extension.map_value_member(shape.target) values.each.with_object({}) do |(key, value), hash| - hash[convert_shape(shape.target.key, key)] = convert_shape(shape.target.value, value) + hash[convert_shape(key_member, key)] = convert_shape(value_member, value) 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 8ade44877..1c1427e0e 100644 --- a/gems/smithy-client/lib/smithy-client/param_validator.rb +++ b/gems/smithy-client/lib/smithy-client/param_validator.rb @@ -27,47 +27,46 @@ def validate!(params, context: 'params') private - # rubocop:disable-next Metrics + # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity def validate_shape(shape, value, errors, context) - case shape.target - when StructureShape then structure(shape, value, errors, context) - when ListShape then list(shape, value, errors, context) - when MapShape then map(shape, value, errors, context) - when DocumentShape then document(shape, value, errors, context) - when UnionShape then union(shape, value, errors, context) - when StringShape, EnumShape + case Schema::Extension.target_shape(shape) + when Schema::Extension::SHAPE_STRUCTURE then structure(shape, value, errors, context) + when Schema::Extension::SHAPE_LIST then list(shape, value, errors, context) + when Schema::Extension::SHAPE_MAP then map(shape, value, errors, context) + when Schema::Extension::SHAPE_DOCUMENT then document(shape, value, errors, context) + when Schema::Extension::SHAPE_UNION then union(shape, value, errors, context) + when Schema::Extension::SHAPE_STRING, Schema::Extension::SHAPE_ENUM errors << expected_got(context, 'a String', value) unless value.is_a?(String) - when IntegerShape, IntEnumShape + when Schema::Extension::SHAPE_INTEGER, Schema::Extension::SHAPE_INT_ENUM errors << expected_got(context, 'an Integer', value) unless value.is_a?(Integer) - when BigDecimalShape + when Schema::Extension::SHAPE_BIG_DECIMAL errors << expected_got(context, 'a BigDecimal', value) unless value.is_a?(BigDecimal) - when FloatShape + when Schema::Extension::SHAPE_FLOAT errors << expected_got(context, 'a Float', value) unless value.is_a?(Float) - when TimestampShape + when Schema::Extension::SHAPE_TIMESTAMP errors << expected_got(context, 'a Time object', value) unless value.is_a?(Time) - when BooleanShape + when Schema::Extension::SHAPE_BOOLEAN errors << expected_got(context, 'true or false', value) unless [true, false].include?(value) - when BlobShape - unless value.is_a?(String) - if streaming_input?(shape) - unless io_like?(value) - errors << expected_got( - context, - 'a String or IO like object that supports read and rewind', - value - ) - end - elsif !io_like?(value, require_size: true) - errors << expected_got( - context, - 'a String or IO like object that supports read, rewind, and size', - value - ) - end - end + when Schema::Extension::SHAPE_BLOB + blob(shape, value, errors, context) end end + def blob(shape, value, errors, context) + return if value.is_a?(String) + + streaming = streaming_input?(shape) + return if io_like?(value, require_size: !streaming) + + expected = + if streaming + 'a String or IO like object that supports read and rewind' + else + 'a String or IO like object that supports read, rewind, and size' + end + errors << expected_got(context, expected, value) + end + def document(shape, value, errors, context) document_types = [Hash, Array, Numeric, String, TrueClass, FalseClass, NilClass] unless document_types.any? { |t| value.is_a?(t) } @@ -92,10 +91,11 @@ def list(shape, values, errors, context) return end + member, = Schema::Extension.list_member(shape.target) values.each.with_index do |value, index| next unless value - validate_shape(shape.target.member, value, errors, context + "[#{index}]") + validate_shape(member, value, errors, context + "[#{index}]") end end @@ -105,11 +105,13 @@ def map(shape, values, errors, context) return end + key_member, = Schema::Extension.map_key_member(shape.target) + value_member, = Schema::Extension.map_value_member(shape.target) values.each do |key, value| - validate_shape(shape.target.key, key, errors, "#{context} #{key.inspect} key") + validate_shape(key_member, key, errors, "#{context} #{key.inspect} key") next unless value - validate_shape(shape.target.value, value, errors, context + "[#{key.inspect}]") + validate_shape(value_member, value, errors, context + "[#{key.inspect}]") end end @@ -174,10 +176,7 @@ def valid_union?(shape, values, errors, context) end def validate_required_members(shape, values, errors, context) - shape.target.members.each do |name, member_shape| - traits = member_shape.traits - next unless traits.key?('smithy.api#required') && !traits.key?('smithy.api#clientOptional') - + Schema::Extension.required_members(shape.target).each do |name| if values[name].nil? param = "#{context}[#{name.inspect}]" errors << "missing required parameter #{param}" @@ -186,7 +185,7 @@ def validate_required_members(shape, values, errors, context) end def streaming_input?(shape) - shape.target.traits.key?('smithy.api#streaming') + Schema::Extension.streaming?(shape.target) end def io_like?(value, require_size: false) diff --git a/gems/smithy-client/lib/smithy-client/plugins/checksum_required.rb b/gems/smithy-client/lib/smithy-client/plugins/checksum_required.rb index fa16f5473..fba704655 100644 --- a/gems/smithy-client/lib/smithy-client/plugins/checksum_required.rb +++ b/gems/smithy-client/lib/smithy-client/plugins/checksum_required.rb @@ -26,7 +26,7 @@ def call(context) private def checksum_required_operation?(context) - context.operation.traits.key?('smithy.api#httpChecksumRequired') + Schema::Extension.checksum_required?(context.operation) end def md5(value) diff --git a/gems/smithy-client/lib/smithy-client/plugins/host_prefix.rb b/gems/smithy-client/lib/smithy-client/plugins/host_prefix.rb index b87f33304..8b3bad6a9 100644 --- a/gems/smithy-client/lib/smithy-client/plugins/host_prefix.rb +++ b/gems/smithy-client/lib/smithy-client/plugins/host_prefix.rb @@ -33,7 +33,7 @@ def add_handlers(handlers, config) # @api private class Handler < Smithy::Client::Handler def call(context) - host_prefix = context.operation.traits.dig('smithy.api#endpoint', 'hostPrefix') + host_prefix = Schema::Extension.endpoint_host_prefix(context.operation) apply_host_prefix(context, host_prefix) if host_prefix @handler.call(context) end @@ -42,21 +42,15 @@ def call(context) # TODO: optimize this to collect all labels in one pass def apply_host_prefix(context, host_prefix) - input = context.operation.input + host_labels = Schema::Extension.host_label_index(context.operation.input) prefix = host_prefix.gsub(/\{.+?}/) do |label| - label_value(input, label.delete('{}'), context.params) + label_value(host_labels, label.delete('{}'), context.params) end context.http_request.endpoint.host = prefix + context.http_request.endpoint.host end - def label_value(input, label, params) - name = nil - input.members.each do |member_name, member_shape| - next unless member_shape.traits.key?('smithy.api#hostLabel') - next unless member_shape.name == label - - name = member_name - end + def label_value(host_labels, label, params) + name = host_labels[label] raise ArgumentError, "#{label} is not a valid host label" if name.nil? raise ArgumentError, "params[:#{name}] must not be nil or blank" if params[name].nil? || params[name].empty? diff --git a/gems/smithy-client/lib/smithy-client/plugins/idempotency_token.rb b/gems/smithy-client/lib/smithy-client/plugins/idempotency_token.rb index 1fc7989f7..546492aa2 100644 --- a/gems/smithy-client/lib/smithy-client/plugins/idempotency_token.rb +++ b/gems/smithy-client/lib/smithy-client/plugins/idempotency_token.rb @@ -21,11 +21,10 @@ def call(context) private def apply_idempotency_token(input, params) - input.members.each do |member_name, member_shape| - next unless member_shape.traits.key?('smithy.api#idempotencyToken') + member_name = Schema::Extension.idempotency_token_member(input) + return unless member_name - params[member_name] ||= SecureRandom.uuid - end + params[member_name] ||= SecureRandom.uuid end end end diff --git a/gems/smithy-client/lib/smithy-client/plugins/request_compression.rb b/gems/smithy-client/lib/smithy-client/plugins/request_compression.rb index 6e4d23573..427c6f62c 100644 --- a/gems/smithy-client/lib/smithy-client/plugins/request_compression.rb +++ b/gems/smithy-client/lib/smithy-client/plugins/request_compression.rb @@ -67,14 +67,12 @@ def add_handlers(handlers, config) # @api private class Handler < Client::Handler def call(context) - if request_compression_trait?(context) - selected_encoding = request_encoding_selection(context) - if selected_encoding - if streaming?(context.operation.input) - process_streaming_compression(selected_encoding, context) - elsif context.http_request.body.size >= context.config.request_min_compression_size_bytes - process_compression(selected_encoding, context) - end + selected_encoding = request_encoding_selection(context) + if selected_encoding + if streaming?(context.operation.input) + process_streaming_compression(selected_encoding, context) + elsif context.http_request.body.size >= context.config.request_min_compression_size_bytes + process_compression(selected_encoding, context) end end track_feature(selected_encoding) { @handler.call(context) } @@ -82,20 +80,13 @@ def call(context) private - def request_compression_trait?(context) - context.operation.traits.key?('smithy.api#requestCompression') - end - def request_encoding_selection(context) - encodings = context.operation.traits['smithy.api#requestCompression']['encodings'] - encodings.find { |encoding| RequestCompression::SUPPORTED_ENCODINGS.include?(encoding) } + encodings = Schema::Extension.request_compression_encodings(context.operation) + encodings&.find { |encoding| RequestCompression::SUPPORTED_ENCODINGS.include?(encoding) } end def streaming?(input) - input.members.any? do |_, member_shape| - member_shape.target.traits.key?('smithy.api#streaming') && - !member_shape.target.traits.key?('smithy.api#requiresLength') - end + Schema::Extension.streaming_member_unknown_length(input) end def process_streaming_compression(encoding, context) diff --git a/gems/smithy-client/lib/smithy-client/plugins/retry_errors.rb b/gems/smithy-client/lib/smithy-client/plugins/retry_errors.rb index 01730a060..df1589159 100644 --- a/gems/smithy-client/lib/smithy-client/plugins/retry_errors.rb +++ b/gems/smithy-client/lib/smithy-client/plugins/retry_errors.rb @@ -153,7 +153,10 @@ def reset_response(context, response) # TODO: Revisit after trait is finalized. def long_polling_operation?(context) - context.operation.traits.key?('smithy.api#longPoll') + operation = context.operation + return operation.traits.key?('smithy.api#longPoll') unless operation.respond_to?(:key?) + + Schema::Extension.long_polling?(operation) end def track_feature(retry_strategy, &block) diff --git a/gems/smithy-client/lib/smithy-client/plugins/transfer_encoding.rb b/gems/smithy-client/lib/smithy-client/plugins/transfer_encoding.rb index 7c34ba8ee..9330d9806 100644 --- a/gems/smithy-client/lib/smithy-client/plugins/transfer_encoding.rb +++ b/gems/smithy-client/lib/smithy-client/plugins/transfer_encoding.rb @@ -29,17 +29,15 @@ def apply_transfer_encoding(context, payload_shape) end def streaming_member(context) - context.operation.input.members.detect do |_, member_shape| - member_shape.target.traits.key?('smithy.api#streaming') - end&.last + Schema::Extension.streaming_member(context.operation.input) end def requires_length?(shape) - shape.traits.key?('smithy.api#requiresLength') + Schema::Extension.requires_length?(shape) end def unsigned_payload?(context) - context.operation.traits.key?('aws.auth#unsignedPayload') + Schema::Extension.unsigned_payload?(context.operation) end end diff --git a/gems/smithy-client/lib/smithy-client/rpc_v2_cbor.rb b/gems/smithy-client/lib/smithy-client/rpc_v2_cbor.rb index e6a61b0ba..343f5910a 100644 --- a/gems/smithy-client/lib/smithy-client/rpc_v2_cbor.rb +++ b/gems/smithy-client/lib/smithy-client/rpc_v2_cbor.rb @@ -115,11 +115,7 @@ def apply_url_path(context) end def event_stream?(input_shape) - input_shape.members.each_value do |member_shape| - shape = member_shape.target - return true if shape.traits.key?('smithy.api#streaming') && shape.is_a?(Schema::Shapes::UnionShape) - end - false + Schema::Extension.event_stream_member(input_shape) end def valid_response?(context) diff --git a/gems/smithy-schema/lib/smithy-schema/extension.rb b/gems/smithy-schema/lib/smithy-schema/extension.rb index 5d0a2c281..cdfb16165 100644 --- a/gems/smithy-schema/lib/smithy-schema/extension.rb +++ b/gems/smithy-schema/lib/smithy-schema/extension.rb @@ -169,6 +169,15 @@ def unsigned_payload?(operation) fetch(operation)[:unsigned_payload] end + # Returns operation errors indexed by target shape name. + # + # Example: + # Extension.error_index(operation)['ResourceNotFound'] + # # => error_member + def error_index(operation) + fetch(operation).fetch(:error_index, {}.freeze) + end + def required_members(shape) fetch(shape).fetch(:required_members, [].freeze) end @@ -247,10 +256,17 @@ def build_operation_metadata(operation) request_compression_encodings: traits.dig('smithy.api#requestCompression', 'encodings'), checksum_required: traits.key?('smithy.api#httpChecksumRequired') || nil, long_polling: traits.key?('smithy.api#longPoll') || nil, - unsigned_payload: traits.key?('aws.auth#unsignedPayload') || nil + unsigned_payload: traits.key?('aws.auth#unsignedPayload') || nil, + error_index: build_error_index(operation) }.compact.freeze end + def build_error_index(operation) + operation.errors.each_with_object({}) do |error, index| + index[error.target.name] = error if error.target&.name + end.freeze + end + def build_shape_metadata(shape) target = shape.target target_shape = SHAPE_REF_BY_CLASS[target.class] diff --git a/gems/smithy-schema/spec/smithy-schema/extension_spec.rb b/gems/smithy-schema/spec/smithy-schema/extension_spec.rb index 0b554ed40..46e96d2ed 100644 --- a/gems/smithy-schema/spec/smithy-schema/extension_spec.rb +++ b/gems/smithy-schema/spec/smithy-schema/extension_spec.rb @@ -60,6 +60,16 @@ module Schema end end + describe '.error_index' do + it 'indexes operation errors by their target shape name' do + error_shape = Shapes::StructureShape.new(name: 'ExampleError') + error_member = Shapes::MemberShape.new(target: error_shape) + operation = Shapes::OperationShape.new(errors: [error_member]) + + expect(described_class.error_index(operation)).to eq('ExampleError' => error_member) + end + end + describe 'generic shape metadata' do it 'classifies target shapes' do expect(described_class.target_shape(Shapes::BlobShape.new)).to eq(described_class::SHAPE_BLOB)