Class: Soaspec::RestHandler

Inherits:
ExchangeHandler show all
Extended by:
RestAccessors
Defined in:
lib/soaspec/exchange_handlers/rest_handler.rb

Overview

Wraps around Savon client defining default values dependent on the soap request

Instance Attribute Summary collapse

Attributes inherited from ExchangeHandler

#template_name

Instance Method Summary collapse

Methods included from RestAccessors

base_url, headers, oauth2, oauth2_file, pascal_keys

Methods inherited from ExchangeHandler

#convert_to_lower?, #default_hash=, #elements, #expected_mandatory_elements, #expected_mandatory_json_values, #expected_mandatory_xpath_values, #set_remove_key, #store, #strip_namespaces?, #to_s, #use

Methods included from HandlerAccessors

#attribute, #convert_to_lower, #element, #mandatory_json_values, #strip_namespaces

Constructor Details

#initialize(name = self.class.to_s, options = {}) ⇒ RestHandler

Setup object to handle communicating with a particular SOAP WSDL



48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 48

def initialize(name = self.class.to_s, options = {})
  raise "Base URL not set! Please set in class with 'base_url' method" unless base_url_value
  @default_hash = {}
  if name.is_a?(Hash) && options == {} # If name is not set

    options = name
    name = self.class.to_s
  end
  super
  set_remove_key(options, :api_username)
  set_remove_key(options, :default_hash)
  @init_options = options
end

Instance Attribute Details

#api_usernameObject

User used in making API calls



19
20
21
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 19

def api_username
  @api_username
end

Instance Method Details

#base_url_valueObject

Set through following method. Base URL in REST requests.



22
23
24
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 22

def base_url_value
  nil
end

#convert_to_pascal_case(key) ⇒ Object

Convert snakecase to PascalCase



62
63
64
65
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 62

def convert_to_pascal_case(key)
  return key if /[[:upper:]]/ =~ key[0] # If first character already capital, don't do conversion

  key.split('_').map(&:capitalize).join
end

#extract_hash(response) ⇒ Hash

Convert XML or JSON response into a Hash



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 146

def extract_hash(response)
  raise ArgumentError("Empty Body. Can't assert on it") if response.body.empty?
  case Interpreter.response_type_for response
  when :json
    converted = JSON.parse(response.body)
    return converted.transform_keys_to_symbols if converted.is_a? Hash
    return converted.map!(&:transform_keys_to_symbols) if converted.is_a? Array
    raise 'Incorrect Type prodcued ' + converted.class
  when :xml
    parser = Nori.new(convert_tags_to: lambda { |tag| tag.snakecase.to_sym })
    parser.parse(response.body)
  else
    raise "Neither XML nor JSON detected. It is #{type}. Don't know how to parse It is #{response.body}"
  end
end

#found?(response) ⇒ Boolean

Whether the request found the desired value or not



139
140
141
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 139

def found?(response)
  status_code_for(response) != 404
end

#hash_used_in_request(override_hash) ⇒ Hash



75
76
77
78
79
80
81
82
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 75

def hash_used_in_request(override_hash)
  request = @default_hash.merge(override_hash)
  if pascal_keys?
    request.map { |k, v| [convert_to_pascal_case(k.to_s), v] }.to_h
  else
    request
  end
end

#include_in_body?(response, expected) ⇒ Boolean



134
135
136
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 134

def include_in_body?(response, expected)
  response.body.include? expected
end

#include_key?(response, expected) ⇒ Boolean



168
169
170
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 168

def include_key?(response, expected)
  value_from_path(response, expected)
end

#include_value?(response, expected) ⇒ Boolean



163
164
165
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 163

def include_value?(response, expected)
  extract_hash(response).include_value? expected
end

#init_merge_optionsObject

Initialize value of merged options



85
86
87
88
89
90
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 85

def init_merge_options
  options = rest_resource_options
  options[:headers] ||= {}
  options[:headers].merge! parse_headers
  options.merge(@init_options)
end

#json_path_values_for(response, path, attribute: nil) ⇒ Enumerable



218
219
220
221
222
223
224
225
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 218

def json_path_values_for(response, path, attribute: nil)
  raise 'JSON does not support attributes' if attribute
  if path[0] != '$'
    path = convert_to_pascal_case(path) if pascal_keys?
    path = '$..' + path
  end
  JsonPath.on(response.body, path)
end

#make_request(override_parameters) ⇒ Object

Used in together with Exchange request that passes such override parameters



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 97

def make_request(override_parameters)
  @merged_options ||= init_merge_options
  test_values = override_parameters
  test_values[:params] ||= {}
  test_values[:method] ||= :post
  test_values[:suburl] = test_values[:suburl].to_s if test_values[:suburl]
  test_values[:params][:params] = test_values[:q] if test_values[:q] # Use q for query parameters. Nested :params is ugly and long


  # In order for ERB to be calculated at correct time, the first time request is made, the resource should be created

  @resource ||= RestClient::Resource.new(ERB.new(base_url_value).result(binding), @merged_options)

  @resource_used = test_values[:suburl] ? @resource[test_values[:suburl]] : @resource

  begin
  response = case test_values[:method]
             when :post, :patch, :put
               unless test_values[:payload]
                 test_values[:payload] = JSON.generate(hash_used_in_request(test_values[:body])).to_s if test_values[:body]
               end
               @resource_used.send(test_values[:method].to_s, test_values[:payload], test_values[:params])
             else
               @resource_used.send(test_values[:method].to_s, test_values[:params])
             end
  rescue RestClient::ExceptionWithResponse => e
    response = e.response
  end
  Soaspec::SpecLogger.add_to('response_headers: ' + response.headers.to_s)
  Soaspec::SpecLogger.add_to('response_body: ' + response.to_s)
  response
