Skip to content

Commit

Permalink
Working error parsing and stubs
Browse files Browse the repository at this point in the history
  • Loading branch information
mullermp committed Feb 26, 2025
1 parent 3ab50e5 commit 1d222f6
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 100 deletions.
1 change: 0 additions & 1 deletion gems/smithy-client/lib/smithy-client/http/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ def initialize(options = {})
@body = options[:body] || StringIO.new
@listeners = Hash.new { |h, k| h[k] = [] }

@complete = false
@done = nil
@error = nil
end
Expand Down
46 changes: 39 additions & 7 deletions gems/smithy-client/lib/smithy-client/protocols/rpc_v2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ def initialize(options = {})
@query_compatible = options[:query_compatible]
end

# @api private
def build(context)
codec = Codecs::CBOR.new(setting(context))
context.request.body = codec.serialize(context.params, context.operation.input)
Expand All @@ -26,16 +25,19 @@ def build(context)
build_url(context)
end

# @api private
def parse(context)
output_shape = context.operation.output
codec = Codecs::CBOR.new(setting(context))
codec.deserialize(context.response.body.read, output_shape)
end

# @api private
# TODO: To implement after error handling
def error(_context, _response); end
def error(context)
code, message, data = extract_error(context)
if code
errors_module = context.client.class.errors_module
errors_module.error_class(code).new(context, message, data)
end
end

def stub_data(operation, data)
resp = HTTP::Response.new
Expand All @@ -47,17 +49,47 @@ def stub_data(operation, data)
resp
end

def stub_error(error_code)
def stub_error(operation, error_code)
resp = HTTP::Response.new
resp.status_code = 400
resp.headers['Smithy-Protocol'] = 'rpc-v2-cbor'
resp.headers['Content-Type'] = 'application/cbor'
resp.body = CBOR.encode({ 'code' => error_code, 'message' => 'stubbed-error-message' })
type = operation.errors.find { |e| e.type.name.include?("Types::#{error_code}") }
resp.body = CBOR.encode({ '__type' => type.id, 'message' => 'stubbed-error-message' })
resp
end

private

def extract_error(context)
body = context.response.body.read
data = CBOR.decode(body)
context.response.body.rewind
return unless data && data['__type']

code = data.delete('__type').split('#').last
message = data['message']
data = parse_error_data(context, body, code)
[code, message, data]
end

def parse_error_data(context, body, code)
data = Schema::EmptyStructure.new
if (error_rules = context.operation.errors)
error_rules.each do |rule|
# match modeled shape name with the type(code) only
# some type(code) might contains invalid characters
# such as ':' (efs) etc
match = rule.id.split('#').last == code.gsub(/[^^a-zA-Z0-9]/, '')
next unless match && rule.members.any?

codec = Codecs::CBOR.new(setting(context))
data = codec.deserialize(body, rule, rule.type.new)
end
end
data
end

def apply_headers(context)
context.request.headers['X-Amzn-Query-Mode'] = 'true' if query_compatible?(context)
context.request.headers['Smithy-Protocol'] = 'rpc-v2-cbor'
Expand Down
9 changes: 5 additions & 4 deletions gems/smithy-client/lib/smithy-client/stubs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ def convert_stub(operation_name, stub)
stub = case stub
when Proc then stub
when Exception, Class then { error: stub }
when String then service_error_stub(stub)
when String then service_error_stub(operation_name, stub)
when Hash then http_response_stub(operation_name, stub)
else { data: stub }
end
Expand All @@ -212,8 +212,9 @@ def convert_stub(operation_name, stub)
stub
end

def service_error_stub(error_code)
{ http: config.protocol.stub_error(error_code) }
def service_error_stub(operation_name, error_code)
operation = config.service.operation(operation_name)
{ http: config.protocol.stub_error(operation, error_code) }
end

def http_response_stub(operation_name, data)
Expand All @@ -225,7 +226,7 @@ def http_response_stub(operation_name, data)
end

