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



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



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

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



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



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



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



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



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



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



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



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

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



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



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



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



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



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



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



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



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

def rest_resource_options
  {}
end

#status_code_for(response) ⇒ Integer



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



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, :html
    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



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

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 i[xml html].include? Interpreter.response_type_for(response)

  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