Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gems/smithy-client/lib/smithy-client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
require_relative 'smithy-client/param_validator'
require_relative 'smithy-client/plugin'
require_relative 'smithy-client/plugin_list'
require_relative 'smithy-client/protocol'
require_relative 'smithy-client/no_op_protocol'
require_relative 'smithy-client/rpc_v2_cbor'
require_relative 'smithy-client/retry'
require_relative 'smithy-client/service_error'
require_relative 'smithy-client/util'
Expand Down
45 changes: 37 additions & 8 deletions gems/smithy-client/lib/smithy-client/no_op_protocol.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,48 @@
# frozen_string_literal: true

require_relative 'protocol'

module Smithy
module Client
# Default protocol used when no protocol is registered for a client. All
# methods are no-ops, so build/parse handlers can delegate safely without
# a nil check.
# Default protocol used when no protocol is registered for a client.
# Also documents the protocol interface: a protocol serializes requests,
# deserializes responses, and builds stubbed responses for a specific
# wire format. A custom protocol passed via +Client.new(protocol:)+ is
# any object that responds to these methods.
#
# All methods are no-ops, so handlers and stubbing can delegate safely
# without a nil check.
# @api private
class NoOpProtocol < Protocol
class NoOpProtocol
# Serialize the request into the wire format.
# @param [HandlerContext] _context
def build_request(_context); end

def parse_data(_response); end
# Deserialize a successful response body.
# @param [HandlerContext] _context
# @return [Object, nil] the response data
def parse_data(_context); end

# Deserialize an error response into the modeled error. Called on
# every response; must return nil when the response is not an error.
# @param [HandlerContext] _context
# @return [StandardError, nil]
def parse_error(_context); end

# Build a stubbed HTTP response for the given output data.
# @param [Configuration] _config
# @param [Schema::OperationShape] _operation
# @param [Object] _data
# @return [Http::Response]
def stub_data(_config, _operation, _data)
Http::Response.new
end

def parse_error(_response); end
# Build a stubbed HTTP error response for the given error code.
# @param [Configuration] _config
# @param [String] _error_code
# @return [Http::Response]
def stub_error(_config, _error_code)
Http::Response.new
end
end
end
end
8 changes: 5 additions & 3 deletions gems/smithy-client/lib/smithy-client/plugins/protocol.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# frozen_string_literal: true

require_relative '../protocol'
require_relative '../no_op_protocol'

module Smithy
Expand Down Expand Up @@ -28,12 +27,15 @@ def call(context)
end
end

# Parses the response after the send handler returns. Send handlers
# must fully signal the response body (signal_done) before returning,
# since parsing happens inline here rather than in a body callback.
# @api private
class ParseHandler < Handler
def call(context)
response = @handler.call(context)
response.error = context.config.protocol.parse_error(response) unless response.error
response.data = context.config.protocol.parse_data(response) unless response.error
response.error = context.config.protocol.parse_error(context) unless response.error
response.data = context.config.protocol.parse_data(context) unless response.error
response
end
end
Expand Down
21 changes: 0 additions & 21 deletions gems/smithy-client/lib/smithy-client/plugins/rpc_v2_cbor.rb

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require_relative '../no_op_protocol'

module Smithy
module Client
module Plugins
Expand All @@ -19,7 +21,9 @@ class StubResponses < Plugin
option(:stubs_mutex) { Mutex.new }
option(:api_requests) { [] }
option(:api_requests_mutex) { Mutex.new }
option(:stubber) { Stubbing::NullProtocol.new }
# Fallback so stubbing works on clients without the Protocol plugin.
# The Protocol plugin's before_initialize overrides this when present.
option(:protocol) { NoOpProtocol.new }

def add_handlers(handlers, config)
return unless config.stub_responses
Expand Down
31 changes: 0 additions & 31 deletions gems/smithy-client/lib/smithy-client/protocol.rb

This file was deleted.

185 changes: 185 additions & 0 deletions gems/smithy-client/lib/smithy-client/rpc_v2_cbor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# frozen_string_literal: true

module Smithy
module Client
# Protocol implementation for Smithy RPC v2 CBOR. Serializes requests and
# deserializes responses (and errors) for the +smithy.protocols#rpcv2Cbor+
# wire format, and builds stubbed responses for testing.
# @api private
class RpcV2Cbor
def initialize
@codec = Smithy::Cbor::Codec.new
end

