soaspec 0.0.83 → 0.0.84

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