soaspec 0.2.24 → 0.2.25

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 (83) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +15 -15
  3. data/.gitlab-ci.yml +51 -33
  4. data/.rspec +3 -3
  5. data/.rubocop.yml +2 -2
  6. data/CODE_OF_CONDUCT.md +74 -74
  7. data/ChangeLog +588 -577
  8. data/Gemfile +6 -6
  9. data/LICENSE.txt +21 -21
  10. data/README.md +230 -230
  11. data/Rakefile +50 -42
  12. data/Todo.md +15 -15
  13. data/exe/soaspec +137 -123
  14. data/exe/xml_to_yaml_file +42 -42
  15. data/lib/soaspec.rb +103 -101
  16. data/lib/soaspec/core_ext/hash.rb +42 -35
  17. data/lib/soaspec/cucumber/generic_steps.rb +85 -85
  18. data/lib/soaspec/demo.rb +4 -4
  19. data/lib/soaspec/exchange/exchange.rb +117 -111
  20. data/lib/soaspec/exchange/exchange_extractor.rb +83 -83
  21. data/lib/soaspec/exchange/exchange_properties.rb +26 -26
  22. data/lib/soaspec/exchange/exchange_repeater.rb +19 -19
  23. data/lib/soaspec/exchange/request_builder.rb +68 -68
  24. data/lib/soaspec/exchange/variable_storer.rb +22 -22
  25. data/lib/soaspec/exchange_handlers/exchange_handler.rb +130 -126
  26. data/lib/soaspec/exchange_handlers/handler_accessors.rb +130 -130
  27. data/lib/soaspec/exchange_handlers/request/rest_request.rb +49 -0
  28. data/lib/soaspec/exchange_handlers/request/soap_request.rb +39 -0
  29. data/lib/soaspec/exchange_handlers/response_extractor.rb +82 -82
  30. data/lib/soaspec/exchange_handlers/rest_exchanger_factory.rb +109 -109
  31. data/lib/soaspec/exchange_handlers/rest_handler.rb +287 -259
  32. data/lib/soaspec/exchange_handlers/rest_methods.rb +63 -44
  33. data/lib/soaspec/exchange_handlers/rest_parameters.rb +90 -86
  34. data/lib/soaspec/exchange_handlers/rest_parameters_defaults.rb +40 -21
  35. data/lib/soaspec/exchange_handlers/soap_handler.rb +239 -235
  36. data/lib/soaspec/exe_helpers.rb +92 -92
  37. data/lib/soaspec/generate_server.rb +46 -37
  38. data/lib/soaspec/generator/.rspec.erb +5 -5
  39. data/lib/soaspec/generator/.travis.yml.erb +5 -5
  40. data/lib/soaspec/generator/Gemfile.erb +8 -8
  41. data/lib/soaspec/generator/README.md.erb +29 -29
  42. data/lib/soaspec/generator/Rakefile.erb +19 -19
  43. data/lib/soaspec/generator/config/data/default.yml.erb +2 -2
  44. data/lib/soaspec/generator/css/bootstrap.css +6833 -6833
  45. data/lib/soaspec/generator/features/support/env.rb.erb +3 -0
  46. data/lib/soaspec/generator/generate_exchange.html.erb +47 -35
  47. data/lib/soaspec/generator/lib/blz_service.rb.erb +26 -26
  48. data/lib/soaspec/generator/lib/dynamic_class_content.rb.erb +12 -12
  49. data/lib/soaspec/generator/lib/new_rest_service.rb.erb +56 -51
  50. data/lib/soaspec/generator/lib/new_soap_service.rb.erb +29 -29
  51. data/lib/soaspec/generator/lib/package_service.rb.erb +2 -2
  52. data/lib/soaspec/generator/lib/shared_example.rb.erb +8 -8
  53. data/lib/soaspec/generator/spec/dynamic_soap_spec.rb.erb +12 -12
  54. data/lib/soaspec/generator/spec/rest_spec.rb.erb +9 -9
  55. data/lib/soaspec/generator/spec/soap_spec.rb.erb +51 -51
  56. data/lib/soaspec/generator/spec/spec_helper.rb.erb +23 -23
  57. data/lib/soaspec/generator/template/soap_template.xml +6 -6
  58. data/lib/soaspec/indifferent_hash.rb +7 -7
  59. data/lib/soaspec/interpreter.rb +39 -39
  60. data/lib/soaspec/matchers.rb +114 -114
  61. data/lib/soaspec/not_found_errors.rb +13 -13
  62. data/lib/soaspec/o_auth2.rb +128 -128
  63. data/lib/soaspec/soaspec_shared_examples.rb +24 -24
  64. data/lib/soaspec/spec_logger.rb +122 -121
  65. data/lib/soaspec/template_reader.rb +28 -28
  66. data/lib/soaspec/test_server/bank.wsdl +90 -90
  67. data/lib/soaspec/test_server/get_bank.rb +164 -164
  68. data/lib/soaspec/test_server/id_manager.rb +39 -39
  69. data/lib/soaspec/test_server/invoices.rb +27 -27
  70. data/lib/soaspec/test_server/namespace.xml +14 -14
  71. data/lib/soaspec/test_server/note.xml +5 -5
  72. data/lib/soaspec/test_server/puppy_service.rb +19 -19
  73. data/lib/soaspec/test_server/test_attribute.rb +12 -12
  74. data/lib/soaspec/test_server/test_namespace.rb +12 -12
  75. data/lib/soaspec/version.rb +4 -4
  76. data/lib/soaspec/virtual_server.rb +174 -174
  77. data/lib/soaspec/wait.rb +41 -41
  78. data/lib/soaspec/wsdl_generator.rb +215 -215
  79. data/soaspec.gemspec +56 -53
  80. data/test.wsdl +116 -116
  81. data/test.xml +10 -10
  82. data/test_wsdl.rb +41 -41
  83. metadata +38 -6