end

#mandatory_elementsArray

Override this to specify elements that must be present in the response Will be used in ‘success_scenarios’ shared examples



180
181
182
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 180

def mandatory_elements
  []
end

#mandatory_xpath_valuesHash

Override this to specify xpath results that must be present in the response Will be used in ‘success_scenarios’ shared examples



187
188
189
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 187

def mandatory_xpath_values
  {}
end

#parse_headersHash

Perform ERB on each header value



42
43
44
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 42

def parse_headers
  Hash[rest_client_headers.map { |k, header| [k, ERB.new(header).result(binding)] }]
end

#pascal_keys?Boolean

Whether to convert each key in the request to PascalCase It will also auto convert simple XPath, JSONPath where ‘//’ or ‘..’ not specified



70
71
72
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 70

def pascal_keys?
  false
end

#response_body(response, _format: :hash) ⇒ Object



130
131
132
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 130

def response_body(response, _format: :hash)
  extract_hash response
end

#rest_client_headersObject

Headers used in RestClient



27
28
29
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 27

def rest_client_headers
  {}
end

#rest_resource_optionsHash

Add values to here when extending this class to have default REST options. See rest client resource at github.com/rest-client/rest-client for details It’s easier to set headers via ‘headers’ accessor rather than here



35
36
37
38
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 35

def rest_resource_options
  {
  }
end

#root_attributesObject

Attributes set at the root XML element of SOAP request



192
193
194
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 192

def root_attributes
  nil
end

#status_code_for(response) ⇒ Integer



173
174
175
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 173

def status_code_for(response)
  response.code
end

#to_hash(response) ⇒ Hash



266
267
268
269
270
271
272
273
274
275
276
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 266

def to_hash(response)
  case Interpreter.response_type_for(response)
  when :xml
    parser = Nori.new(strip_namespaces: strip_namespaces?, convert_tags_to: ->(tag) { tag.snakecase.to_sym })
    parser.parse(response.body.to_s)
  when :json
    JSON.parse(response.body.to_s)
  else
    raise "Unable to interpret type of #{response.body}"
  end
end

#value_from_path(response, path, attribute: nil) ⇒ String

Based on a exchange, return the value at the provided xpath If the path does not begin with a ‘/’, a ‘//’ is added to it



233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 233

def value_from_path(response, path, attribute: nil)
  path = path.to_s
  case Interpreter.response_type_for(response)
  when :xml
    result = xpath_elements_for(response: response, xpath: path, attribute: attribute).first
    raise NoElementAtPath, "No value at Xpath '#{path}'" unless result
    return result.inner_text if attribute.nil?
    return result.attributes[attribute].inner_text
  when :json
    matching_values = json_path_values_for(response, path, attribute: attribute)
    raise NoElementAtPath, "Element in #{response.body} not found with path '#{path}'" if matching_values.empty?
    matching_values.first
  when :hash
    response.dig(path.split('.')) # Use path as Hash dig expression separating params via '.' TODO: Unit test

  else
    response.to_s[/path/] # Perform regular expression using path if not XML nor JSON TODO: Unit test

  end
end

#values_from_path(response, path, attribute: nil) ⇒ Enumerable



253
254
255
256
257
258
259
260
261
262
263
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 253

def values_from_path(response, path, attribute: nil)
  path = path.to_s
  case Interpreter.response_type_for(response)
  when :xml
    xpath_elements_for(response: response, xpath: path, attribute: attribute).map(&:inner_text)
  when :json
    json_path_values_for(response, path, attribute: attribute)
  else
    raise "Unable to interpret type of #{response.body}"
  end
end

#xpath_elements_for(response: nil, xpath: nil, attribute: nil) ⇒ Enumerable

Returns the value at the provided xpath

Raises:

  • (ArgumentError)


200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 200

def xpath_elements_for(response: nil, xpath: nil, attribute: nil)
  raise ArgumentError unless response && xpath
  raise "Can't perform XPATH if response is not XML" unless Interpreter.response_type_for(response) == :xml
  xpath = "//*[@#{attribute}]" unless attribute.nil?
  if xpath[0] != '/'
    xpath = convert_to_pascal_case(xpath) if pascal_keys?
    xpath = '//' + xpath
  end
  temp_doc = Nokogiri.parse(response.body).dup
  if strip_namespaces? && !xpath.include?(':')
    temp_doc.remove_namespaces!
    temp_doc.xpath(xpath)
  else
    temp_doc.xpath(xpath, temp_doc.collect_namespaces)
  end
end