def hash_to_http_resp(data)
http_resp = Smithy::Client::HTTP::Response.new
http_resp = HTTP::Response.new
http_resp.status_code = data[:status_code]
http_resp.headers.update(data[:headers])
http_resp.body = data[:body]
Expand Down
10 changes: 10 additions & 0 deletions gems/smithy/lib/smithy/templates/client/client.erb
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,15 @@ module <%= module_name %>
context[:gem_version] = '<%= gem_version %>'
Smithy::Client::Input.new(handlers: handlers, context: context)
end

class << self
# @api private
attr_reader :identifier

# @api private
def errors_module
Errors
end
end
end
end
117 changes: 58 additions & 59 deletions gems/smithy/lib/smithy/templates/client/protocol_plugin.erb
Original file line number Diff line number Diff line change
Expand Up @@ -4,71 +4,70 @@

module <%= module_name %>
module Plugins
# Protocol plugin - allows user to configure protocol on client.
# TODO: Add convenience mapping - see https://github.com/smithy-lang/smithy-ruby/pull/264/files#r1946524403
# @api private
class Protocol < Smithy::Client::Plugin
option(
:protocol,
# Protocol plugin - allows user to configure protocol on client.
# TODO: Add convenience mapping - see https://github.com/smithy-lang/smithy-ruby/pull/264/files#r1946524403
# @api private
class Protocol < Smithy::Client::Plugin
option(
:protocol,
<% if protocol -%>
doc_default: '<%= protocol %>',
doc_type: '#build, #parse, #error',
rbs_type: 'Smithy::Client::_Protocol',
docstring: <<~DOCS) do |_cfg|
Allows you to overwrite default protocol. The given protocol
must provide the following functionalities:
- `build`
- `parse`
- `error`
See existing protocols within Smithy::Client::Protocols for examples.
DOCS
<%= protocol %>.new
end
doc_default: '<%= protocol %>',
doc_type: '#build, #parse, #error',
rbs_type: 'Smithy::Client::_Protocol',
docstring: <<~DOCS) do |_cfg|
Allows you to overwrite default protocol. The given protocol
must provide the following functionalities:
- `build`
- `parse`
- `error`
See existing protocols within Smithy::Client::Protocols for examples.
DOCS
<%= protocol %>.new
end
<% else -%>
default: nil,
doc_type: '#build, #parse, #error',
rbs_type: 'Smithy::Client::_Protocol',
docstring: <<~DOCS)
This configuration is required to build requests and parse responses.
In Smithy, a protocol is a named set of rules that defines the syntax
and semantics of how a client and server communicate. The given protocol
must provide the following functionalities: `build`, `parse` and `error`.
See existing protocols within Smithy::Client::Protocols for examples.
DOCS
default: nil,
doc_type: '#build, #parse, #error',
rbs_type: 'Smithy::Client::_Protocol',
docstring: <<~DOCS)
This configuration is required to build requests and parse responses.
In Smithy, a protocol is a named set of rules that defines the syntax
and semantics of how a client and server communicate. The given protocol
must provide the following functionalities: `build`, `parse` and `error`.
See existing protocols within Smithy::Client::Protocols for examples.
DOCS
<% end -%>

# @api private
class Build < Smithy::Client::Handler
def call(context)
context.config.protocol.build(context)
@handler.call(context)
end
end
# @api private
class Build < Smithy::Client::Handler
def call(context)
context.config.protocol.build(context)
@handler.call(context)
end
end

# @api private
class Parse < Smithy::Client::Handler
def call(context)
output = @handler.call(context)
output.data = context.config.protocol.parse(context)
output
end
end
# @api private
class Parse < Smithy::Client::Handler
def call(context)
output = @handler.call(context)
output.data = context.config.protocol.parse(context)
output
end
end

# @api private
class Error < Smithy::Client::Handler
def call(context)
@handler.call(context).on(300..599) do |response|
context.config.protocol.error(context, response)
end
end
end
# @api private
class Error < Smithy::Client::Handler
def call(context)
output = @handler.call(context)
output.error = context.config.protocol.error(context)
output
end
end

