soaspec 0.1.6 → 0.1.7

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 (60) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +15 -15
  3. data/.gitlab-ci.yml +48 -48
  4. data/.rspec +3 -3
  5. data/.rubocop.yml +2 -2
  6. data/CODE_OF_CONDUCT.md +74 -74
  7. data/ChangeLog +408 -404
  8. data/Gemfile +6 -6
  9. data/LICENSE.txt +21 -21
  10. data/README.md +113 -113
  11. data/Rakefile +24 -24
  12. data/Todo.md +6 -6
  13. data/exe/soaspec +109 -109
  14. data/exe/soaspec-virtual-server +156 -156
  15. data/exe/xml_to_yaml_file +60 -60
  16. data/lib/soaspec.rb +107 -103
  17. data/lib/soaspec/core_ext/hash.rb +83 -83
  18. data/lib/soaspec/exchange.rb +235 -235
  19. data/lib/soaspec/exchange_handlers/exchange_handler.rb +103 -103
  20. data/lib/soaspec/exchange_handlers/handler_accessors.rb +106 -106
  21. data/lib/soaspec/exchange_handlers/rest_accessors.rb +54 -92
  22. data/lib/soaspec/exchange_handlers/rest_handler.rb +318 -314
  23. data/lib/soaspec/exchange_handlers/rest_methods.rb +44 -44
  24. data/lib/soaspec/exchange_handlers/soap_handler.rb +236 -236
  25. data/lib/soaspec/exe_helpers.rb +60 -60
  26. data/lib/soaspec/generator/.rspec.erb +5 -5
  27. data/lib/soaspec/generator/.travis.yml.erb +5 -5
  28. data/lib/soaspec/generator/Gemfile.erb +8 -8
  29. data/lib/soaspec/generator/README.md.erb +29 -29
  30. data/lib/soaspec/generator/Rakefile.erb +19 -19
  31. data/lib/soaspec/generator/config/data/default.yml.erb +1 -1
  32. data/lib/soaspec/generator/lib/blz_service.rb.erb +26 -26
  33. data/lib/soaspec/generator/lib/dynamic_class_content.rb.erb +12 -12
  34. data/lib/soaspec/generator/lib/shared_example.rb.erb +8 -8
  35. data/lib/soaspec/generator/spec/dynamic_soap_spec.rb.erb +12 -12
  36. data/lib/soaspec/generator/spec/soap_spec.rb.erb +51 -51
  37. data/lib/soaspec/generator/spec/spec_helper.rb.erb +20 -20
  38. data/lib/soaspec/generator/template/soap_template.xml +6 -6
  39. data/lib/soaspec/interpreter.rb +40 -40
  40. data/lib/soaspec/matchers.rb +65 -65
  41. data/lib/soaspec/not_found_errors.rb +13 -13
  42. data/lib/soaspec/o_auth2.rb +65 -0
  43. data/lib/soaspec/soaspec_shared_examples.rb +24 -24
  44. data/lib/soaspec/spec_logger.rb +34 -27
  45. data/lib/soaspec/test_server/bank.wsdl +90 -90
  46. data/lib/soaspec/test_server/get_bank.rb +160 -160
  47. data/lib/soaspec/test_server/id_manager.rb +31 -31
  48. data/lib/soaspec/test_server/invoices.rb +27 -27
  49. data/lib/soaspec/test_server/namespace.xml +14 -14
  50. data/lib/soaspec/test_server/note.xml +5 -5
  51. data/lib/soaspec/test_server/puppy_service.rb +20 -20
  52. data/lib/soaspec/test_server/test_attribute.rb +13 -13
  53. data/lib/soaspec/test_server/test_namespace.rb +12 -12
  54. data/lib/soaspec/version.rb +2 -2
  55. data/lib/soaspec/wsdl_generator.rb +144 -144
  56. data/soaspec.gemspec +46 -46
  57. data/test.wsdl +116 -116
  58. data/test.xml +10 -10
  59. data/test_wsdl.rb +43 -43
  60. metadata +4 -3
