Class: Soaspec::RestHandler

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

#exception, #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, retry_on_exceptions

Methods included from RestExchangeFactory

delete, get, patch, post, put

Methods included from RestParametersDefaults

#base_url_value, #parent_url, #pascal_keys?, #rest_client_headers

Methods included from ResponseExtractor

#extract_hash, #to_hash

Methods inherited from ExchangeHandler

#default_hash=, #elements, #set_remove_key, #set_remove_keys, #store, #to_s, use, #use

Methods included from HandlerAccessors

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

Methods included from ExchangeHandlerDefaults

#convert_to_lower?, #expected_mandatory_elements, #expected_mandatory_json_values, #expected_mandatory_xpath_values, #retry_exception_limit, #retry_on_exceptions, #retry_pause_time, #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



31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 31

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



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

def api_username
  @api_username
end

Instance Method Details

#after_response(_response, _self) ⇒ Object

Override this with ‘after_response’ within class definition to perform an action after response is retrieved

Parameters:

  • _response (RestClient::Response)

    Response to interpret to perform after block



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

def after_response(_response, _self); end

#calculated_json_path_matches(path, response, attribute, not_empty: false) ⇒ Array

Calculate all JSON path values based on rules. ‘,’, pascal_case

Parameters:

  • response (RestClient::Response)

    Response from API

  • path (Object)

    Xpath, JSONPath or other path identifying how to find element

  • attribute (String)

    Generic attribute to find. Will override path

  • not_empty (Boolean) (defaults to: false)

    Whether to fail if result is empty

Returns:

  • (Array)

    Paths to check as first and matching values (List of values matching JSON Path) as second

Raises:



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

def calculated_json_path_matches(path, response, attribute, not_empty: false)
  path = add_pascal_path(path)
  paths_to_check = path.split(',')
  paths_to_check = paths_to_check.map { |path_to_check| prefix_json_path(path_to_check) }
  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, "No value at JSONPath '#{paths_to_check}' in '#{response.body}'" if matching_values.empty? && not_empty

  matching_values.first
end

#found?(response) ⇒ Boolean

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

Returns:

  • (Boolean)


170
171
172
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 170

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

#hash_used_in_request(override_hash) ⇒ Hash

Returns Hash used in REST request based on data conversion.

Parameters:

  • override_hash (Hash)

    Values to override default hash with

Returns:

  • (Hash)

    Hash used in REST request based on data conversion



298
299
300
301
302
303
304
305
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 298

def hash_used_in_request(override_hash)
  request = override_hash ? @default_hash.merge(override_hash) : @default_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

Returns Whether response body includes String.

Returns:

  • (Boolean)

    Whether response body includes String



165
166
167
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 165

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



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

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



175
176
177
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 175

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



149
150
151
152
153
154
155
156
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 149

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
  options[:headers][:authorization] ||= ERB.new('Bearer <%= access_token %>').result(binding) if Soaspec.auto_oauth && respond_to?(:access_token)
  options.merge(@init_options)
end

#interpret_parameters(request_parameters) ⇒ Hash

Interpret REST parameters given provided parameters and adding defaults, making transformations

Parameters:

  • request_parameters (Hash)

    Parameters used in making a request

Returns:

  • (Hash)

    Request parameters merged with default values



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 70

def interpret_parameters(request_parameters)
  request_parameters[:params] ||= {}
  request_parameters[:method] ||= :post
  suburl = request_parameters[:suburl]
  if suburl
    if suburl.is_a? Array
      request_parameters[:suburl] = suburl.collect(&:to_s).join('/')
    else
      request_parameters[:suburl] = suburl.to_s
    end
  end
  # Use q for query parameters. Nested :params is ugly, long and unclear
  request_parameters[:params][:params] = request_parameters[:q] if request_parameters[:q]
  request_parameters
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



209
210
211
212
213
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 209

def json_path_values_for(response, path, attribute: nil)
  raise 'JSON does not support attributes' if attribute

  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



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
127
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 102

