smithy 2.0.0.pre0 → 2.0.0.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/bin/smithy-ruby +1 -4
  4. data/lib/smithy/command.rb +68 -0
  5. data/lib/smithy/generators/base.rb +19 -0
  6. data/lib/smithy/generators/client.rb +106 -0
  7. data/lib/smithy/generators/schema.rb +61 -0
  8. data/lib/smithy/generators.rb +29 -0
  9. data/lib/smithy/model/flattener.rb +114 -0
  10. data/lib/smithy/model/operation_parser.rb +42 -0
  11. data/lib/smithy/model/rbs.rb +57 -0
  12. data/lib/smithy/model/service_index.rb +51 -0
  13. data/lib/smithy/model/service_parser.rb +74 -0
  14. data/lib/smithy/model/shape.rb +49 -0
  15. data/lib/smithy/model/structure_parser.rb +43 -0
  16. data/lib/smithy/model/yard.rb +100 -0
  17. data/lib/smithy/model.rb +54 -0
  18. data/lib/smithy/plan.rb +79 -3
  19. data/lib/smithy/templates/client/auth_parameters.erb +29 -0
  20. data/lib/smithy/templates/client/auth_parameters_rbs.erb +14 -0
  21. data/lib/smithy/templates/client/auth_plugin.erb +115 -0
  22. data/lib/smithy/templates/client/auth_resolver.erb +16 -0
  23. data/lib/smithy/templates/client/auth_resolver_rbs.erb +5 -0
  24. data/lib/smithy/templates/client/client.erb +142 -0
  25. data/lib/smithy/templates/client/client_rbs.erb +29 -0
  26. data/lib/smithy/templates/client/customizations.erb +3 -0
  27. data/lib/smithy/templates/client/endpoint_parameters.erb +65 -0
  28. data/lib/smithy/templates/client/endpoint_parameters_rbs.erb +13 -0
  29. data/lib/smithy/templates/client/endpoint_plugin.erb +58 -0
  30. data/lib/smithy/templates/client/endpoint_provider.erb +15 -0
  31. data/lib/smithy/templates/client/endpoint_provider_rbs.erb +5 -0
  32. data/lib/smithy/templates/client/endpoint_provider_spec.erb +70 -0
  33. data/lib/smithy/templates/client/errors.erb +69 -0
  34. data/lib/smithy/templates/client/errors_rbs.erb +17 -0
  35. data/lib/smithy/templates/client/gemspec.erb +17 -0
  36. data/lib/smithy/templates/client/module.erb +22 -0
  37. data/lib/smithy/templates/client/module_rbs.erb +7 -0
  38. data/lib/smithy/templates/client/paginators.erb +33 -0
  39. data/lib/smithy/templates/client/protocol_spec.erb +144 -0
  40. data/lib/smithy/templates/client/rubocop_yml.erb +33 -0
  41. data/lib/smithy/templates/client/schema.erb +76 -0
  42. data/lib/smithy/templates/client/schema_rbs.erb +13 -0
  43. data/lib/smithy/templates/client/spec_helper.erb +10 -0
  44. data/lib/smithy/templates/client/types.erb +64 -0
  45. data/lib/smithy/templates/client/types_rbs.erb +47 -0
  46. data/lib/smithy/templates/client/waiters.erb +42 -0
  47. data/lib/smithy/util/hash_formatter.rb +124 -0
  48. data/lib/smithy/util/underscore.rb +18 -0
  49. data/lib/smithy/util.rb +9 -0
  50. data/lib/smithy/views/client/auth_parameter.rb +29 -0
  51. data/lib/smithy/views/client/auth_parameters.rb +23 -0
  52. data/lib/smithy/views/client/auth_parameters_rbs.rb +23 -0
  53. data/lib/smithy/views/client/auth_plugin.rb +35 -0
  54. data/lib/smithy/views/client/auth_resolver.rb +125 -0
  55. data/lib/smithy/views/client/auth_resolver_rbs.rb +19 -0
  56. data/lib/smithy/views/client/client.rb +208 -0
  57. data/lib/smithy/views/client/client_rbs.rb +231 -0
  58. data/lib/smithy/views/client/customizations.rb +10 -0
  59. data/lib/smithy/views/client/endpoint_parameter.rb +156 -0
  60. data/lib/smithy/views/client/endpoint_parameters.rb +43 -0
  61. data/lib/smithy/views/client/endpoint_parameters_rbs.rb +28 -0
  62. data/lib/smithy/views/client/endpoint_plugin.rb +27 -0
  63. data/lib/smithy/views/client/endpoint_provider.rb +241 -0
  64. data/lib/smithy/views/client/endpoint_provider_rbs.rb +19 -0
  65. data/lib/smithy/views/client/endpoint_provider_spec.rb +137 -0
  66. data/lib/smithy/views/client/errors.rb +88 -0
  67. data/lib/smithy/views/client/errors_rbs.rb +12 -0
  68. data/lib/smithy/views/client/gemspec.rb +36 -0
  69. data/lib/smithy/views/client/module.rb +107 -0
  70. data/lib/smithy/views/client/module_rbs.rb +20 -0
  71. data/lib/smithy/views/client/operation_examples.rb +157 -0
  72. data/lib/smithy/views/client/paginators.rb +108 -0
  73. data/lib/smithy/views/client/plugin.rb +29 -0
  74. data/lib/smithy/views/client/plugin_list.rb +57 -0
  75. data/lib/smithy/views/client/protocol_spec.rb +254 -0
  76. data/lib/smithy/views/client/request_response_example.rb +179 -0
  77. data/lib/smithy/views/client/rubocop_yml.rb +19 -0
  78. data/lib/smithy/views/client/schema.rb +356 -0
  79. data/lib/smithy/views/client/schema_rbs.rb +84 -0
  80. data/lib/smithy/views/client/shape_to_hash.rb +99 -0
  81. data/lib/smithy/views/client/spec_helper.rb +19 -0
  82. data/lib/smithy/views/client/types.rb +293 -0
  83. data/lib/smithy/views/client/types_rbs.rb +67 -0
  84. data/lib/smithy/views/client/waiters.rb +82 -0
  85. data/lib/smithy/views/client.rb +47 -0
  86. data/lib/smithy/views/view.rb +30 -0
  87. data/lib/smithy/views.rb +9 -0
  88. data/lib/smithy/weld.rb +109 -0
  89. data/lib/smithy/welds/auth/anonymous_auth.rb +31 -0
  90. data/lib/smithy/welds/auth/http_api_key_auth.rb +34 -0
  91. data/lib/smithy/welds/auth/http_basic_auth.rb +34 -0
  92. data/lib/smithy/welds/auth/http_bearer_auth.rb +34 -0
  93. data/lib/smithy/welds/auth/http_digest_auth.rb +34 -0
  94. data/lib/smithy/welds/plugins.rb +54 -0
  95. data/lib/smithy/welds/rpc_v2_cbor.rb +20 -0
  96. data/lib/smithy/welds/rubocop.rb +33 -0
  97. data/lib/smithy/welds/transforms/default_endpoint_rules.json +35 -0
  98. data/lib/smithy/welds/transforms/default_endpoint_tests.json +24 -0
  99. data/lib/smithy/welds/transforms/endpoints.rb +68 -0
  100. data/lib/smithy/welds/transforms/synthetic_input_output.rb +60 -0
  101. data/lib/smithy/welds.rb +29 -0
  102. data/lib/smithy.rb +33 -2
  103. metadata +144 -9
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Views
5
+ module Client
6
+ # @api private
7
+ class OperationExamples
8
+ # @param [Hash] model Model
9
+ # @param [String] operation_name Operation name
10
+ # @param [Hash<String, Object>] operation Operation shape
11
+ def initialize(model, operation_name, operation)
12
+ @model = model
13
+ @operation_name = operation_name
14
+ @operation = operation
15
+ @examples = operation.fetch('traits', {}).fetch('smithy.api#examples', [])
16
+ end
17
+
18
+ def docstrings
19
+ examples = ''
20
+ @examples.each do |example|
21
+ examples += example(example)
22
+ rescue ArgumentError => e
23
+ puts "Error processing example for #{@operation_name}: #{e}"
24
+ next
25
+ end
26
+ examples.split("\n")
27
+ end
28
+
29
+ private
30
+
31
+ def example(example)
32
+ if example['error']
33
+ error_example(example)
34
+ else
35
+ output_example(example)
36
+ end
37
+ end
38
+
39
+ def output_example(example)
40
+ <<~EXAMPLE
41
+ @example #{example['title']}
42
+ #{documentation(example)}
43
+ params = #{params(example['input'], @operation['input']['target'])}
44
+ options = {}
45
+ response = client.#{@operation_name}(params, options)
46
+ response.to_h #=>
47
+ #{params(example['output'], @operation['output']['target'])}
48
+ EXAMPLE
49
+ end
50
+
51
+ def error_example(example)
52
+ error = example['error']
53
+ <<~EXAMPLE
54
+ @example #{example['title']}
55
+ #{documentation(example)}
56
+ params = #{params(example['input'], @operation['input']['target'])}
57
+ options = {}
58
+ begin
59
+ response = client.#{@operation_name}(params, options)
60
+ rescue Smithy::Client::ServiceError => e
61
+ puts e.class #=> #{Model::Shape.name(error['shapeId'])}
62
+ puts e.data.to_h #=>
63
+ #{error_data(error['content'], error['shapeId'])}
64
+ end
65
+ EXAMPLE
66
+ end
67
+
68
+ def params(document, target, indent = ' ')
69
+ return '{}' if document.nil? || document.empty?
70
+
71
+ shape = Model.shape(@model, target)
72
+ structure(document, shape, indent).join("\n")
73
+ end
74
+
75
+ def error_data(document, shape_id, indent = ' ')
76
+ shape = Model.shape(@model, shape_id)
77
+ structure(document, shape, indent).join("\n")
78
+ end
79
+
80
+ def documentation(example)
81
+ documentation = example.fetch('documentation', "Example operation for #{@operation_name}")
82
+ documentation.split("\n").map { |line| "# #{line}" }.join("\n")
83
+ end
84
+
85
+ def value(json, shape, indent)
86
+ case shape['type']
87
+ when 'structure' then structure(json, shape, indent)
88
+ when 'map' then map(json, shape, indent)
89
+ when 'list' then list(json, shape, indent)
90
+ when 'timestamp' then "Time.parse(#{json.inspect})"
91
+ when 'string', 'blob' then json.inspect
92
+ else json
93
+ end
94
+ end
95
+
96
+ def structure(json, shape, indent)
97
+ lines = []
98
+ lines << '{'
99
+ json.each_pair do |key, value|
100
+ member_shape = Model.shape(@model, shape['members'][key]['target'])
101
+ entry = value(value, member_shape, "#{indent} ")
102
+ lines << "#{indent} #{key.underscore}: #{format(entry)},"
103
+ end
104
+ lines.last.chomp!(',') if lines.last.end_with?(',')
105
+ lines << "#{indent}}"
106
+ lines
107
+ end
108
+
109
+ def map(json, shape, indent)
110
+ lines = []
111
+ lines << '{'
112
+ json.each do |key, value|
113
+ member_shape = Model.shape(@model, shape['value']['target'])
114
+ entry = value(value, member_shape, "#{indent} ")
115
+ lines << "#{indent} \"#{key}\" => #{format(entry)},"
116
+ end
117
+ lines.last.chomp!(',') if lines.last.end_with?(',')
118
+ lines << "#{indent}}"
119
+ lines
120
+ end
121
+
122
+ def list(json, shape, indent)
123
+ lines = []
124
+ lines << '['
125
+ json.each do |element|
126
+ member_shape = Model.shape(@model, shape['member']['target'])
127
+ entry = value(element, member_shape, "#{indent} ")
128
+ lines << "#{indent} #{format(entry)},"
129
+ end
130
+ lines.last.chomp!(',') if lines.last.end_with?(',')
131
+ lines << "#{indent}]"
132
+ lines
133
+ end
134
+
135
+ def format(shape_val)
136
+ formatted = []
137
+ if shape_val.is_a?(Array)
138
+ hashes = shape_val.map do |v|
139
+ (v.is_a?(Hash) ? format_hash(v, ' ') : v)
140
+ end
141
+ hashes.join(',')
142
+ formatted << hashes
143
+ elsif shape_val.is_a?(Hash)
144
+ formatted << format_hash(shape_val, ' ')
145
+ else
146
+ formatted << shape_val
147
+ end
148
+ formatted.join("\n")
149
+ end
150
+
151
+ def format_hash(value, indent)
152
+ Util::HashFormatter.new(indent: indent).format(value)
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Views
5
+ module Client
6
+ # @api private
7
+ class Paginators < View
8
+ def initialize(plan)
9
+ @plan = plan
10
+ @model = plan.model
11
+ _, service = plan.service.first
12
+ @service_trait = paginated_trait(service)
13
+ super()
14
+ end
15
+
16
+ def module_name
17
+ @plan.module_name
18
+ end
19
+
20
+ def paginators
21
+ Model::ServiceIndex
22
+ .new(@model)
23
+ .operations_for(@plan.service)
24
+ .map do |id, operation|
25
+ operation_trait = paginated_trait(operation)
26
+ next if operation_trait.empty?
27
+
28
+ resolved_trait = @service_trait.merge(operation_trait)
29
+ Paginator.new(Model::Shape.name(id), resolved_trait)
30
+ end
31
+ .compact
32
+ end
33
+
34
+ private
35
+
36
+ def paginated_trait(shape)
37
+ shape.fetch('traits', {}).fetch('smithy.api#paginated', {})
38
+ end
39
+
40
+ # @api private
41
+ class Paginator
42
+ def initialize(name, trait)
43
+ @name = name
44
+ @input_token = trait['inputToken']
45
+ @output_token = trait['outputToken']
46
+ @items = trait['items']
47
+ end
48
+
49
+ attr_reader :name
50
+
51
+ def next_tokens_code
52
+ next_token_getter = output_getter(@output_token)
53
+ code = ["next_token = #{next_token_getter}"]
54
+ code << 'return {} if next_token.nil? || next_token.empty?'
55
+ code << ''
56
+ code << 'tokens = Hash.new { |h, k| h[k] = {} }'
57
+ next_token_setter = input_getter(@input_token, 'tokens')
58
+ code << "#{next_token_setter} = next_token"
59
+ code << 'tokens'
60
+ code
61
+ end
62
+
63
+ def prev_tokens_code
64
+ prev_token_getter = input_getter(@input_token)
65
+ code = ["prev_token = #{prev_token_getter}"]
66
+ code << 'return {} if prev_token.nil? || prev_token.empty?'
67
+ code << ''
68
+ code << 'tokens = Hash.new { |h, k| h[k] = {} }'
69
+ prev_token_setter = input_getter(@input_token, 'tokens')
70
+ code << "#{prev_token_setter} = prev_token"
71
+ code << 'tokens'
72
+ code
73
+ end
74
+
75
+ def items_code
76
+ return ["raise NotImplementedError, 'item iteration is not implemented for this operation'"] unless items?
77
+
78
+ [output_getter(@items)]
79
+ end
80
+
81
+ def items?
82
+ !!@items
83
+ end
84
+
85
+ # Builds a getter using the output shape and a path.
86
+ # This is used to get the value of the output token or items.
87
+ def output_getter(path)
88
+ getter = String.new('data')
89
+ path.split('.').each do |member|
90
+ getter << ".#{member.underscore}"
91
+ end
92
+ getter
93
+ end
94
+
95
+ # Builds a getter using the input shape and a path.
96
+ # This is used to set the value of the input token or fetch the previous token.
97
+ def input_getter(path, context = 'params')
98
+ getter = String.new(context)
99
+ path.split('.').each do |member|
100
+ getter << "[:#{member.underscore}]"
101
+ end
102
+ getter
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Views
5
+ module Client
6
+ # @api private
7
+ class Plugin
8
+ def initialize(options = {})
9
+ @class_name = options[:class_name]
10
+ @require_path = options[:require_path]
11
+ @require_relative = options.fetch(:require_relative, false)
12
+ @source = options[:source]
13
+ end
14
+
15
+ attr_accessor :class_name, :require_path, :source
16
+
17
+ def options
18
+ klass = @class_name
19
+ klass = Object.const_get(@class_name) if @class_name.is_a?(String)
20
+ klass.options
21
+ end
22
+
23
+ def require_relative?
24
+ @require_relative
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Views
5
+ module Client
6
+ # @api private
7
+ class PluginList
8
+ include Enumerable
9
+
10
+ def initialize(plan, code_generated_plugins)
11
+ @plan = plan
12
+ @plugins = plugins(plan, code_generated_plugins)
13
+ end
14
+
15
+ def each(&)
16
+ @plugins.each(&)
17
+ end
18
+
19
+ private
20
+
21
+ def plugins(plan, code_generated_plugins)
22
+ plugins = []
23
+ code_generated_plugins(plugins, code_generated_plugins)
24
+ weld_plugins(plugins, plan.welds)
25
+ plugins
26
+ end
27
+
28
+ def code_generated_plugins(plugins, code_generated_plugins)
29
+ define_module_names
30
+ code_generated_plugins.each do |_, plugin| # rubocop:disable Style/HashEachMethods
31
+ Object.module_eval(plugin.source)
32
+ plugins << plugin
33
+ end
34
+ end
35
+
36
+ # Code generated plugins may have nested namespaces, so we need to ensure
37
+ # that they are defined before we try to evaluate the source.
38
+ def define_module_names
39
+ parent = Object
40
+ @plan.module_name.split('::') do |mod|
41
+ child = mod
42
+ parent.const_set(child, ::Module.new) unless parent.const_defined?(child)
43
+ parent = parent.const_get(child)
44
+ end
45
+ end
46
+
47
+ def weld_plugins(plugins, welds)
48
+ weld_plugins = welds.map(&:add_plugins).reduce({}, :merge)
49
+ weld_plugins = weld_plugins.except(*welds.map(&:remove_plugins).reduce([], :+))
50
+ weld_plugins.each do |class_name, options|
51
+ plugins << Plugin.new(class_name: class_name, **options)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Views
5
+ module Client
6
+ # @api private
7
+ class ProtocolSpec < View
8
+ PROTOCOL_TEST_TRAITS = %w[smithy.test#httpRequestTests smithy.test#httpResponseTests].freeze
9
+
10
+ def initialize(plan)
11
+ @plan = plan
12
+ @model = plan.model
13
+ @all_operation_tests =
14
+ Model::ServiceIndex
15
+ .new(@model)
16
+ .operations_for(@plan.service)
17
+ .map { |id, o| OperationTests.new(@model, id, o) }
18
+ .reject(&:empty?)
19
+ super()
20
+ end
21
+
22
+ attr_reader :all_operation_tests
23
+
24
+ def module_name
25
+ @plan.module_name
26
+ end
27
+
28
+ def additional_requires
29
+ Set.new(@all_operation_tests.map(&:additional_requires).flatten)
30
+ end
31
+
32
+ # @api private
33
+ class OperationTests
34
+ def initialize(model, id, operation)
35
+ @model = model
36
+ @id = id
37
+ @operation = operation
38
+ @request_tests = build_request_tests
39
+ @response_tests = build_response_tests
40
+ @error_tests = build_error_tests
41
+ end
42
+
43
+ attr_reader :request_tests, :response_tests, :error_tests
44
+
45
+ def name
46
+ Model::Shape.name(@id).underscore
47
+ end
48
+
49
+ def additional_requires
50
+ @request_tests.map(&:additional_requires) + @response_tests.map(&:additional_requires)
51
+ end
52
+
53
+ def empty?
54
+ request_tests.empty? && response_tests.empty? && error_tests.empty?
55
+ end
56
+
57
+ private
58
+
59
+ def build_response_tests
60
+ @operation
61
+ .fetch('traits', {})
62
+ .fetch('smithy.test#httpResponseTests', [])
63
+ .select { |t| t.fetch('appliesTo', 'client') == 'client' }
64
+ .map { |t| ResponseTest.new(@model, @operation, t) }
65
+ end
66
+
67
+ def build_request_tests
68
+ @operation
69
+ .fetch('traits', {})
70
+ .fetch('smithy.test#httpRequestTests', [])
71
+ .select { |t| t.fetch('appliesTo', 'client') == 'client' }
72
+ .map { |t| RequestTest.new(@model, @operation, t) }
73
+ end
74
+
75
+ def build_error_tests
76
+ @operation
77
+ .fetch('errors', [])
78
+ .map { |e| tests_for_error(e) }
79
+ .flatten
80
+ end
81
+
82
+ def tests_for_error(error)
83
+ error_shape = Model.shape(@model, error['target'])
84
+ # NOTE: error tests do not have a appliesTo that should be checked
85
+ error_shape
86
+ .fetch('traits', {})
87
+ .fetch('smithy.test#httpResponseTests', [])
88
+ .map { |t| ErrorTest.new(@model, @operation, error['target'], t) }
89
+ end
90
+ end
91
+
92
+ # @api private
93
+ class TestCase
94
+ def initialize(model, operation, test_case)
95
+ @model = model
96
+ @operation = operation
97
+ @test_case = test_case
98
+ @input_shape = Model.shape(@model, @operation['input']['target'])
99
+ @output_shape = Model.shape(@model, @operation['output']['target'])
100
+ end
101
+
102
+ attr_reader :test_case
103
+
104
+ def [](key)
105
+ test_case[key]
106
+ end
107
+
108
+ def comments
109
+ test_case.fetch('documentation', '').split("\n")
110
+ end
111
+
112
+ def id
113
+ test_case['id']
114
+ end
115
+
116
+ def additional_requires
117
+ requires = []
118
+ if test_case['bodyMediaType']
119
+ requires +=
120
+ case test_case['bodyMediaType']
121
+ when 'application/cbor'
122
+ %w[base64]
123
+ when 'application/json'
124
+ %w[json]
125
+ else
126
+ []
127
+ end
128
+ end
129
+ requires
130
+ end
131
+
132
+ def skip?
133
+ @operation
134
+ .fetch('traits', {})
135
+ .fetch('smithy.ruby#skipTests', [])
136
+ .any? { |skip| skip['id'] == id }
137
+ end
138
+
139
+ def skip_reason
140
+ @operation
141
+ .fetch('traits', {})
142
+ .fetch('smithy.ruby#skipTests', [])
143
+ .find { |skip| skip['id'] == id }
144
+ &.fetch('reason', 'skipped')
145
+ end
146
+ end
147
+
148
+ # @api private
149
+ class RequestTest < TestCase
150
+ def params
151
+ ShapeToHash.transform_value(@model, test_case.fetch('params', {}), @input_shape)
152
+ end
153
+
154
+ def endpoint
155
+ "https://#{test_case.fetch('host', '127.0.0.1')}"
156
+ end
157
+
158
+ def body_expect
159
+ return nil unless test_case['body']
160
+
161
+ case test_case['bodyMediaType']
162
+ when 'application/cbor'
163
+ 'expect(Smithy::CBOR.decode(request.body.read)).' \
164
+ "to match_data(Smithy::CBOR.decode(::Base64.decode64('#{test_case['body']}')))"
165
+ when 'application/json'
166
+ "expect(JSON.parse(request.body.read)).to eq(JSON.parse('#{test_case['body']}'))"
167
+ else
168
+ "expect(request.body.read).to eq('#{test_case['body']}')"
169
+ end
170
+ end
171
+
172
+ def query_expect?
173
+ test_case['queryParams'] || test_case['forbidQueryParams'] || test_case['requireQueryParams']
174
+ end
175
+
176
+ def idempotency_token_trait?
177
+ @input_shape.fetch('members', {})
178
+ .any? { |_name, shape| shape.fetch('traits', {}).key?('smithy.api#idempotencyToken') }
179
+ end
180
+ end
181
+
182
+ # @api private
183
+ class ResponseTest < TestCase
184
+ def params
185
+ # Finds all required members to pass operation validation
186
+ ShapeToHash.transform_value(@model, structure(@input_shape, {}), @input_shape)
187
+ end
188
+
189
+ def shape(shape, value)
190
+ case shape['type']
191
+ when 'structure' then structure(shape, value)
192
+ else value
193
+ end
194
+ end
195
+
196
+ def structure(shape, values)
197
+ shape['members'].each_with_object({}) do |(member_name, member_shape), data|
198
+ next unless required?(member_shape.fetch('traits', {}))
199
+
200
+ target = Model.shape(@model, member_shape['target'])
201
+ data[member_name] = shape(target, values)
202
+ end
203
+ end
204
+
205
+ def required?(traits)
206
+ traits.key?('smithy.api#required') && !traits.key?('smithy.api#clientOptional')
207
+ end
208
+
209
+ def stub_body
210
+ case test_case['bodyMediaType']
211
+ when 'application/cbor'
212
+ "::Base64.decode64('#{test_case['body']}')"
213
+ else
214
+ "'#{test_case['body']}'"
215
+ end
216
+ end
217
+
218
+ def data_expect
219
+ output = ShapeToHash.transform_value(@model, test_case.fetch('params', {}), @output_shape)
220
+ "expect(response.data.to_h).to match_data(#{output})"
221
+ end
222
+
223
+ def streaming_member
224
+ @output_shape
225
+ .fetch('members', {})
226
+ .map { |name, member| [name, Model.shape(@model, member['target'])] }
227
+ .find { |_name, shape| shape.fetch('traits', {}).key?('smithy.api#streaming') }
228
+ end
229
+ end
230
+
231
+ # @api private
232
+ class ErrorTest < ResponseTest
233
+ def initialize(model, operation, error_id, test_case)
234
+ super(model, operation, test_case)
235
+ @error_id = error_id
236
+ @error_shape = Model.shape(@model, error_id)
237
+ end
238
+
239
+ def error_name
240
+ Model::Shape.name(@error_id)
241
+ end
242
+
243
+ 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})"
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end