Skip to content
Open
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: 2 additions & 0 deletions gems/smithy-client/lib/smithy-client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +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/retry'
require_relative 'smithy-client/service_error'
require_relative 'smithy-client/util'
Expand Down
19 changes: 19 additions & 0 deletions gems/smithy-client/lib/smithy-client/no_op_protocol.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# 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.
# @api private
class NoOpProtocol < Protocol
def build_request(_context); end

def parse_data(_response); end

def parse_error(_response); end
end
end
end
68 changes: 68 additions & 0 deletions gems/smithy-client/lib/smithy-client/plugins/protocol.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# frozen_string_literal: true

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

module Smithy
module Client
module Plugins
# Generic protocol plugin. Resolves the +:protocol+ option and installs
# build/parse handlers that delegate serialization and deserialization to
# the resolved protocol instance.
# @api private
class Protocol < Plugin
option(
:protocol,
doc_type: 'Symbol, Object',
docstring: 'The protocol used for request serialization and response ' \
'deserialization. Defaults to the service default protocol. ' \
'May be a Symbol naming a supported protocol, or a custom ' \
'object implementing the protocol interface.'
)

# @api private
class BuildHandler < Handler
def call(context)
context.config.protocol.build_request(context)
@handler.call(context)
end
end

# @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
end
end

def add_handlers(handlers, _config)
handlers.add(BuildHandler)
handlers.add(ParseHandler)
end

def before_initialize(client_class, options)
case options[:protocol]
when nil
options[:protocol] = client_class.protocols.values.first&.new || NoOpProtocol.new
when Symbol
options[:protocol] = resolve!(client_class, options[:protocol])
# else: custom protocol instance, used as-is
end
end

private

# TODO: client class to have prioritized list
def resolve!(client_class, name)
protocol_class = client_class.protocols[name]
raise ArgumentError, "Unknown protocol: #{name}" unless protocol_class

protocol_class.new
end
end
end
end
end
31 changes: 31 additions & 0 deletions gems/smithy-client/lib/smithy-client/protocol.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

module Smithy
module Client
# Base class documenting the protocol interface. A protocol serializes
# requests and deserializes responses for a specific wire format.
#
# A custom protocol passed via +Client.new(protocol:)+ is any object that
# responds to these methods; it need not inherit from this class.
# @api private
class Protocol
# Serialize the request into the wire format.
# @param [Interceptor::Context] _context
def build_request(_context)
raise NotImplementedError
end

# Deserialize a successful response body.
# @param [Response] _response
def parse_data(_response)
raise NotImplementedError
end

# Deserialize an error response into the modeled error.
# @param [Response] _response
def parse_error(_response)
raise NotImplementedError
end
end
end
end
120 changes: 120 additions & 0 deletions gems/smithy-client/spec/smithy-client/plugins/protocol_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# frozen_string_literal: true

require_relative '../../spec_helper'

require 'smithy-client/plugins/protocol'

module Smithy
module Client
module Plugins
describe Protocol do
let(:sample_client) { ClientHelper.sample_client }
let(:client_class) do
client_class = sample_client.const_get(:Client)
client_class.clear_plugins
client_class.add_plugin(sample_client::Plugins::Endpoint)
client_class.add_plugin(Protocol)
client_class
end
let(:client_options) { { endpoint: 'https://example.com' } }

let(:fake_protocol_class) do
Class.new(Client::Protocol) do
def build_request(_context); end
def parse_data(_response); end
def parse_error(_response); end
end
end

# TODO: remove this test-only registry once the `protocols` weld is impl
before do
protocols = { rpc_v2_cbor: fake_protocol_class }
client_class.define_singleton_method(:protocols) { protocols }
end

it 'adds a :protocol option to config' do
client = client_class.new(client_options)
expect(client.config).to respond_to(:protocol)
end

context 'when :protocol is not provided (default)' do
it 'defaults to an instance of the first registered protocol' do
client = client_class.new(client_options)
expect(client.config.protocol).to be_a(fake_protocol_class)
end

it 'falls back to NoOpProtocol when no protocol is registered' do
client_class.define_singleton_method(:protocols) { {} }
client = client_class.new(client_options)
expect(client.config.protocol).to be_a(Client::NoOpProtocol)
end
end

context 'when :protocol is a Symbol' do
it 'resolves to an instance of the named protocol' do
client = client_class.new(client_options.merge(protocol: :rpc_v2_cbor))
expect(client.config.protocol).to be_a(fake_protocol_class)
end

it 'raises an ArgumentError for an unknown protocol' do
expect { client_class.new(client_options.merge(protocol: :nope)) }
.to raise_error(ArgumentError, /Unknown protocol: nope/)
end
end

context 'when :protocol is a custom object' do
it 'uses the provided object as-is' do
custom = fake_protocol_class.new
client = client_class.new(client_options.merge(protocol: custom))
expect(client.config.protocol).to be(custom)
end
end

it 'adds the build and parse handlers' do
client = client_class.new(client_options)
expect(client.handlers).to include(Protocol::BuildHandler)
expect(client.handlers).to include(Protocol::ParseHandler)
end
end

describe Client::Protocol do
subject(:protocol) { described_class.new }

it 'raises NotImplementedError for #build_request' do
expect { protocol.build_request(double('context')) }
.to raise_error(NotImplementedError)
end

it 'raises NotImplementedError for #parse_data' do
expect { protocol.parse_data(double('response')) }
.to raise_error(NotImplementedError)
end

it 'raises NotImplementedError for #parse_error' do
expect { protocol.parse_error(double('response')) }
.to raise_error(NotImplementedError)
end
end

describe Client::NoOpProtocol do
subject(:protocol) { described_class.new }

it 'is a Protocol' do
expect(protocol).to be_a(Client::Protocol)
end

it 'returns nil from #build_request without raising' do
expect(protocol.build_request(double('context'))).to be_nil
end

it 'returns nil from #parse_data without raising' do
expect(protocol.parse_data(double('response'))).to be_nil
end

it 'returns nil from #parse_error without raising' do
expect(protocol.parse_error(double('response'))).to be_nil
end
end
end
end
end