@@ -1,109 +1,109 @@
1
- module Soaspec
2
- # Convenience methods for once off usage of a REST request
3
- module RestExchangeFactory
4
- # Make REST Exchange with 'post' method within this Handler context
5
- # @param [Hash, String] params Exchange parameters. If String is used it will be for the request payload
6
- # @option params [String] :name Name to appear in traffic logs
7
- # @option params [Hash] :params Extra parameters (E.g. headers)
8
- # @option params [String] :suburl URL appended to base_url of class
9
- # @option params [Hash] :q Query for REST
10
- # Following are for the body of the request
11
- # @option params [Hash] :body Hash to be converted to JSON in request body
12
- # @option params [String] :payload String to be passed directly in request body
13
- # @option params [String] :template_name Path to file to be read via ERB and passed in request body
14
- # @return [Exchange] Instance of Exchange class. Assertions are made by default on the response body
15
- def post(params = {})
16
- perform_exchange_with(:post, params)
17
- end
18
-
19
- # Make REST Exchange with 'patch' method within this Handler context
20
- # @param [Hash, String] params Exchange parameters. If String is used it will be for the request payload
21
- # @option params [String] :name Name to appear in traffic logs
22
- # @option params [Hash] :params Extra parameters (E.g. headers)
23
- # @option params [String] suburl URL appended to base_url of class
24
- # @option params [Hash] :q Query for REST
25
- # Following are for the body of the request
26
- # @option params [Hash] :body Hash to be converted to JSON in request body
27
- # @option params [String] :payload String to be passed directly in request body
28
- # @option params [String] :template_name Path to file to be read via ERB and passed in request body
29
- # @return [Exchange] Instance of Exchange class. Assertions are made by default on the response body
30
- def patch(params)
31
- perform_exchange_with(:patch, params)
32
- end
33
-
34
- # Make REST Exchange with 'put' method within this Handler context
35
- # @param [Hash, String] params Exchange parameters. If String is used it will be for the request payload
36
- # @option params [String] :name Name to appear in traffic logs
37
- # @option params [Hash] :params Extra parameters (E.g. headers)
38
- # @option params [String] :suburl URL appended to base_url of class
39
- # @option params [Hash] :q Query for REST
40
- # Following are for the body of the request
41
- # @option params [Hash] :body Hash to be converted to JSON in request body
42
- # @option params [String] :payload String to be passed directly in request body
43
- # @option params [String] :template_name Path to file to be read via ERB and passed in request body
44
- # @return [Exchange] Instance of Exchange class. Assertions are made by default on the response body
45
- def put(params = {})
46
- perform_exchange_with(:put, params)
47
- end
48
-
49
- # Make REST Exchange with 'get' method within this Handler context.
50
- # If merely a string is passed it will be used as the URL appended to base_url (same as suburl). Otherwise a Hash is expected
51
- # @param [Hash, String] params Exchange parameters. If String is used it will be for suburl
52
- # @option params [String] :name Name to appear in traffic logs
53
- # @option params [String] :suburl URL appended to base_url of class
54
- # @option params [Hash] :params Extra parameters (E.g. headers)
55
- # @option params [Hash] :q Query for REST
56
- # @return [Exchange] Instance of Exchange class. Assertions are made by default on the response body
57
- def get(params = {})
58
- perform_exchange_with(:get, params)
59
- end
60
-
61
- # Make REST Exchange with 'delete' method within this Handler context.
62
- # If merely a string is passed it will be used as the URL appended to base_url (same as suburl). Otherwise a Hash is expected
63
- # @param [Hash, String] params Exchange parameters. If String is used it will be for suburl
64
- # @option params [String] :name Name to appear in traffic logs
65
- # @option params [String] :suburl URL appended to base_url of class
66
- # @option params [Hash] :q Query for REST
67
- # @option params [Hash] :params Extra parameters (E.g. headers)
68
- # @return [Exchange] Instance of Exchange class. Assertions are made by default on the response body
69
- def delete(params = {})
70
- perform_exchange_with(:delete, params)
71
- end
72
-
73
- private
74
-
75
- # Make REST Exchange within this Handler context
76
- # @param [Symbol] rest_method HTTP rest method to use
77
- # @param [Hash, String] params Exchange parameters.
78
- # @return [Exchange] Instance of Exchange class. Assertions are made by default on the response body
79
- def perform_exchange_with(rest_method, params = {})
80
- params = determine_params_for(rest_method, params)
81
- params[:name] ||= rest_method.to_s
82
- exchange_params = { name: params[:name] }
83
- if params[:template_name]
84
- exchange_params[:template_name] = params[:template_name]
85
- params.delete :template_name
86
- end
87
- new(exchange_params)
88
- exchange = Exchange.new(params[:name], method: rest_method, **params)
89
- yield exchange if block_given?
90
- exchange
91
- end
92
-
93
- # @param [Symbol] method HTTP rest method to use
94
- # @param [Hash, String] params Exchange parameters.
95
- # @return [Hash] Exchange Parameters after setting shorthand parameters
96
- def determine_params_for(method, params)
97
- return params if params.is_a? Hash
98
-
99
- case method
100
- when :get, :delete
101
- { suburl: params.to_s }
102
- when :post, :put, :patch
103
- { payload: params.to_s }
104
- else
105
- raise "'#{params}' needs to be a 'Hash' but is a #{params.class}"
106
- end
107
- end
108
- end
109
- end
1
+ module Soaspec
2
+ # Convenience methods for once off usage of a REST request
3
+ module RestExchangeFactory
4
+ # Make REST Exchange with 'post' method within this Handler context
5
+ # @param [Hash, String] params Exchange parameters. If String is used it will be for the request payload
6
+ # @option params [String] :name Name to appear in traffic logs
7
+ # @option params [Hash] :params Extra parameters (E.g. headers)
8
+ # @option params [String] :suburl URL appended to base_url of class
9
+ # @option params [Hash] :q Query for REST
10
+ # Following are for the body of the request
11
+ # @option params [Hash] :body Hash to be converted to JSON in request body
12
+ # @option params [String] :payload String to be passed directly in request body
13
+ # @option params [String] :template_name Path to file to be read via ERB and passed in request body
14
+ # @return [Exchange] Instance of Exchange class. Assertions are made by default on the response body
15
+ def post(params = {})
16
+ perform_exchange_with(:post, params)
17
+ end
18
+
19
+ # Make REST Exchange with 'patch' method within this Handler context
20
+ # @param [Hash, String] params Exchange parameters. If String is used it will be for the request payload
21
+ # @option params [String] :name Name to appear in traffic logs
22
+ # @option params [Hash] :params Extra parameters (E.g. headers)
23
+ # @option params [String] suburl URL appended to base_url of class
24
+ # @option params [Hash] :q Query for REST
25
+ # Following are for the body of the request
26
+ # @option params [Hash] :body Hash to be converted to JSON in request body
27
+ # @option params [String] :payload String to be passed directly in request body
28
+ # @option params [String] :template_name Path to file to be read via ERB and passed in request body
29
+ # @return [Exchange] Instance of Exchange class. Assertions are made by default on the response body
30
+ def patch(params)
31
+ perform_exchange_with(:patch, params)
32
+ end
33
+
34
+ # Make REST Exchange with 'put' method within this Handler context
35
+ # @param [Hash, String] params Exchange parameters. If String is used it will be for the request payload
36
+ # @option params [String] :name Name to appear in traffic logs
37
+ # @option params [Hash] :params Extra parameters (E.g. headers)
38
+ # @option params [String] :suburl URL appended to base_url of class
39
+ # @option params [Hash] :q Query for REST
40
+ # Following are for the body of the request
41
+ # @option params [Hash] :body Hash to be converted to JSON in request body
42
+ # @option params [String] :payload String to be passed directly in request body
43
+ # @option params [String] :template_name Path to file to be read via ERB and passed in request body
44
+ # @return [Exchange] Instance of Exchange class. Assertions are made by default on the response body
45
+ def put(params = {})
46
+ perform_exchange_with(:put, params)
47
+ end
48
+
49
+ # Make REST Exchange with 'get' method within this Handler context.
50
+ # If merely a string is passed it will be used as the URL appended to base_url (same as suburl). Otherwise a Hash is expected
51
+ # @param [Hash, String] params Exchange parameters. If String is used it will be for suburl
52
+ # @option params [String] :name Name to appear in traffic logs
53
+ # @option params [String] :suburl URL appended to base_url of class
54
+ # @option params [Hash] :params Extra parameters (E.g. headers)
55
+ # @option params [Hash] :q Query for REST
56
+ # @return [Exchange] Instance of Exchange class. Assertions are made by default on the response body
57
+ def get(params = {})
58
+ perform_exchange_with(:get, params)
59
+ end
60
+
61
+ # Make REST Exchange with 'delete' method within this Handler context.
62
+ # If merely a string is passed it will be used as the URL appended to base_url (same as suburl). Otherwise a Hash is expected
63
+ # @param [Hash, String] params Exchange parameters. If String is used it will be for suburl
64
+ # @option params [String] :name Name to appear in traffic logs
65
+ # @option params [String] :suburl URL appended to base_url of class
66
+ # @option params [Hash] :q Query for REST
67
+ # @option params [Hash] :params Extra parameters (E.g. headers)
68
+ # @return [Exchange] Instance of Exchange class. Assertions are made by default on the response body
69
+ def delete(params = {})
70
+ perform_exchange_with(:delete, params)
71
+ end
72
+
73
+ private
74
+
75
+ # Make REST Exchange within this Handler context
76
+ # @param [Symbol] rest_method HTTP rest method to use
77
+ # @param [Hash, String] params Exchange parameters.
78
+ # @return [Exchange] Instance of Exchange class. Assertions are made by default on the response body
79
+ def perform_exchange_with(rest_method, params = {})
80
+ params = determine_params_for(rest_method, params)
81
+ params[:name] ||= rest_method.to_s
82
+ exchange_params = { name: params[:name] }
83
+ if params[:template_name]
84
+ exchange_params[:template_name] = params[:template_name]
85
+ params.delete :template_name
86
+ end
87
+ new(exchange_params)
88
+ exchange = Exchange.new(params[:name], method: rest_method, **params)
89
+ yield exchange if block_given?
90
+ exchange
91
+ end
92
+
93
+ # @param [Symbol] method HTTP rest method to use
94
+ # @param [Hash, String] params Exchange parameters.
95
+ # @return [Hash] Exchange Parameters after setting shorthand parameters
96
+ def determine_params_for(method, params)
97
+ return params if params.is_a? Hash
98
+
99
+ case method
100
+ when :get, :delete
101
+ { suburl: params.to_s }
102
+ when :post, :put, :patch
103
+ { payload: params.to_s }
104
+ else
105
+ raise "'#{params}' needs to be a 'Hash' but is a #{params.class}"
106
+ end
107
+ end
108
+ end
109
+ end
@@ -1,259 +1,287 @@
1
- require_relative 'exchange_handler'
2
- require_relative 'rest_parameters'
3
- require_relative 'rest_parameters_defaults'
4
- require_relative 'rest_exchanger_factory'
5
- require_relative '../core_ext/hash'
6
- require_relative '../not_found_errors'
7
- require_relative 'handler_accessors'
8
- require_relative '../interpreter'
9
- require_relative 'response_extractor'
10
- require 'json'
11
- require 'jsonpath'
12
- require 'nori'
13
- require 'erb'
14
-
15
- module Soaspec
16
- # Wraps around Savon client defining default values dependent on the soap request
17
- class RestHandler < ExchangeHandler
18
- include ResponseExtractor
19
- extend Soaspec::RestParameters
20
- include Soaspec::RestParametersDefaults
21
- extend Soaspec::RestExchangeFactory
22
-
23
- # User used in making API calls
24
- attr_accessor :api_username
25
-
26
- # Setup object to handle communicating with a particular SOAP WSDL
27
- # @param [Hash] options Options defining REST request. base_url, default_hash
28
- def initialize(name = self.class.to_s, options = {})
29
- raise "Base URL not set! Please set in class with 'base_url' method" unless base_url_value
30
-
31
- if name.is_a?(Hash) && options == {} # If name is not set, use first parameter as the options hash
32
- options = name
33
- name = self.class.to_s
34
- end
35
- super
36
- set_remove_keys(options, %i[api_username default_hash template_name])
37
- @init_options = options
38
- init_merge_options # Call this to verify any issues with options on creating object
39
- end
40
-
41
- # Used in together with Exchange request that passes such override parameters
42
- # @param [Hash] override_parameters Params to characterize REST request
43
- # @option override_parameters [Hash] :params Extra parameters (E.g. headers)
44
- # @option override_parameters [String] suburl URL appended to base_url of class
45
- # @option override_parameters [Hash] :q Query for REST
46
- # @option override_parameters [Symbol] :method REST method (:get, :post, :patch, etc)
47
- # Following are for the body of the request
48
- # @option override_parameters [Hash] :body Hash to be converted to JSON in request body
49
- # @option override_parameters [String] :payload String to be passed directly in request body
50
- # @option override_parameters [String] :template_name Path to file to be read via ERB and passed in request body
51
- # @return [RestClient::Response] Response from making request
52
- def make_request(override_parameters)
53
- @merged_options ||= init_merge_options
54
- test_values = override_parameters
55
- test_values[:params] ||= {}
56
- test_values[:method] ||= :post
57
- test_values[:suburl] = test_values[:suburl].to_s if test_values[:suburl]
58
- test_values[:params][:params] = test_values[:q] if test_values[:q] # Use q for query parameters. Nested :params is ugly and long
59
- # In order for ERB to be calculated at correct time, the first time request is made, the resource should be created
60
- @resource ||= RestClient::Resource.new(ERB.new(base_url_value).result(binding), @merged_options)
61
-
62
- @resource_used = test_values[:suburl] ? @resource[test_values[:suburl]] : @resource
63
-
64
- begin
65
- response = case test_values[:method]
66
- when :post, :patch, :put
67
- Soaspec::SpecLogger.info("request body: #{post_data(test_values)}")
68
- @resource_used.send(test_values[:method].to_s, post_data(test_values), test_values[:params])
69
- else # :get, :delete
70
- @resource_used.send(test_values[:method].to_s, test_values[:params])
71
- end
72
- rescue RestClient::ExceptionWithResponse => e
73
- response = e.response
74
- end
75
- Soaspec::SpecLogger.info(["response_headers: #{response.headers}", "response_body: #{response}"])
76
- response
77
- end
78
-
79
- # Add values to here when extending this class to have default REST options.
80
- # See rest client resource at https://github.com/rest-client/rest-client for details
81
- # It's easier to set headers via 'headers' accessor rather than here
82
- # @return [Hash] Options adding to & overriding defaults
83
- def rest_resource_options
84
- {
85
- }
86
- end
87
-
88
- # Perform ERB on each header value
89
- # @return [Hash] Hash from 'rest_client_headers' passed through ERB
90
- def parse_headers
91
- Hash[rest_client_headers.map do |header_name, header_value|
92
- raise ArgumentError, "Header '#{header_name}' is null. Headers are #{rest_client_headers}" if header_value.nil?
93
-
94
- [header_name, ERB.new(header_value).result(binding)]
95
- end]
96
- end
97
-
98
- # Initialize value of merged options
99
- # @return [Hash] Hash of merged options
100
- def init_merge_options
101
- options = rest_resource_options
102
- options.merge! basic_auth_params if respond_to? :basic_auth_params
103
- options[:headers] ||= {}
104
- options[:headers].merge! parse_headers
105
- options[:headers][:authorization] ||= ERB.new('Bearer <%= access_token %>').result(binding) if Soaspec.auto_oauth && respond_to?(:access_token)
106
- options.merge(@init_options)
107
- end
108
-
109
- # @param [Hash] format Format of expected result.
110
- # @return [Object] Generic body to be displayed in error messages
111
- def response_body(response, format: :hash)
112
- extract_hash response
113
- end
114
-
115
- # @return [Boolean] Whether response body includes String
116
- def include_in_body?(response, expected)
117
- response.body.include? expected
118
- end
119
-
120
- # @@return [Boolean] Whether the request found the desired value or not
121
- def found?(response)
122
- status_code_for(response) != 404
123
- end
124
-
125
- # @return [Boolean] Whether response contains expected value
126
- def include_value?(response, expected)
127
- extract_hash(response).include_value? expected
128
- end
129
-
130
- # @return [Boolean] Whether response body contains expected key
131
- def include_key?(response, expected)
132
- value_from_path(response, expected)
133
- end
134
-
135
- # @return [Integer] HTTP Status code for response
136
- def status_code_for(response)
137
- response.code
138
- end
139
-
140
- # Returns the value at the provided xpath
141
- # @param [RestClient::Response] response
142
- # @param [String] xpath Path to find elements from
143
- # @param [String] attribute Attribute to find path for
144
- # @return [Enumerable] Value inside element found through Xpath
145
- def xpath_elements_for(response: nil, xpath: nil, attribute: nil)
146
- raise ArgumentError unless response && xpath
147
- raise "Can't perform XPATH if response is not XML" unless Interpreter.response_type_for(response) == :xml
148
-
149
- xpath = prefix_xpath(xpath, attribute)
150
- temp_doc = Nokogiri.parse(response.body).dup
151
- if strip_namespaces? && !xpath.include?(':')
152
- temp_doc.remove_namespaces!
153
- temp_doc.xpath(xpath)
154
- else
155
- temp_doc.xpath(xpath, temp_doc.collect_namespaces)
156
- end
157
- end
158
-
159
- # @return [Enumerable] List of values matching JSON path
160
- def json_path_values_for(response, path, attribute: nil)
161
- raise 'JSON does not support attributes' if attribute
162
-
163
- JsonPath.on(response.body, path)
164
- end
165
-
166
- # Calculate all JSON path values based on rules. ',', pascal_case
167
- # @param [RestClient::Response] response Response from API
168
- # @param [Object] path Xpath, JSONPath or other path identifying how to find element
169
- # @param [String] attribute Generic attribute to find. Will override path
170
- # @param [Boolean] not_empty Whether to fail if result is empty
171
- # @return [Array] Paths to check as first and matching values (List of values matching JSON Path) as second
172
- def calculated_json_path_matches(path, response, attribute, not_empty: false)
173
- path = add_pascal_path(path)
174
- paths_to_check = path.split(',')
175
- paths_to_check = paths_to_check.map { |path_to_check| prefix_json_path(path_to_check) }
176
- matching_values = paths_to_check.collect do |path_to_check|
177
- json_path_values_for(response, path_to_check, attribute: attribute)
178
- end.reject(&:empty?)
179
- raise NoElementAtPath, "No value at JSONPath '#{paths_to_check}' in '#{response.body}'" if matching_values.empty? && not_empty
180
-
181
- matching_values.first
182
- end
183
-
184
- # Based on a exchange, return the value at the provided xpath
185
- # If the path does not begin with a '/', a '//' is added to it
186
- # @param [RestClient::Response] response Response from API
187
- # @param [Object] path Xpath, JSONPath or other path identifying how to find element
188
- # @param [String] attribute Generic attribute to find. Will override path
189
- # @return [String] Value at Xpath
190
- def value_from_path(response, path, attribute: nil)
191
- path = path.to_s
192
- case Interpreter.response_type_for(response)
193
- when :xml
194
- result = xpath_elements_for(response: response, xpath: path, attribute: attribute).first
195
- raise NoElementAtPath, "No value at Xpath '#{prefix_xpath(path, attribute)}' in '#{response.body}'" unless result
196
- return result.inner_text if attribute.nil?
197
-
198
- return result.attributes[attribute].inner_text
199
- when :json
200
- matching_values = calculated_json_path_matches(path, response, attribute, not_empty: true)
201
- matching_values.first
202
- else # Assume this is a String
203
- raise NoElementAtPath, 'Response is empty' if response.to_s.empty?
204
-
205
- response.to_s[/#{path}/] # Perform regular expression using path if not XML nor JSON
206
- end
207
- end
208
-
209
- # @return [Enumerable] List of values returned from path
210
- def values_from_path(response, path, attribute: nil)
211
- path = path.to_s
212
- case Interpreter.response_type_for(response)
213
- when :xml
214
- xpath_elements_for(response: response, xpath: path, attribute: attribute).map(&:inner_text)
215
- when :json
216
- result = calculated_json_path_matches(path, response, attribute)
217
- result || []
218
- # json_path_values_for(response, path, attribute: attribute)
219
- else
220
- raise "Unable to interpret type of #{response.body}"
221
- end
222
- end
223
-
224
- # @return [RestClient::Request] Request of API call. Either intended request or actual request
225
- def request(response)
226
- return 'Request not yet sent' if response.nil?
227
-
228
- response.request
229
- end
230
-
231
- private
232
-
233
- # Work out data to send based upon payload, template_name, or body
234
- # @return [String] Payload to send in REST request
235
- def post_data(test_values)
236
- data = if @request_option == :hash && !test_values[:payload]
237
- test_values[:payload] = JSON.generate(hash_used_in_request(test_values[:body])).to_s
238
- elsif @request_option == :template
239
- test_values = test_values[:body].dup if test_values[:body]
240
- test_values = IndifferentHash.new(test_values) # Allow test_values to be either Symbol or String
241
- Soaspec::TemplateReader.new.render_body(template_name, binding)
242
- else
243
- test_values[:payload]
244
- end
245
- # Soaspec::SpecLogger.info "Request Empty for '#{@request_option}'" if data.strip.empty?
246
- data
247
- end
248
-
249
- # @return [Hash] Hash used in REST request based on data conversion
250
- def hash_used_in_request(override_hash)
251
- request = override_hash ? @default_hash.merge(override_hash) : @default_hash
252
- if pascal_keys?
253
- request.map { |k, v| [convert_to_pascal_case(k.to_s), v] }.to_h
254
- else
255
- request
256
- end
257
- end
258
- end
259
- end
1
+ require_relative 'exchange_handler'
2
+ require_relative 'rest_parameters'
3
+ require_relative 'rest_parameters_defaults'
4
+ require_relative 'rest_exchanger_factory'
5
+ require_relative '../core_ext/hash'
6
+ require_relative '../not_found_errors'
7
+ require_relative 'handler_accessors'
8
+ require_relative '../interpreter'
9
+ require_relative 'response_extractor'
10
+ require_relative 'request/rest_request'
11
+ require 'json'
12
+ require 'jsonpath'
13
+ require 'nori'
14
+ require 'erb'
15
+
16
+ module Soaspec
17
+ # Wraps around Savon client defining default values dependent on the soap request
18
+ class RestHandler < ExchangeHandler
19
+ include ResponseExtractor
20
+ extend Soaspec::RestParameters
21
+ include Soaspec::RestParametersDefaults
22
+ extend Soaspec::RestExchangeFactory
23
+
24
+ # User used in making API calls
25
+ attr_accessor :api_username
26
+
27
+ # Setup object to handle communicating with a particular SOAP WSDL
28
+ # @param [Hash] options Options defining REST request. base_url, default_hash
29
+ def initialize(name = self.class.to_s, options = {})
30
+ raise "Base URL not set! Please set in class with 'base_url' method" unless base_url_value
31
+
32
+ if name.is_a?(Hash) && options == {} # If name is not set, use first parameter as the options hash
33
+ options = name
34
+ name = self.class.to_s
35
+ end
36
+ super
37
+ set_remove_keys(options, %i[api_username default_hash template_name])
38
+ @init_options = options
39
+ init_merge_options # Call this to verify any issues with options on creating object
40
+ end
41
+
42
+ # @return [Boolean] Whether REST method should have a payload
43
+ def payload?(overall_params)
44
+ case overall_params[:method]
45
+ when :post, :patch, :put
46
+ true
47
+ else
48
+ false
49
+ end
50
+ end
51
+
52
+ # @todo Use this in actually making the request
53
+ # @return [RestRequest] Parameters used in making a request
54
+ def request_parameters(override_parameters)
55
+ overall_params = interpret_parameters(override_parameters)
56
+ request = { overall: overall_params, options: init_merge_options }
57
+ request[:body] = post_data(overall_params) if payload?(overall_params)
58
+ RestRequest.new(overall_params, init_merge_options, payload?(overall_params) ? post_data(overall_params) : nil)
59
+ end
60
+
61
+ # Interpret REST parameters given provided parameters and adding defaults, making
62
+ # transformations
63
+ #
64
+ # @param [Hash] request_parameters Parameters used in making a request
65
+ # @return [Hash] Request parameters merged with default values
66
+ def interpret_parameters(request_parameters)
67
+ request_parameters[:params] ||= {}
68
+ request_parameters[:method] ||= :post
69
+ request_parameters[:suburl] = request_parameters[:suburl].to_s if request_parameters[:suburl]
70
+ # Use q for query parameters. Nested :params is ugly, long and unclear
71
+ request_parameters[:params][:params] = request_parameters[:q] if request_parameters[:q]
72
+ request_parameters
73
+ end
74
+
75
+ # Used in together with Exchange request that passes such override parameters
76
+ # @param [Hash] override_parameters Params to characterize REST request
77
+ # @option override_parameters [Hash] :params Extra parameters (E.g. headers)
78
+ # @option override_parameters [String] suburl URL appended to base_url of class
79
+ # @option override_parameters [Hash] :q Query for REST
80
+ # @option override_parameters [Symbol] :method REST method (:get, :post, :patch, etc)
81
+ # Following are for the body of the request
82
+ # @option override_parameters [Hash] :body Hash to be converted to JSON in request body
83
+ # @option override_parameters [String] :payload String to be passed directly in request body
84
+ # @option override_parameters [String] :template_name Path to file to be read via ERB and passed in request body
85
+ # @return [RestClient::Response] Response from making request
86
+ def make_request(override_parameters)
87
+ @merged_options ||= init_merge_options # TODO: Is this var needed? Can method be passed to resource creation?
88
+ test_values = interpret_parameters override_parameters
89
+ # In order for ERB to be calculated at correct time, the first time request is made, the resource should be created
90
+ @resource ||= RestClient::Resource.new(ERB.new(base_url_value).result(binding), @merged_options)
91
+
92
+ @resource_used = test_values[:suburl] ? @resource[test_values[:suburl]] : @resource
93
+
94
+ begin
95
+ response = case test_values[:method]
96
+ when :post, :patch, :put
97
+ Soaspec::SpecLogger.info("request body: #{post_data(test_values)}")
98
+ @resource_used.send(test_values[:method].to_s, post_data(test_values), test_values[:params])
99
+ else # :get, :delete
100
+ @resource_used.send(test_values[:method].to_s, test_values[:params])
101
+ end
102
+ rescue RestClient::ExceptionWithResponse => e
103
+ response = e.response
104
+ end
105
+ Soaspec::SpecLogger.info(["response_headers: #{response.headers}", "response_body: #{response}"])
106
+ response
107
+ end
108
+
109
+ # Add values to here when extending this class to have default REST options.
110
+ # See rest client resource at https://github.com/rest-client/rest-client for details
111
+ # It's easier to set headers via 'headers' accessor rather than here
112
+ # @return [Hash] Options adding to & overriding defaults
113
+ def rest_resource_options
114
+ {}
115
+ end
116
+
117
+ # Perform ERB on each header value
118
+ # @return [Hash] Hash from 'rest_client_headers' passed through ERB
119
+ def parse_headers
120
+ Hash[rest_client_headers.map do |header_name, header_value|
121
+ raise ArgumentError, "Header '#{header_name}' is null. Headers are #{rest_client_headers}" if header_value.nil?
122
+
123
+ [header_name, ERB.new(header_value).result(binding)]
124
+ end]
125
+ end
126
+
127
+ # Initialize value of merged options
128
+ # @return [Hash] Hash of merged options
129
+ def init_merge_options
130
+ options = rest_resource_options
131
+ options.merge! basic_auth_params if respond_to? :basic_auth_params
132
+ options[:headers] ||= {}
133
+ options[:headers].merge! parse_headers
134
+ options[:headers][:authorization] ||= ERB.new('Bearer <%= access_token %>').result(binding) if Soaspec.auto_oauth && respond_to?(:access_token)
135
+ options.merge(@init_options)
136
+ end
137
+
138
+ # @param [Hash] format Format of expected result.
139
+ # @return [Object] Generic body to be displayed in error messages
140
+ def response_body(response, format: :hash)
141
+ extract_hash response
142
+ end
143
+
144
+ # @return [Boolean] Whether response body includes String
145
+ def include_in_body?(response, expected)
146
+ response.body.include? expected
147
+ end
148
+
149
+ # @@return [Boolean] Whether the request found the desired value or not
150
+ def found?(response)
151
+ status_code_for(response) != 404
152
+ end
153
+
154
+ # @return [Boolean] Whether response contains expected value
155
+ def include_value?(response, expected)
156
+ extract_hash(response).include_value? expected
157
+ end
158
+
159
+ # @return [Boolean] Whether response body contains expected key
160
+ def include_key?(response, expected)
161
+ value_from_path(response, expected)
162
+ end
163
+
164
+ # @return [Integer] HTTP Status code for response
165
+ def status_code_for(response)
166
+ response.code
167
+ end
168
+
169
+ # Returns the value at the provided xpath
170
+ # @param [RestClient::Response] response
171
+ # @param [String] xpath Path to find elements from
172
+ # @param [String] attribute Attribute to find path for
173
+ # @return [Enumerable] Value inside element found through Xpath
174
+ def xpath_elements_for(response: nil, xpath: nil, attribute: nil)
175
+ raise ArgumentError unless response && xpath
176
+ raise "Can't perform XPATH if response is not XML" unless Interpreter.response_type_for(response) == :xml
177
+
178
+ xpath = prefix_xpath(xpath, attribute)
179
+ temp_doc = Nokogiri.parse(response.body).dup
180
+ if strip_namespaces? && !xpath.include?(':')
181
+ temp_doc.remove_namespaces!
182
+ temp_doc.xpath(xpath)
183
+ else
184
+ temp_doc.xpath(xpath, temp_doc.collect_namespaces)
185
+ end
186
+ end
187
+
188
+ # @return [Enumerable] List of values matching JSON path
189
+ def json_path_values_for(response, path, attribute: nil)
190
+ raise 'JSON does not support attributes' if attribute
191
+
192
+ JsonPath.on(response.body, path)
193
+ end
194
+
195
+ # Calculate all JSON path values based on rules. ',', pascal_case
196
+ # @param [RestClient::Response] response Response from API
197
+ # @param [Object] path Xpath, JSONPath or other path identifying how to find element
198
+ # @param [String] attribute Generic attribute to find. Will override path
199
+ # @param [Boolean] not_empty Whether to fail if result is empty
200
+ # @return [Array] Paths to check as first and matching values (List of values matching JSON Path) as second
201
+ def calculated_json_path_matches(path, response, attribute, not_empty: false)
202
+ path = add_pascal_path(path)
203
+ paths_to_check = path.split(',')
204
+ paths_to_check = paths_to_check.map { |path_to_check| prefix_json_path(path_to_check) }
205
+ matching_values = paths_to_check.collect do |path_to_check|
206
+ json_path_values_for(response, path_to_check, attribute: attribute)
207
+ end.reject(&:empty?)
208
+ raise NoElementAtPath, "No value at JSONPath '#{paths_to_check}' in '#{response.body}'" if matching_values.empty? && not_empty
209
+
210
+ matching_values.first
211
+ end
212
+
213
+ # Based on a exchange, return the value at the provided xpath
214
+ # If the path does not begin with a '/', a '//' is added to it
215
+ # @param [RestClient::Response] response Response from API
216
+ # @param [Object] path Xpath, JSONPath or other path identifying how to find element
217
+ # @param [String] attribute Generic attribute to find. Will override path
218
+ # @return [String] Value at Xpath
219
+ def value_from_path(response, path, attribute: nil)
220
+ path = path.to_s
221
+ case Interpreter.response_type_for(response)
222
+ when :xml
223
+ result = xpath_elements_for(response: response, xpath: path, attribute: attribute).first
224
+ raise NoElementAtPath, "No value at Xpath '#{prefix_xpath(path, attribute)}' in '#{response.body}'" unless result
225
+ return result.inner_text if attribute.nil?
226
+
227
+ return result.attributes[attribute].inner_text
228
+ when :json
229
+ matching_values = calculated_json_path_matches(path, response, attribute, not_empty: true)
230
+ matching_values.first
231
+ else # Assume this is a String
232
+ raise NoElementAtPath, 'Response is empty' if response.to_s.empty?
233
+
234
+ response.to_s[/#{path}/] # Perform regular expression using path if not XML nor JSON
235
+ end
236
+ end
237
+
238
+ # @return [Enumerable] List of values returned from path
239
+ def values_from_path(response, path, attribute: nil)
240
+ path = path.to_s
241
+ case Interpreter.response_type_for(response)
242
+ when :xml
243
+ xpath_elements_for(response: response, xpath: path, attribute: attribute).map(&:inner_text)
244
+ when :json
245
+ result = calculated_json_path_matches(path, response, attribute)
246
+ result || []
247
+ # json_path_values_for(response, path, attribute: attribute)
248
+ else
249
+ raise "Unable to interpret type of #{response.body}"
250
+ end
251
+ end
252
+
253
+ # @return [RestClient::Request] Request of API call. Either intended request or actual request
254
+ def request(response)
255
+ return 'Request not yet sent' if response.nil?
256
+
257
+ response.request
258
+ end
259
+
260
+ # Work out data to send based upon payload, template_name, or body
261
+ # @return [String] Payload to send in REST request
262
+ def post_data(test_values)
263
+ data = if @request_option == :hash && !test_values[:payload]
264
+ test_values[:payload] = JSON.generate(hash_used_in_request(test_values[:body])).to_s
265
+ elsif @request_option == :template
266
+ test_values = test_values[:body].dup if test_values[:body]
267
+ test_values = IndifferentHash.new(test_values) # Allow test_values to be either Symbol or String
268
+ Soaspec::TemplateReader.new.render_body(template_name, binding)
269
+ else
270
+ test_values[:payload]
271
+ end
272
+ # Soaspec::SpecLogger.info "Request Empty for '#{@request_option}'" if data.strip.empty?
273
+ data
274
+ end
275
+
276
+ # @param [Hash] override_hash Values to override default hash with
277
+ # @return [Hash] Hash used in REST request based on data conversion
278
+ def hash_used_in_request(override_hash)
279
+ request = override_hash ? @default_hash.merge(override_hash) : @default_hash
280
+ if pascal_keys?
281
+ request.map { |k, v| [convert_to_pascal_case(k.to_s), v] }.to_h
282
+ else
283
+ request
284
+ end
285
+ end
286
+ end
287
+ end