@@ -1,315 +1,319 @@
1
- require_relative 'exchange_handler'
2
- require_relative 'rest_accessors'
3
- require_relative '../core_ext/hash'
4
- require_relative '../not_found_errors'
5
- require_relative 'handler_accessors'
6
- require_relative '../interpreter'
7
- require 'json'
8
- require 'jsonpath'
9
- require 'nori'
10
- require 'erb'
11
-
12
- module Soaspec
13
-
14
- # Wraps around Savon client defining default values dependent on the soap request
15
- class RestHandler < ExchangeHandler
16
- extend Soaspec::RestAccessors
17
-
18
- # User used in making API calls
19
- attr_accessor :api_username
20
-
21
- # Set through following method. Base URL in REST requests.
22
- def base_url_value
23
- nil
24
- end
25
-
26
- # Headers used in RestClient
27
- def rest_client_headers
28
- {}
29
- end
30
-
31
- # Add values to here when extending this class to have default REST options.
32
- # See rest client resource at https://github.com/rest-client/rest-client for details
33
- # It's easier to set headers via 'headers' accessor rather than here
34
- # @return [Hash] Options adding to & overriding defaults
35
- def rest_resource_options
36
- {
37
- }
38
- end
39
-
40
- # Perform ERB on each header value
41
- # @return [Hash] Hash from 'rest_client_headers' passed through ERB
42
- def parse_headers
43
- Hash[rest_client_headers.map { |k, header| [k, ERB.new(header).result(binding)] }]
44
- end
45
-
46
- # Setup object to handle communicating with a particular SOAP WSDL
47
- # @param [Hash] options Options defining SOAP request. WSDL, authentication
48
- def initialize(name = self.class.to_s, options = {})
49
- raise "Base URL not set! Please set in class with 'base_url' method" unless base_url_value
50
- @default_hash = {}
51
- if name.is_a?(Hash) && options == {} # If name is not set
52
- options = name
53
- name = self.class.to_s
54
- end
55
- super
56
- set_remove_key(options, :api_username)
57
- set_remove_key(options, :default_hash)
58
- set_remove_key(options, :template_name)
59
- @init_options = options
60
- end
61
-
62
- # Convert snakecase to PascalCase
63
- def convert_to_pascal_case(key)
64
- return key if /[[:upper:]]/ =~ key[0] # If first character already capital, don't do conversion
65
- key.split('_').map(&:capitalize).join
66
- end
67
-
68
- # Whether to convert each key in the request to PascalCase
69
- # It will also auto convert simple XPath, JSONPath where '//' or '..' not specified
70
- # @return Whether to convert to PascalCase
71
- def pascal_keys?
72
- false
73
- end
74
-
75
- # Initialize value of merged options
76
- def init_merge_options
77
- options = rest_resource_options
78
- options[:headers] ||= {}
79
- options[:headers].merge! parse_headers
80
- options.merge(@init_options)
81
- end
82
-
83
- # Used in together with Exchange request that passes such override parameters
84
- # @param [Hash] override_parameters Params to characterize REST request
85
- # @param_value [params] Extra parameters (E.g. headers)
86
- # @param_value [suburl] URL appended to base_url of class
87
- # @param_value [q] Query for REST
88
- # @param_value [method] REST method (:get, :post, etc)
89
- def make_request(override_parameters)
90
- @merged_options ||= init_merge_options
91
- test_values = override_parameters
92
- test_values[:params] ||= {}
93
- test_values[:method] ||= :post
94
- test_values[:suburl] = test_values[:suburl].to_s if test_values[:suburl]
95
- test_values[:params][:params] = test_values[:q] if test_values[:q] # Use q for query parameters. Nested :params is ugly and long
96
- # In order for ERB to be calculated at correct time, the first time request is made, the resource should be created
97
- @resource ||= RestClient::Resource.new(ERB.new(base_url_value).result(binding), @merged_options)
98
-
99
- @resource_used = test_values[:suburl] ? @resource[test_values[:suburl]] : @resource
100
-
101
- begin
102
- response = case test_values[:method]
103
- when :post, :patch, :put
104
- Soaspec::SpecLogger.info("request body: #{post_data(test_values)}")
105
- @resource_used.send(test_values[:method].to_s, post_data(test_values), test_values[:params])
106
- else # :get, :delete
107
- @resource_used.send(test_values[:method].to_s, test_values[:params])
108
- end
109
- rescue RestClient::ExceptionWithResponse => e
110
- response = e.response
111
- end
112
- Soaspec::SpecLogger.info('response_headers: ' + response.headers.to_s)
113
- Soaspec::SpecLogger.info('response_body: ' + response.to_s)
114
- response
115
- end
116
-
117
- # @param [Hash] format Format of expected result.
118
- # @return [Object] Generic body to be displayed in error messages
119
- def response_body(response, format: :hash)
120
- extract_hash response
121
- end
122
-
123
- # @return [Boolean] Whether response body includes String
124
- def include_in_body?(response, expected)
125
- response.body.include? expected
126
- end
127
-
128
- # @@return [Boolean] Whether the request found the desired value or not
129
- def found?(response)
130
- status_code_for(response) != 404
131
- end
132
-
133
- # Convert XML or JSON response into a Hash
134
- # @param [String] response Response as a String (either in XML or JSON)
135
- # @return [Hash]
136
- def extract_hash(response)
137
- raise ArgumentError("Empty Body. Can't assert on it") if response.body.empty?
138
- case Interpreter.response_type_for response
139
- when :json
140
- converted = JSON.parse(response.body)
141
- return converted.transform_keys_to_symbols if converted.is_a? Hash
142
- return converted.map!(&:transform_keys_to_symbols) if converted.is_a? Array
143
- raise 'Incorrect Type prodcued ' + converted.class
144
- when :xml
145
- parser = Nori.new(convert_tags_to: lambda { |tag| tag.snakecase.to_sym })
146
- parser.parse(response.body)
147
- else
148
- raise "Neither XML nor JSON detected. It is #{type}. Don't know how to parse It is #{response.body}"
149
- end
150
- end
151
-
152
- # @return [Boolean] Whether response contains expected value
153
- def include_value?(response, expected)
154
- extract_hash(response).include_value? expected
155
- end
156
-
157
- # @return [Boolean] Whether response body contains expected key
158
- def include_key?(response, expected)
159
- value_from_path(response, expected)
160
- end
161
-
162
- # @return [Integer] HTTP Status code for response
163
- def status_code_for(response)
164
- response.code
165
- end
166
-
167
- # Override this to specify elements that must be present in the response
168
- # Will be used in 'success_scenarios' shared examples
169
- # @return [Array] Array of symbols specifying element names
170
- def mandatory_elements
171
- []
172
- end
173
-
174
- # Override this to specify xpath results that must be present in the response
175
- # Will be used in 'success_scenarios' shared examples
176
- # @return [Hash] Hash of 'xpath' => 'expected value' pairs
177
- def mandatory_xpath_values
178
- {}
179
- end
180
-
181
- # Attributes set at the root XML element of SOAP request
182
- def root_attributes
183
- nil
184
- end
185
-
186
- # Returns the value at the provided xpath
187
- # @param [RestClient::Response] response
188
- # @param [String] xpath
189
- # @return [Enumerable] Value inside element found through Xpath
190
- def xpath_elements_for(response: nil, xpath: nil, attribute: nil)
191
- raise ArgumentError unless response && xpath
192
- raise "Can't perform XPATH if response is not XML" unless Interpreter.response_type_for(response) == :xml
193
- xpath = "//*[@#{attribute}]" unless attribute.nil?
194
- if xpath[0] != '/'
195
- xpath = convert_to_pascal_case(xpath) if pascal_keys?
196
- xpath = '//' + xpath
197
- end
198
- temp_doc = Nokogiri.parse(response.body).dup
199
- if strip_namespaces? && !xpath.include?(':')
200
- temp_doc.remove_namespaces!
201
- temp_doc.xpath(xpath)
202
- else
203
- temp_doc.xpath(xpath, temp_doc.collect_namespaces)
204
- end
205
- end
206
-
207
- # @return [Enumerable] List of values matching JSON path
208
- def json_path_values_for(response, path, attribute: nil)
209
- raise 'JSON does not support attributes' if attribute
210
- if path[0] != '$'
211
- path = convert_to_pascal_case(path) if pascal_keys?
212
- path = '$..' + path
213
- end
214
- JsonPath.on(response.body, path)
215
- end
216
-
217
- # Based on a exchange, return the value at the provided xpath
218
- # If the path does not begin with a '/', a '//' is added to it
219
- # @param [Response] response
220
- # @param [Object] path Xpath, JSONPath or other path identifying how to find element
221
- # @param [String] attribute Generic attribute to find. Will override path
222
- # @return [String] Value at Xpath
223
- def value_from_path(response, path, attribute: nil)
224
- path = path.to_s
225
- case Interpreter.response_type_for(response)
226
- when :xml
227
- result = xpath_elements_for(response: response, xpath: path, attribute: attribute).first
228
- raise NoElementAtPath, "No value at Xpath '#{path}'" unless result
229
- return result.inner_text if attribute.nil?
230
- return result.attributes[attribute].inner_text
231
- when :json
232
- paths_to_check = path.split(',')
233
- matching_values = paths_to_check.collect do |path_to_check|
234
- json_path_values_for(response, path_to_check, attribute: attribute)
235
- end.reject(&:empty?)
236
- raise NoElementAtPath, "Path '#{path}' not found in '#{response.body}'" if matching_values.empty?
237
- matching_values.first.first
238
- when :hash
239
- response.dig(path.split('.')) # Use path as Hash dig expression separating params via '.' TODO: Unit test
240
- else
241
- response.to_s[/path/] # Perform regular expression using path if not XML nor JSON TODO: Unit test
242
- end
243
- end
244
-
245
- # @return [Enumerable] List of values returned from path
246
- def values_from_path(response, path, attribute: nil)
247
- path = path.to_s
248
- case Interpreter.response_type_for(response)
249
- when :xml
250
- xpath_elements_for(response: response, xpath: path, attribute: attribute).map(&:inner_text)
251
- when :json
252
- json_path_values_for(response, path, attribute: attribute)
253
- else
254
- raise "Unable to interpret type of #{response.body}"
255
- end
256
- end
257
-
258
- # @return [Hash] Hash representing response body
259
- def to_hash(response)
260
- case Interpreter.response_type_for(response)
261
- when :xml
262
- parser = Nori.new(strip_namespaces: strip_namespaces?, convert_tags_to: ->(tag) { tag.snakecase.to_sym })
263
- parser.parse(response.body.to_s)
264
- when :json
265
- JSON.parse(response.body.to_s)
266
- else
267
- raise "Unable to interpret type of #{response.body}"
268
- end
269
- end
270
-
271
- private
272
-
273
- # Work out data to send based upon payload, template_name
274
- # @return [String] Payload to send in REST request
275
- def post_data(test_values)
276
- if test_values[:body]
277
- test_values[:payload] = JSON.generate(hash_used_in_request(test_values[:body])).to_s
278
- elsif @request_option == :template
279
- request_body = File.read(File.join(Soaspec.template_folder, template_name))
280
- ERB.new(request_body).result(binding)
281
- else
282
- test_values[:payload]
283
- end
284
- end
285
-
286
- # @return [Hash] Hash used in REST request based on data conversion
287
- def hash_used_in_request(override_hash)
288
- request = @default_hash.merge(override_hash)
289
- if pascal_keys?
290
- request.map { |k, v| [convert_to_pascal_case(k.to_s), v] }.to_h
291
- else
292
- request
293
- end
294
- end
295
-
296
- # Convenience methods for once off usage of a REST request
297
- class << self
298
-
299
- methods = %w[post patch put get delete]
300
-
301
- methods.each do |rest_method|
302
- # Make REST Exchange within this Handler context
303
- # @param [Hash] params Exchange parameters
304
- # @return [Exchange] Instance of Exchange class. Assertions are made by default on the response body
305
- define_method(rest_method) do |params = {}|
306
- # params ||= {}
307
- params[:name] ||= rest_method
308
- new(params[:name])
309
- Exchange.new(params[:name], method: rest_method.to_sym, **params)
310
- end
311
- end
312
- end
313
-
314
- end
1
+ require_relative 'exchange_handler'
2
+ require_relative 'rest_accessors'
3
+ require_relative '../core_ext/hash'
4
+ require_relative '../not_found_errors'
5
+ require_relative 'handler_accessors'
6
+ require_relative '../interpreter'
7
+ require 'json'
8
+ require 'jsonpath'
9
+ require 'nori'
10
+ require 'erb'
11
+
12
+ module Soaspec
13
+
14
+ # Wraps around Savon client defining default values dependent on the soap request
15
+ class RestHandler < ExchangeHandler
16
+ extend Soaspec::RestAccessors
17
+
18
+ # User used in making API calls
19
+ attr_accessor :api_username
20
+
21
+ # Set through following method. Base URL in REST requests.
22
+ def base_url_value
23
+ nil
24
+ end
25
+
26
+ # Headers used in RestClient
27
+ def rest_client_headers
28
+ {}
29
+ end
30
+
31
+ # Add values to here when extending this class to have default REST options.
32
+ # See rest client resource at https://github.com/rest-client/rest-client for details
33
+ # It's easier to set headers via 'headers' accessor rather than here
34
+ # @return [Hash] Options adding to & overriding defaults
35
+ def rest_resource_options
36
+ {
37
+ }
38
+ end
39
+
40
+ # Perform ERB on each header value
41
+ # @return [Hash] Hash from 'rest_client_headers' passed through ERB
42
+ def parse_headers
43
+ Hash[rest_client_headers.map { |k, header| [k, ERB.new(header).result(binding)] }]
44
+ end
45
+
46
+ # Setup object to handle communicating with a particular SOAP WSDL
47
+ # @param [Hash] options Options defining SOAP request. WSDL, authentication
48
+ def initialize(name = self.class.to_s, options = {})
49
+ raise "Base URL not set! Please set in class with 'base_url' method" unless base_url_value
50
+ @default_hash = {}
51
+ if name.is_a?(Hash) && options == {} # If name is not set
52
+ options = name
53
+ name = self.class.to_s
54
+ end
55
+ super
56
+ set_remove_key(options, :api_username)
57
+ set_remove_key(options, :default_hash)
58
+ set_remove_key(options, :template_name)
59
+ @init_options = options
60
+ end
61
+
62
+ # Convert snakecase to PascalCase
63
+ def convert_to_pascal_case(key)
64
+ return key if /[[:upper:]]/ =~ key[0] # If first character already capital, don't do conversion
65
+ key.split('_').map(&:capitalize).join
66
+ end
67
+
68
+ # Whether to convert each key in the request to PascalCase
69
+ # It will also auto convert simple XPath, JSONPath where '//' or '..' not specified
70
+ # @return Whether to convert to PascalCase
71
+ def pascal_keys?
72
+ false
73
+ end
74
+
75
+ # Initialize value of merged options
76
+ # @return [Hash] Hash of merged options
77
+ def init_merge_options
78
+ options = rest_resource_options
79
+ options[:headers] ||= {}
80
+ options[:headers].merge! parse_headers
81
+ if Soaspec.auto_oauth && respond_to?(:access_token)
82
+ options[:headers][:authorization] ||= ERB.new('Bearer <%= access_token %>').result(binding)
83
+ end
84
+ options.merge(@init_options)
85
+ end
86
+
87
+ # Used in together with Exchange request that passes such override parameters
88
+ # @param [Hash] override_parameters Params to characterize REST request
89
+ # @param_value [params] Extra parameters (E.g. headers)
90
+ # @param_value [suburl] URL appended to base_url of class
91
+ # @param_value [q] Query for REST
92
+ # @param_value [method] REST method (:get, :post, etc)
93
+ def make_request(override_parameters)
94
+ @merged_options ||= init_merge_options
95
+ test_values = override_parameters
96
+ test_values[:params] ||= {}
97
+ test_values[:method] ||= :post
98
+ test_values[:suburl] = test_values[:suburl].to_s if test_values[:suburl]
99
+ test_values[:params][:params] = test_values[:q] if test_values[:q] # Use q for query parameters. Nested :params is ugly and long
100
+ # In order for ERB to be calculated at correct time, the first time request is made, the resource should be created
101
+ @resource ||= RestClient::Resource.new(ERB.new(base_url_value).result(binding), @merged_options)
102
+
103
+ @resource_used = test_values[:suburl] ? @resource[test_values[:suburl]] : @resource
104
+
105
+ begin
106
+ response = case test_values[:method]
107
+ when :post, :patch, :put
108
+ Soaspec::SpecLogger.info("request body: #{post_data(test_values)}")
109
+ @resource_used.send(test_values[:method].to_s, post_data(test_values), test_values[:params])
110
+ else # :get, :delete
111
+ @resource_used.send(test_values[:method].to_s, test_values[:params])
112
+ end
113
+ rescue RestClient::ExceptionWithResponse => e
114
+ response = e.response
115
+ end
116
+ Soaspec::SpecLogger.info('response_headers: ' + response.headers.to_s)
117
+ Soaspec::SpecLogger.info('response_body: ' + response.to_s)
118
+ response
119
+ end
120
+
121
+ # @param [Hash] format Format of expected result.
122
+ # @return [Object] Generic body to be displayed in error messages
123
+ def response_body(response, format: :hash)
124
+ extract_hash response
125
+ end
126
+
127
+ # @return [Boolean] Whether response body includes String
128
+ def include_in_body?(response, expected)
129
+ response.body.include? expected
130
+ end
131
+
132
+ # @@return [Boolean] Whether the request found the desired value or not
133
+ def found?(response)
134
+ status_code_for(response) != 404
135
+ end
136
+
137
+ # Convert XML or JSON response into a Hash
138
+ # @param [String] response Response as a String (either in XML or JSON)
139
+ # @return [Hash]
140
+ def extract_hash(response)
141
+ raise ArgumentError("Empty Body. Can't assert on it") if response.body.empty?
142
+ case Interpreter.response_type_for response
143
+ when :json
144
+ converted = JSON.parse(response.body)
145
+ return converted.transform_keys_to_symbols if converted.is_a? Hash
146
+ return converted.map!(&:transform_keys_to_symbols) if converted.is_a? Array
147
+ raise 'Incorrect Type prodcued ' + converted.class
148
+ when :xml
149
+ parser = Nori.new(convert_tags_to: lambda { |tag| tag.snakecase.to_sym })
150
+ parser.parse(response.body)
151
+ else
152
+ raise "Neither XML nor JSON detected. It is #{type}. Don't know how to parse It is #{response.body}"
153
+ end
154
+ end
155
+
156
+ # @return [Boolean] Whether response contains expected value
157
+ def include_value?(response, expected)
158
+ extract_hash(response).include_value? expected
159
+ end
160
+
161
+ # @return [Boolean] Whether response body contains expected key
162
+ def include_key?(response, expected)
163
+ value_from_path(response, expected)
164
+ end
165
+
166
+ # @return [Integer] HTTP Status code for response
167
+ def status_code_for(response)
168
+ response.code
169
+ end
170
+
171
+ # Override this to specify elements that must be present in the response
172
+ # Will be used in 'success_scenarios' shared examples
173
+ # @return [Array] Array of symbols specifying element names
174
+ def mandatory_elements
175
+ []
176
+ end
177
+
178
+ # Override this to specify xpath results that must be present in the response
179
+ # Will be used in 'success_scenarios' shared examples
180
+ # @return [Hash] Hash of 'xpath' => 'expected value' pairs
181
+ def mandatory_xpath_values
182
+ {}
183
+ end
184
+
185
+ # Attributes set at the root XML element of SOAP request
186
+ def root_attributes
187
+ nil
188
+ end
189
+
190
+ # Returns the value at the provided xpath
191
+ # @param [RestClient::Response] response
192
+ # @param [String] xpath
193
+ # @return [Enumerable] Value inside element found through Xpath
194
+ def xpath_elements_for(response: nil, xpath: nil, attribute: nil)
195
+ raise ArgumentError unless response && xpath
196
+ raise "Can't perform XPATH if response is not XML" unless Interpreter.response_type_for(response) == :xml
197
+ xpath = "//*[@#{attribute}]" unless attribute.nil?
198
+ if xpath[0] != '/'
199
+ xpath = convert_to_pascal_case(xpath) if pascal_keys?
200
+ xpath = '//' + xpath
201
+ end
202
+ temp_doc = Nokogiri.parse(response.body).dup
203
+ if strip_namespaces? && !xpath.include?(':')
204
+ temp_doc.remove_namespaces!
205
+ temp_doc.xpath(xpath)
206
+ else
207
+ temp_doc.xpath(xpath, temp_doc.collect_namespaces)
208
+ end
209
+ end
210
+
211
+ # @return [Enumerable] List of values matching JSON path
212
+ def json_path_values_for(response, path, attribute: nil)
213
+ raise 'JSON does not support attributes' if attribute
214
+ if path[0] != '$'
215
+ path = convert_to_pascal_case(path) if pascal_keys?
216
+ path = '$..' + path
217
+ end
218
+ JsonPath.on(response.body, path)
219
+ end
220
+
221
+ # Based on a exchange, return the value at the provided xpath
222
+ # If the path does not begin with a '/', a '//' is added to it
223
+ # @param [Response] response
224
+ # @param [Object] path Xpath, JSONPath or other path identifying how to find element
225
+ # @param [String] attribute Generic attribute to find. Will override path
226
+ # @return [String] Value at Xpath
227
+ def value_from_path(response, path, attribute: nil)
228
+ path = path.to_s
229
+ case Interpreter.response_type_for(response)
230
+ when :xml
231
+ result = xpath_elements_for(response: response, xpath: path, attribute: attribute).first
232
+ raise NoElementAtPath, "No value at Xpath '#{path}'" unless result
233
+ return result.inner_text if attribute.nil?
234
+ return result.attributes[attribute].inner_text
235
+ when :json
236
+ paths_to_check = path.split(',')
237
+ matching_values = paths_to_check.collect do |path_to_check|
238
+ json_path_values_for(response, path_to_check, attribute: attribute)
239
+ end.reject(&:empty?)
240
+ raise NoElementAtPath, "Path '#{path}' not found in '#{response.body}'" if matching_values.empty?
241
+ matching_values.first.first
242
+ when :hash
243
+ response.dig(path.split('.')) # Use path as Hash dig expression separating params via '.' TODO: Unit test
244
+ else
245
+ response.to_s[/path/] # Perform regular expression using path if not XML nor JSON TODO: Unit test
246
+ end
247
+ end
248
+
249
+ # @return [Enumerable] List of values returned from path
250
+ def values_from_path(response, path, attribute: nil)
251
+ path = path.to_s
252
+ case Interpreter.response_type_for(response)
253
+ when :xml
254
+ xpath_elements_for(response: response, xpath: path, attribute: attribute).map(&:inner_text)
255
+ when :json
256
+ json_path_values_for(response, path, attribute: attribute)
257
+ else
258
+ raise "Unable to interpret type of #{response.body}"
259
+ end
260
+ end
261
+
262
+ # @return [Hash] Hash representing response body
263
+ def to_hash(response)
264
+ case Interpreter.response_type_for(response)
265
+ when :xml
266
+ parser = Nori.new(strip_namespaces: strip_namespaces?, convert_tags_to: ->(tag) { tag.snakecase.to_sym })
267
+ parser.parse(response.body.to_s)
268
+ when :json
269
+ JSON.parse(response.body.to_s)
270
+ else
271
+ raise "Unable to interpret type of #{response.body}"
272
+ end
273
+ end
274
+
275
+ private
276
+
277
+ # Work out data to send based upon payload, template_name
278
+ # @return [String] Payload to send in REST request
279
+ def post_data(test_values)
280
+ if test_values[:body]
281
+ test_values[:payload] = JSON.generate(hash_used_in_request(test_values[:body])).to_s
282
+ elsif @request_option == :template
283
+ request_body = File.read(File.join(Soaspec.template_folder, template_name))
284
+ ERB.new(request_body).result(binding)
285
+ else
286
+ test_values[:payload]
287
+ end
288
+ end
289
+
290
+ # @return [Hash] Hash used in REST request based on data conversion
291
+ def hash_used_in_request(override_hash)
292
+ request = @default_hash.merge(override_hash)
293
+ if pascal_keys?
294
+ request.map { |k, v| [convert_to_pascal_case(k.to_s), v] }.to_h
295
+ else
296
+ request
297
+ end
298
+ end
299
+
300
+ # Convenience methods for once off usage of a REST request
301
+ class << self
302
+
303
+ methods = %w[post patch put get delete]
304
+
305
+ methods.each do |rest_method|
306
+ # Make REST Exchange within this Handler context
307
+ # @param [Hash] params Exchange parameters
308
+ # @return [Exchange] Instance of Exchange class. Assertions are made by default on the response body
309
+ define_method(rest_method) do |params = {}|
310
+ # params ||= {}
311
+ params[:name] ||= rest_method
312
+ new(params[:name])
313
+ Exchange.new(params[:name], method: rest_method.to_sym, **params)
314
+ end
315
+ end
316
+ end
317
+
318
+ end
315
319
  end