Class: Soaspec::RestHandler

Inherits:
ExchangeHandler show all
Extended by:
RestExchangeFactory, RestParameters
Includes:
RestParametersDefaults
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 RestParameters

base_url, basic_auth, basic_auth_file, client_id, headers, oauth2, oauth2_file, pascal_keys

Methods included from RestExchangeFactory

delete, get, patch, post, put

Methods included from RestParametersDefaults

#base_url_value, #pascal_keys?, #rest_client_headers

Methods inherited from ExchangeHandler

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

Methods included from HandlerAccessors

#attribute, #convert_to_lower, #default_hash, #element, #mandatory_elements, #mandatory_json_values, #mandatory_xpath_values, #strip_namespaces, #template_name

Constructor Details

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

Setup object to handle communicating with a particular SOAP WSDL

Parameters:

  • options (Hash) (defaults to: {})

    Options defining REST request. base_url, default_hash



26
27
28
29
30
31
32
33
34
35
36
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 26

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
  if name.is_a?(Hash) && options == {} # If name is not set, use first parameter as the options hash
    options = name
    name = self.class.to_s
  end
  super
  set_remove_keys(options, %i[api_username default_hash template_name])
  @init_options = options
  init_merge_options # Call this to verify any issues with options on creating object
end

Instance Attribute Details

#api_usernameObject

User used in making API calls



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

def api_username
  @api_username
end

Instance Method Details

#convert_to_pascal_case(key) ⇒ Object

Convert snakecase to PascalCase



95
96
97
98
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 95

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

TODO: This and ‘to_hash’ method should be merged Convert XML or JSON response into a Hash

Parameters:

  • response (String)

    Response as a String (either in XML or JSON)

Returns:

Raises:



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 221

def extract_hash(response)
  raise NoElementAtPath, "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 produced ' + 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

@@return [Boolean] Whether the request found the desired value or not

Returns:

  • (Boolean)


125
126
127
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 125

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

#include_in_body?(response, expected) ⇒ Boolean

Returns Whether response body includes String.

Returns:

  • (Boolean)

    Whether response body includes String



120
121
122
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 120

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

#include_key?(response, expected) ⇒ Boolean

Returns Whether response body contains expected key.

Returns:

  • (Boolean)

    Whether response body contains expected key



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

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

#include_value?(response, expected) ⇒ Boolean

Returns Whether response contains expected value.

Returns:

  • (Boolean)

    Whether response contains expected value



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

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

#init_merge_optionsHash

Initialize value of merged options

Returns:

  • (Hash)

    Hash of merged options



102
103
104
105
106
107
108
109
110
111
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 102

def init_merge_options
  options = rest_resource_options
  options.merge! basic_auth_params if respond_to? :basic_auth_params
  options[:headers] ||= {}
  options[:headers].merge! parse_headers
  if Soaspec.auto_oauth && respond_to?(:access_token)
    options[:headers][:authorization] ||= ERB.new('Bearer <%= access_token %>').result(binding)
  end
  options.merge(@init_options)
end

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

Returns List of values matching JSON path.

Returns:

  • (Enumerable)

    List of values matching JSON path



166
167
168
169
170
171
172
173
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 166

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) ⇒ RestClient::Response

Used in together with Exchange request that passes such override parameters Following are for the body of the request

Parameters:

  • override_parameters (Hash)

    Params to characterize REST request

Options Hash (override_parameters):

  • :params (Hash)

    Extra parameters (E.g. headers)

  • suburl (String)

    URL appended to base_url of class

  • :q (Hash)

    Query for REST

  • :method (Symbol)

    REST method (:get, :post, :patch, etc)

  • :body (Hash)

    Hash to be converted to JSON in request body

  • :payload (String)

    String to be passed directly in request body

  • :template_name (String)

    Path to file to be read via ERB and passed in request body

Returns:

  • (RestClient::Response)

    Response from making request



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 49

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
                 Soaspec::SpecLogger.info("request body: #{post_data(test_values)}")
                 @resource_used.send(test_values[:method].to_s, post_data(test_values), test_values[:params])
               else # :get, :delete
                 @resource_used.send(test_values[:method].to_s, test_values[:params])
               end
  rescue RestClient::ExceptionWithResponse => e
    response = e.response
  end
  Soaspec::SpecLogger.info(["response_headers: #{response.headers}", "response_body: #{response}"])
  response
end

#parse_headersHash

Perform ERB on each header value

Returns:

  • (Hash)

    Hash from ‘rest_client_headers’ passed through ERB



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

def parse_headers
  Hash[rest_client_headers.map do |header_name, header_value|
    raise ArgumentError, "Header '#{header_name}' is null. Headers are #{rest_client_headers}" if header_value.nil?
    [header_name, ERB.new(header_value).result(binding)]
  end]
end

#request(response) ⇒ Object



251
252
253
254
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 251

def request(response)
  return 'Request not yet sent' if response.nil?
  response.request
end

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

Returns Generic body to be displayed in error messages.

Parameters:

  • format (Hash) (defaults to: :hash)

    Format of expected result.

Returns:

  • (Object)

    Generic body to be displayed in error messages



115
116
117
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 115

def response_body(response, format: :hash)
  extract_hash response
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

Returns:

  • (Hash)

    Options adding to & overriding defaults



80
81
82
83
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 80

def rest_resource_options
  {
  }
end

#status_code_for(response) ⇒ Integer

Returns HTTP Status code for response.

Returns:

  • (Integer)

    HTTP Status code for response



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

def status_code_for(response)
  response.code
end

#to_hash(response) ⇒ Hash

Returns Hash representing response body.

Returns:

  • (Hash)

    Hash representing response body



238
239
240
241
242
243
244
245
246
247
248
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 238

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

Parameters:

  • response (Response)
  • path (Object)

    Xpath, JSONPath or other path identifying how to find element

  • attribute (String) (defaults to: nil)

    Generic attribute to find. Will override path

Returns:

  • (String)

    Value at Xpath



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 181

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
    paths_to_check = path.split(',')
    matching_values = paths_to_check.collect do |path_to_check|
      json_path_values_for(response, path_to_check, attribute: attribute)
    end.reject(&:empty?)
    raise NoElementAtPath, "Path '#{path}' not found in '#{response.body}'" if matching_values.empty?
    matching_values.first.first
  when :hash
    response.dig(path.split('.')) # Use path as Hash dig expression separating params via '.' TODO: Unit test
  else
    raise NoElementAtPath, 'Response is empty' if response.to_s.empty?
    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

Returns List of values returned from path.

Returns:

  • (Enumerable)

    List of values returned from path



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

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

Parameters:

  • response (RestClient::Response) (defaults to: nil)
  • xpath (String) (defaults to: nil)

Returns:

  • (Enumerable)

    Value inside element found through Xpath

Raises:

  • (ArgumentError)


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

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