Skip to content

Commit 70b4af9

Browse files
authored
Support vendor params in protocol tests (#315)
1 parent 9594833 commit 70b4af9

5 files changed

Lines changed: 70 additions & 54 deletions

File tree

gems/smithy-client/lib/smithy-client/dynamic_errors.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ def error_class(error_code)
5252
# @return [Symbol] Returns a symbolized constant name for the given `error_code`.
5353
def error_class_constant(error_code)
5454
constant = error_code.to_s
55-
constant = constant.gsub(/https?:.*$/, '')
5655
constant = constant.gsub(/[^a-zA-Z0-9]/, '')
5756
constant = "Error#{constant}" unless constant.match(/^[a-z]/i)
5857
constant = constant[0].upcase + constant[1..]

gems/smithy-client/spec/smithy-client/dynamic_errors_spec.rb

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,6 @@ module Client
1919
expect(mod.error_class('My.Error')).to be(mod::MyError)
2020
end
2121

22-
it 'removes http schemas from the error code' do
23-
expect(mod.error_class('ErrorClass:http://example.com')).to be(mod::ErrorClass)
24-
expect(mod.error_class('ErrorClass:https://example.com')).to be(mod::ErrorClass)
25-
end
26-
2722
it 'ensures the error class name starts with a letter' do
2823
expect(mod.error_class('123Code')).to be(mod::Error123Code)
2924
end

gems/smithy/lib/smithy/templates/client/protocol_spec.erb

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ module <%= module_name %>
1616
<% unless operation_tests.request_tests.empty? -%>
1717
describe 'requests' do
1818
<% operation_tests.request_tests.each do |test| -%>
19-
<% test.comments.each do |line| -%>
19+
<% test.docstrings.each do |line| -%>
2020
# <%= line %>
2121
<% end -%>
2222
<% if test.skip? -%>
23-
it '<%= test.id %>', skip: '<%= test.skip_reason %>' do
23+
it '<%= test['id'] %>', skip: '<%= test.skip_reason %>' do
2424
<% else -%>
25-
it '<%= test.id %>' do
25+
it '<%= test['id'] %>' do
2626
<% end -%>
2727
<% if test['host'] -%>
2828
client = Client.new(client_options.merge(endpoint: '<%= test.endpoint %>'))
@@ -76,13 +76,13 @@ module <%= module_name %>
7676
<% unless operation_tests.response_tests.empty? -%>
7777
describe 'responses' do
7878
<% operation_tests.response_tests.each do |test| -%>
79-
<% test.comments.each do |line| -%>
79+
<% test.docstrings.each do |line| -%>
8080
# <%= line %>
8181
<% end -%>
8282
<% if test.skip? -%>
83-
it '<%= test.id %>', skip: '<%= test.skip_reason %>' do
83+
it '<%= test['id'] %>', skip: '<%= test.skip_reason %>' do
8484
<% else -%>
85-
it '<%= test.id %>' do
85+
it '<%= test['id'] %>' do
8686
<% end -%>
8787
response = { status_code: <%= test['code'] %> }
8888
<% if test['headers'] -%>
@@ -110,13 +110,13 @@ module <%= module_name %>
110110
<% unless operation_tests.error_tests.empty? -%>
111111
describe 'response errors' do
112112
<% operation_tests.error_tests.each do |test| -%>
113-
<% test.comments.each do |line| -%>
113+
<% test.docstrings.each do |line| -%>
114114
# <%= line %>
115115
<% end -%>
116116
<% if test.skip? -%>
117-
it '<%= test.id %>: <%= test.error_name %>', skip: '<%= test.skip_reason %>' do
117+
it '<%= test['id'] %>: <%= test.error_name %>', skip: '<%= test.skip_reason %>' do
118118
<% else -%>
119-
it '<%= test.id %>: <%= test.error_name %>' do
119+
it '<%= test['id'] %>: <%= test.error_name %>' do
120120
<% end -%>
121121
response = { status_code: <%= test['code'] %> }
122122
<% if test['headers'] -%>
@@ -132,7 +132,10 @@ module <%= module_name %>
132132
client.stub_responses(:<%= operation_tests.name %>, response)
133133
expect { client.<%= operation_tests.name %> }.to raise_error do |e|
134134
expect(e).to be_a(Errors::<%= test.error_name %>)
135-
<%= test.data_expect %>
135+
expect(e.data.to_h).to match_data(<%= test.params %>)
136+
<% test.vendor_code(:error_expect_code).each do |line| -%>
137+
<%= line %>
138+
<% end -%>
136139
end
137140
end
138141
<% end -%>

gems/smithy/lib/smithy/views/client/protocol_spec.rb

Lines changed: 49 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def initialize(plan)
1414
Model::ServiceIndex
1515
.new(@model)
1616
.operations_for(@plan.service)
17-
.map { |id, o| OperationTests.new(@model, id, o) }
17+
.map { |id, o| OperationTests.new(@model, id, o, vendor_code) }
1818
.reject(&:empty?)
1919
super()
2020
end
@@ -29,12 +29,20 @@ def additional_requires
2929
Set.new(@all_operation_tests.map(&:additional_requires).flatten)
3030
end
3131

32+
def vendor_code
33+
@plan
34+
.welds
35+
.map(&:protocol_test_vendor_code)
36+
.reduce({}, :merge)
37+
end
38+
3239
# @api private
3340
class OperationTests
34-
def initialize(model, id, operation)
41+
def initialize(model, id, operation, vendor_code)
3542
@model = model
3643
@id = id
3744
@operation = operation
45+
@vendor_code = vendor_code
3846
@request_tests = build_request_tests
3947
@response_tests = build_response_tests
4048
@error_tests = build_error_tests
@@ -61,15 +69,15 @@ def build_response_tests
6169
.fetch('traits', {})
6270
.fetch('smithy.test#httpResponseTests', [])
6371
.select { |t| t.fetch('appliesTo', 'client') == 'client' }
64-
.map { |t| ResponseTest.new(@model, @operation, t) }
72+
.map { |t| ResponseTest.new(@model, @operation, t, @vendor_code) }
6573
end
6674

6775
def build_request_tests
6876
@operation
6977
.fetch('traits', {})
7078
.fetch('smithy.test#httpRequestTests', [])
7179
.select { |t| t.fetch('appliesTo', 'client') == 'client' }
72-
.map { |t| RequestTest.new(@model, @operation, t) }
80+
.map { |t| RequestTest.new(@model, @operation, t, @vendor_code) }
7381
end
7482

7583
def build_error_tests
@@ -85,39 +93,45 @@ def tests_for_error(error)
8593
error_shape
8694
.fetch('traits', {})
8795
.fetch('smithy.test#httpResponseTests', [])
88-
.map { |t| ErrorTest.new(@model, @operation, error['target'], t) }
96+
.map { |t| ErrorTest.new(@model, @operation, error['target'], t, @vendor_code) }
8997
end
9098
end
9199

92100
# @api private
93101
class TestCase
94-
def initialize(model, operation, test_case)
102+
def initialize(model, operation, test_case, vendor_code)
95103
@model = model
96104
@operation = operation
97105
@test_case = test_case
106+
@vendor_code = vendor_code
98107
@input_shape = Model.shape(@model, @operation['input']['target'])
99108
@output_shape = Model.shape(@model, @operation['output']['target'])
100109
end
101110

102-
attr_reader :test_case
111+
def vendor_code(method)
112+
return [] unless @test_case['vendorParamsShape'] && @test_case['vendorParams']
103113

104-
def [](key)
105-
test_case[key]
114+
vendor_code = @vendor_code[@test_case['vendorParamsShape']]
115+
unless vendor_code.respond_to?(method)
116+
raise "Unhandled protocol test vendor code for shape: '#{@test_case['vendorParamsShape']}. '" \
117+
"Please implement a class that responds to :#{method} and register it with a weld."
118+
end
119+
vendor_code.send(method, @test_case['vendorParams']).split("\n")
106120
end
107121

108-
def comments
109-
test_case.fetch('documentation', '').split("\n")
122+
def [](key)
123+
@test_case[key]
110124
end
111125

112-
def id
113-
test_case['id']
126+
def docstrings
127+
@test_case.fetch('documentation', '').split("\n")
114128
end
115129

116130
def additional_requires
117131
requires = []
118-
if test_case['bodyMediaType']
132+
if @test_case['bodyMediaType']
119133
requires +=
120-
case test_case['bodyMediaType']
134+
case @test_case['bodyMediaType']
121135
when 'application/cbor'
122136
%w[base64]
123137
when 'application/json'
@@ -133,49 +147,50 @@ def skip?
133147
@operation
134148
.fetch('traits', {})
135149
.fetch('smithy.ruby#skipTests', [])
136-
.any? { |skip| skip['id'] == id }
150+
.any? { |skip| skip['id'] == @test_case['id'] }
137151
end
138152

139153
def skip_reason
140154
@operation
141155
.fetch('traits', {})
142156
.fetch('smithy.ruby#skipTests', [])
143-
.find { |skip| skip['id'] == id }
157+
.find { |skip| skip['id'] == @test_case['id'] }
144158
&.fetch('reason', 'skipped')
145159
end
146160
end
147161

148162
# @api private
149163
class RequestTest < TestCase
150164
def params
151-
ShapeToHash.transform_value(@model, test_case.fetch('params', {}), @input_shape)
165+
ShapeToHash.transform_value(@model, @test_case.fetch('params', {}), @input_shape)
152166
end
153167

154168
def endpoint
155-
"https://#{test_case.fetch('host', '127.0.0.1')}"
169+
"https://#{@test_case.fetch('host', '127.0.0.1')}"
156170
end
157171

158172
def body_expect
159-
return nil unless test_case['body']
173+
return nil unless @test_case['body']
160174

161-
case test_case['bodyMediaType']
175+
case @test_case['bodyMediaType']
162176
when 'application/cbor'
163177
'expect(Smithy::CBOR.decode(request.body.read)).' \
164-
"to match_data(Smithy::CBOR.decode(::Base64.decode64('#{test_case['body']}')))"
178+
"to match_data(Smithy::CBOR.decode(::Base64.decode64('#{@test_case['body']}')))"
165179
when 'application/json'
166-
"expect(JSON.parse(request.body.read)).to eq(JSON.parse('#{test_case['body']}'))"
180+
"expect(JSON.parse(request.body.read)).to eq(JSON.parse('#{@test_case['body']}'))"
167181
else
168-
"expect(request.body.read).to eq('#{test_case['body']}')"
182+
"expect(request.body.read).to eq('#{@test_case['body']}')"
169183
end
170184
end
171185

172186
def query_expect?
173-
test_case['queryParams'] || test_case['forbidQueryParams'] || test_case['requireQueryParams']
187+
@test_case['queryParams'] || @test_case['forbidQueryParams'] || @test_case['requireQueryParams']
174188
end
175189

176190
def idempotency_token_trait?
177-
@input_shape.fetch('members', {})
178-
.any? { |_name, shape| shape.fetch('traits', {}).key?('smithy.api#idempotencyToken') }
191+
@input_shape
192+
.fetch('members', {})
193+
.any? { |_name, shape| shape.fetch('traits', {}).key?('smithy.api#idempotencyToken') }
179194
end
180195
end
181196

@@ -207,16 +222,16 @@ def required?(traits)
207222
end
208223

209224
def stub_body
210-
case test_case['bodyMediaType']
225+
case @test_case['bodyMediaType']
211226
when 'application/cbor'
212-
"::Base64.decode64('#{test_case['body']}')"
227+
"::Base64.decode64('#{@test_case['body']}')"
213228
else
214-
"'#{test_case['body']}'"
229+
"'#{@test_case['body']}'"
215230
end
216231
end
217232

218233
def data_expect
219-
output = ShapeToHash.transform_value(@model, test_case.fetch('params', {}), @output_shape)
234+
output = ShapeToHash.transform_value(@model, @test_case.fetch('params', {}), @output_shape)
220235
"expect(response.data.to_h).to match_data(#{output})"
221236
end
222237

@@ -230,8 +245,8 @@ def streaming_member
230245

231246
# @api private
232247
class ErrorTest < ResponseTest
233-
def initialize(model, operation, error_id, test_case)
234-
super(model, operation, test_case)
248+
def initialize(model, operation, error_id, test_case, vendor_code)
249+
super(model, operation, test_case, vendor_code)
235250
@error_id = error_id
236251
@error_shape = Model.shape(@model, error_id)
237252
end
@@ -241,11 +256,7 @@ def error_name
241256
end
242257

243258
def params
244-
ShapeToHash.transform_value(@model, test_case.fetch('params', {}), @error_shape)
245-
end
246-
247-
def data_expect
248-
"expect(e.data.to_h).to match_data(#{params})"
259+
ShapeToHash.transform_value(@model, @test_case.fetch('params', {}), @error_shape)
249260
end
250261
end
251262
end

gems/smithy/lib/smithy/weld.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,5 +94,13 @@ def add_auth_schemes
9494
def remove_auth_schemes
9595
[]
9696
end
97+
98+
# Called when generating protocol tests. The key should be the same as the vendor params shape
99+
# in a protocol test, and the value should be a class that responds to one of the following methods:
100+
# * error_expect_code(params) - returns a string that is rendered inside a rescue block (with error rescued as `e`).
101+
# Protocol tests are run with RSpec and expectations should be used.
102+
def protocol_test_vendor_code
103+
{}
104+
end
97105
end
98106
end

0 commit comments

Comments
 (0)