Class: Soaspec::RestHandler

Inherits:
ExchangeHandler show all
Extended by:
RestAccessors
Includes:
RestAccessorsDefaults
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 included from RestAccessorsDefaults

#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, #element, #mandatory_elements, #mandatory_json_values, #mandatory_xpath_values, #strip_namespaces

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



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

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, 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
end

Instance Attribute Details

#api_usernameObject

User used in making API calls



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

def api_username
  @api_username
end

Instance Method Details

#convert_to_pascal_case(key) ⇒ Object

Convert snakecase to PascalCase



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

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:



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 213

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 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)


118
119
120
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 118

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



113
114
115
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 113

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



128
129
130
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 128

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



123
124
125
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 123

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



96
97
98
99
100
101
102
103
104
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 96

def init_merge_options
  options = rest_resource_options
  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



159
160
161
162
163
164
165
166
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 159

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 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



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

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



84
85
86
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 84

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

#request(response) ⇒ Object



243
244
245
246
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 243

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



108
109
110
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 108

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



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

def rest_resource_options
  {
  }
end

#status_code_for(response) ⇒ Integer

Returns HTTP Status code for response.

Returns:

  • (Integer)

    HTTP Status code for response



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

def status_code_for(response)
  response.code
end

#to_hash(response) ⇒ Hash

Returns Hash representing response body.

Returns:

  • (Hash)

    Hash representing response body



230
231
232
233
234
235
236
237
238
239
240
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 230

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



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 174

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
    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



197
198
199
200
201
202
203
204
205
206
207
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 197

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)


141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 141

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