def add_handlers(handlers, config)
handlers.add(Build)
handlers.add(Parse)
# TODO: Requires error handling to be implemented
# handlers.add(Error, step: :sign)
end
end
def add_handlers(handlers, config)
handlers.add(Build)
handlers.add(Parse)
handlers.add(Error, step: :sign)
end
end
end
end
19 changes: 1 addition & 18 deletions gems/smithy/spec/fixtures/protocol_tests/rpcv2_cbor/model.json
Original file line number Diff line number Diff line change
Expand Up @@ -624,24 +624,7 @@
],
"traits": {
"smithy.api#documentation": "This operation has three possible return values:\n\n1. A successful response in the form of GreetingWithErrorsOutput\n2. An InvalidGreeting error.\n3. A ComplexError error.\n\nImplementations must be able to successfully take a response and\nproperly deserialize successful and error responses.",
"smithy.api#idempotent": {},
"smithy.ruby#skipTests": [
{
"id": "RpcV2CborComplexError",
"reason": "Error handling not Implemented yet.",
"type": "response"
},
{
"id": "RpcV2CborEmptyComplexError",
"reason": "Error handling not Implemented yet.",
"type": "response"
},
{
"id": "RpcV2CborInvalidGreetingError",
"reason": "Error handling not Implemented yet.",
"type": "response"
}
]
"smithy.api#idempotent": {}
}
},
"smithy.protocoltests.rpcv2Cbor#GreetingWithErrorsOutput": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ apply OperationWithDefaults @skipTests([
{ id: "RpcV2CborClientPopulatesDefaultValuesInInput", reason: "Defaults not Implemented yet.", type: "request" }
{ id: "RpcV2CborClientPopulatesDefaultsValuesWhenMissingInResponse", reason: "Defaults not Implemented yet.", type: "response" }
])

apply GreetingWithErrors @skipTests([
{ id: "RpcV2CborComplexError", reason: "Error handling not Implemented yet.", type: "response" }
{ id: "RpcV2CborEmptyComplexError", reason: "Error handling not Implemented yet.", type: "response" }
{ id: "RpcV2CborInvalidGreetingError", reason: "Error handling not Implemented yet.", type: "response" }
])
//
//apply GreetingWithErrors @skipTests([
// { id: "RpcV2CborComplexError", reason: "Error handling not Implemented yet.", type: "response" }
// { id: "RpcV2CborEmptyComplexError", reason: "Error handling not Implemented yet.", type: "response" }
// { id: "RpcV2CborInvalidGreetingError", reason: "Error handling not Implemented yet.", type: "response" }
//])
10 changes: 10 additions & 0 deletions projections/weather/lib/weather/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -207,5 +207,15 @@ def build_input(operation_name, params)
context[:gem_version] = '1.0.0'
Smithy::Client::Input.new(handlers: handlers, context: context)
end

class << self
# @api private
attr_reader :identifier

# @api private
def errors_module
Errors
end
end
end
end
9 changes: 4 additions & 5 deletions projections/weather/lib/weather/plugins/protocol.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,16 @@ def call(context)
# @api private
class Error < Smithy::Client::Handler
def call(context)
@handler.call(context).on(300..599) do |response|
context.config.protocol.error(context, response)
end
output = @handler.call(context)
output.error = context.config.protocol.error(context)
output
end
end

def add_handlers(handlers, _config)
handlers.add(Build)
handlers.add(Parse)
# TODO: Requires error handling to be implemented
# handlers.add(Error, step: :sign)
handlers.add(Error, step: :sign)
end
end
end
Expand Down
2 changes: 2 additions & 0 deletions projections/weather/sig/client.rbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module Weather
class Client < Smithy::Client::Base
include Smithy::Client::Stubs

def self.new: (
?convert_params: bool,
?endpoint: String,
Expand Down

0 comments on commit 1d222f6

Please sign in to comment.