def make_request(override_parameters)
  @merged_options ||= init_merge_options # TODO: Is this var needed? Can method be passed to resource creation?
  test_values = interpret_parameters override_parameters
  # 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

  self.exception = nil # Remove any previously stored exception
  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::Exception => e
    self.exception = e
    raise e unless e.respond_to? :response

    response = e.response
  end
  Soaspec::SpecLogger.info("response: \n  headers: #{response&.headers}\n  body: #{response}\n")
  after_response(response, self)
  response
end

#parse_headersHash

Perform ERB on each header value

Returns:

  • (Hash)

    Hash from ‘rest_client_headers’ passed through ERB



139
140
141
142
143
144
145
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 139

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

#payload?(overall_params) ⇒ Boolean

Returns Whether REST method should have a payload.

Returns:

  • (Boolean)

    Whether REST method should have a payload



45
46
47
48
49
50
51
52
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 45

def payload?(overall_params)
  case overall_params[:method]
  when :post, :patch, :put
    true
  else
    false
  end
end

#post_data(test_values) ⇒ String

Work out data to send based upon payload, template_name, or body

Returns:

  • (String)

    Payload to send in REST request



282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 282

def post_data(test_values)
  data = if @request_option == :hash && !test_values[:payload]
           test_values[:payload] = JSON.generate(hash_used_in_request(test_values[:body])).to_s
         elsif @request_option == :template
           test_values = test_values[:body].dup if test_values[:body]
           test_values = IndifferentHash.new(test_values) # Allow test_values to be either Symbol or String
           Soaspec::TemplateReader.new.render_body(template_name, binding)
         else
           test_values[:payload]
         end
  # Soaspec::SpecLogger.info "Request Empty for '#{@request_option}'" if data.strip.empty?
  data
end

#request(response) ⇒ RestClient::Request

Returns Request of API call. Either intended request or actual request.

Returns:

  • (RestClient::Request)

    Request of API call. Either intended request or actual request



274
275
276
277
278
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 274

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

  response.request
end

#request_parameters(override_parameters) ⇒ RestRequest

TODO:

Use this in actually making the request

At the moment this is just for one to manually view the parameters that will be used in making a request

Returns:

  • (RestRequest)

    Parameters used in making a request



58
59
60
61
62
63
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 58

def request_parameters(override_parameters)
  overall_params = interpret_parameters(override_parameters)
  request = { overall: overall_params, options: init_merge_options }
  request[:body] = post_data(overall_params) if payload?(overall_params)
  RestRequest.new(overall_params, init_merge_options, payload?(overall_params) ? post_data(overall_params) : nil)
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



160
161
162
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 160

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



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

def rest_resource_options
  {}
end

#status_code_for(response) ⇒ Integer

Returns HTTP Status code for response.

Returns:

  • (Integer)

    HTTP Status code for response



185
186
187
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 185

def status_code_for(response)
  response.code
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 (RestClient::Response)

    Response from API

  • 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



239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 239

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 '#{prefix_xpath(path, attribute)}' in '#{response.body}'" unless result
    return result.inner_text if attribute.nil?

    return result.attributes[attribute].inner_text
  when :json
    matching_values = calculated_json_path_matches(path, response, attribute, not_empty: true)
    matching_values.first
  else # Assume this is a String
    raise NoElementAtPath, 'Response is empty' if response.to_s.empty?

    response.to_s[/#{path}/] # Perform regular expression using path if not XML nor JSON
  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



259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/soaspec/exchange_handlers/rest_handler.rb', line 259

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
    result = calculated_json_path_matches(path, response, attribute)
    result || []
    # 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)

    Path to find elements from

  • attribute (String) (defaults to: nil)

    Attribute to find path for

Returns:

  • (Enumerable)

    Value inside element found through Xpath

Raises:

  • (ArgumentError)


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

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 = prefix_xpath(xpath, attribute)
  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