# Serialize the request into the RPC v2 CBOR wire format.
# @param [HandlerContext] context
def build_request(context)
context.http_request.http_method = 'POST'
apply_headers(context)
apply_body(context)
apply_url_path(context)
end

# Deserialize a successful response body.
# @param [HandlerContext] context
# @return [Object] the parsed output data
def parse_data(context)
@codec.parse(context.operation.output, context.http_response.body.read)
end

# Deserialize an error response into the modeled error. Called on every
# response; returns nil when the response is not an error.
# @param [HandlerContext] context
# @return [StandardError, nil]
def parse_error(context)
# Only inspect responses in the HTTP status range. Outside it (e.g. a
# status 0 from a signaled transport error), leave the response alone
# so the transport error propagates untouched. Mirrors the old
# ErrorHandler's on_done(200..599) gate.
return unless (200..599).cover?(context.http_response.status_code)

# Malformed responses should raise an http-based error, so we validate
# the protocol header across the full 200..599 range.
unless valid_response?(context)
code, data = http_status_error(context)
return build_error(context, code, data)
end
return unless (400..599).cover?(context.http_response.status_code)

error(context)
end

# Build a stubbed HTTP response for the given output data.
# @param [Configuration] _config
# @param [Schema::OperationShape] operation
# @param [Object] data
# @return [Http::Response]
def stub_data(_config, operation, data)
response = Http::Response.new
response.status_code = 200
response.headers['Smithy-Protocol'] = 'rpc-v2-cbor'
response.headers['Content-Type'] = 'application/cbor'
response.body = @codec.build(operation.output, data)
response
end

# Build a stubbed HTTP error response for the given error code.
# @param [Configuration] _config
# @param [String] error_code
# @return [Http::Response]
def stub_error(_config, error_code)
response = Http::Response.new
response.status_code = 400
response.headers['Smithy-Protocol'] = 'rpc-v2-cbor'
response.headers['Content-Type'] = 'application/cbor'
data = { '__type' => "smithy.ruby.tests##{error_code}", 'message' => 'stubbed-error-message' }
response.body = Cbor.encode(data)
response
end

private

def apply_headers(context)
context.http_request.headers['Smithy-Protocol'] = 'rpc-v2-cbor'
apply_content_type_header(context)
apply_accept_header(context)
end

def apply_content_type_header(context)
input = context.operation.input
content_type =
if event_stream?(input)
'application/vnd.amazon.eventstream'
elsif input != Schema::Shapes::Prelude::Unit
'application/cbor'
end

context.http_request.headers['Content-Type'] ||= content_type if content_type
end

def apply_accept_header(context)
accept =
if event_stream?(context.operation.output)
'application/vnd.amazon.eventstream'
else
'application/cbor'
end

context.http_request.headers['Accept'] ||= accept
end

def apply_body(context)
context.http_request.body = @codec.build(context.operation.input, context.params)
end

def apply_url_path(context)
base = context.http_request.endpoint
service_name = context.config.service.name
base.path += "/service/#{service_name}/operation/#{context.operation.name}"
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
end

def valid_response?(context)
req_header = context.http_request.headers['smithy-protocol']
resp_header = context.http_response.headers['smithy-protocol']
req_header == resp_header
end

def error(context)
body = context.http_response.body.read
code, data =
if body.empty?
http_status_error(context)
else
extract_error(body, context)
end
build_error(context, code, data)
end

def extract_error(body, context)
data = Cbor.decode(body)
code = error_code(context, data)
data = parse_error_data(context, body, code)
[code, data]
rescue Cbor::ParseError
[http_status_error_code(context), Schema::EmptyStructure.new]
end

def parse_error_data(context, body, code)
data = Schema::EmptyStructure.new
context.operation.errors.each do |err_shape|
next unless err_shape.name == code

data = Cbor::Parser.new.parse(err_shape, body, err_shape.type.new)
end
data
end

def error_code(context, data)
code = data['__type']
code ||= http_status_error_code(context)
code.split('#').last.split('$').first
end

def build_error(context, code, data)
errors_module = context.client.class.errors_module
errors_module.error_class(code).new(context, data)
end

def http_status_error(context)
[http_status_error_code(context), Schema::EmptyStructure.new]
end

def http_status_error_code(context)
"HTTP#{context.http_response.status_code}Error"
end
end
end
end
Loading
Loading