lib/soaspec/exchange_handlers/rest_handler.rb in soaspec-0.2.23 vs lib/soaspec/exchange_handlers/rest_handler.rb in soaspec-0.2.24
- old
+ new
@@ -1,259 +1,259 @@
-require_relative 'exchange_handler'
-require_relative 'rest_parameters'
-require_relative 'rest_parameters_defaults'
-require_relative 'rest_exchanger_factory'
-require_relative '../core_ext/hash'
-require_relative '../not_found_errors'
-require_relative 'handler_accessors'
-require_relative '../interpreter'
-require_relative 'response_extractor'
-require 'json'
-require 'jsonpath'
-require 'nori'
-require 'erb'
-
-module Soaspec
- # Wraps around Savon client defining default values dependent on the soap request
- class RestHandler < ExchangeHandler
- include ResponseExtractor
- extend Soaspec::RestParameters
- include Soaspec::RestParametersDefaults
- extend Soaspec::RestExchangeFactory
-
- # User used in making API calls
- attr_accessor :api_username
-
- # Setup object to handle communicating with a particular SOAP WSDL
- # @param [Hash] options Options defining REST request. base_url, default_hash
- 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
-
- # Used in together with Exchange request that passes such override parameters
- # @param [Hash] override_parameters Params to characterize REST request
- # @option override_parameters [Hash] :params Extra parameters (E.g. headers)
- # @option override_parameters [String] suburl URL appended to base_url of class
- # @option override_parameters [Hash] :q Query for REST
- # @option override_parameters [Symbol] :method REST method (:get, :post, :patch, etc)
- # Following are for the body of the request
- # @option override_parameters [Hash] :body Hash to be converted to JSON in request body
- # @option override_parameters [String] :payload String to be passed directly in request body
- # @option override_parameters [String] :template_name Path to file to be read via ERB and passed in request body
- # @return [RestClient::Response] Response from making request
- 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
-
- # Add values to here when extending this class to have default REST options.
- # See rest client resource at https://github.com/rest-client/rest-client for details
- # It's easier to set headers via 'headers' accessor rather than here
- # @return [Hash] Options adding to & overriding defaults
- def rest_resource_options
- {
- }
- end
-
- # Perform ERB on each header value
- # @return [Hash] Hash from 'rest_client_headers' passed through ERB
- 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
-
- # Initialize value of merged options
- # @return [Hash] Hash of merged options
- 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
-
- # @param [Hash] format Format of expected result.
- # @return [Object] Generic body to be displayed in error messages
- def response_body(response, format: :hash)
- extract_hash response
- end
-
- # @return [Boolean] Whether response body includes String
- def include_in_body?(response, expected)
- response.body.include? expected
- end
-
- # @@return [Boolean] Whether the request found the desired value or not
- def found?(response)
- status_code_for(response) != 404
- end
-
- # @return [Boolean] Whether response contains expected value
- def include_value?(response, expected)
- extract_hash(response).include_value? expected
- end
-
- # @return [Boolean] Whether response body contains expected key
- def include_key?(response, expected)
- value_from_path(response, expected)
- end
-
- # @return [Integer] HTTP Status code for response
- def status_code_for(response)
- response.code
- end
-
- # Returns the value at the provided xpath
- # @param [RestClient::Response] response
- # @param [String] xpath Path to find elements from
- # @param [String] attribute Attribute to find path for
- # @return [Enumerable] Value inside element found through Xpath
- 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
-
- # @return [Enumerable] List of values matching JSON path
- def json_path_values_for(response, path, attribute: nil)
- raise 'JSON does not support attributes' if attribute
-
- JsonPath.on(response.body, path)
- end
-
- # Calculate all JSON path values based on rules. ',', pascal_case
- # @param [RestClient::Response] response Response from API
- # @param [Object] path Xpath, JSONPath or other path identifying how to find element
- # @param [String] attribute Generic attribute to find. Will override path
- # @param [Boolean] not_empty Whether to fail if result is empty
- # @return [Array] Paths to check as first and matching values (List of values matching JSON Path) as second
- 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
-
- # Based on a exchange, return the value at the provided xpath
- # If the path does not begin with a '/', a '//' is added to it
- # @param [RestClient::Response] response Response from API
- # @param [Object] path Xpath, JSONPath or other path identifying how to find element
- # @param [String] attribute Generic attribute to find. Will override path
- # @return [String] Value at Xpath
- 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
-
- # @return [Enumerable] List of values returned from path
- 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
-
- # @return [RestClient::Request] Request of API call. Either intended request or actual request
- def request(response)
- return 'Request not yet sent' if response.nil?
-
- response.request
- end
-
- private
-
- # Work out data to send based upon payload, template_name, or body
- # @return [String] Payload to send in REST request
- 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
-
- # @return [Hash] Hash used in REST request based on data conversion
- 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
- end
-end
+require_relative 'exchange_handler'
+require_relative 'rest_parameters'
+require_relative 'rest_parameters_defaults'
+require_relative 'rest_exchanger_factory'
+require_relative '../core_ext/hash'
+require_relative '../not_found_errors'
+require_relative 'handler_accessors'
+require_relative '../interpreter'
+require_relative 'response_extractor'
+require 'json'
+require 'jsonpath'
+require 'nori'
+require 'erb'
+
+module Soaspec
+ # Wraps around Savon client defining default values dependent on the soap request
+ class RestHandler < ExchangeHandler
+ include ResponseExtractor
+ extend Soaspec::RestParameters
+ include Soaspec::RestParametersDefaults
+ extend Soaspec::RestExchangeFactory
+
+ # User used in making API calls
+ attr_accessor :api_username
+
+ # Setup object to handle communicating with a particular SOAP WSDL
+ # @param [Hash] options Options defining REST request. base_url, default_hash
+ 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
+
+ # Used in together with Exchange request that passes such override parameters
+ # @param [Hash] override_parameters Params to characterize REST request
+ # @option override_parameters [Hash] :params Extra parameters (E.g. headers)
+ # @option override_parameters [String] suburl URL appended to base_url of class
+ # @option override_parameters [Hash] :q Query for REST
+ # @option override_parameters [Symbol] :method REST method (:get, :post, :patch, etc)
+ # Following are for the body of the request
+ # @option override_parameters [Hash] :body Hash to be converted to JSON in request body
+ # @option override_parameters [String] :payload String to be passed directly in request body
+ # @option override_parameters [String] :template_name Path to file to be read via ERB and passed in request body
+ # @return [RestClient::Response] Response from making request
+ 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
+
+ # Add values to here when extending this class to have default REST options.
+ # See rest client resource at https://github.com/rest-client/rest-client for details
+ # It's easier to set headers via 'headers' accessor rather than here
+ # @return [Hash] Options adding to & overriding defaults
+ def rest_resource_options
+ {
+ }
+ end
+
+ # Perform ERB on each header value
+ # @return [Hash] Hash from 'rest_client_headers' passed through ERB
+ 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
+
+ # Initialize value of merged options
+ # @return [Hash] Hash of merged options
+ 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
+
+ # @param [Hash] format Format of expected result.
+ # @return [Object] Generic body to be displayed in error messages
+ def response_body(response, format: :hash)
+ extract_hash response
+ end
+
+ # @return [Boolean] Whether response body includes String
+ def include_in_body?(response, expected)
+ response.body.include? expected
+ end
+
+ # @@return [Boolean] Whether the request found the desired value or not
+ def found?(response)
+ status_code_for(response) != 404
+ end
+
+ # @return [Boolean] Whether response contains expected value
+ def include_value?(response, expected)
+ extract_hash(response).include_value? expected
+ end
+
+ # @return [Boolean] Whether response body contains expected key
+ def include_key?(response, expected)
+ value_from_path(response, expected)
+ end
+
+ # @return [Integer] HTTP Status code for response
+ def status_code_for(response)
+ response.code
+ end
+
+ # Returns the value at the provided xpath
+ # @param [RestClient::Response] response
+ # @param [String] xpath Path to find elements from
+ # @param [String] attribute Attribute to find path for
+ # @return [Enumerable] Value inside element found through Xpath
+ 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
+
+ # @return [Enumerable] List of values matching JSON path
+ def json_path_values_for(response, path, attribute: nil)
+ raise 'JSON does not support attributes' if attribute
+
+ JsonPath.on(response.body, path)
+ end
+
+ # Calculate all JSON path values based on rules. ',', pascal_case
+ # @param [RestClient::Response] response Response from API
+ # @param [Object] path Xpath, JSONPath or other path identifying how to find element
+ # @param [String] attribute Generic attribute to find. Will override path
+ # @param [Boolean] not_empty Whether to fail if result is empty
+ # @return [Array] Paths to check as first and matching values (List of values matching JSON Path) as second
+ 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
+
+ # Based on a exchange, return the value at the provided xpath
+ # If the path does not begin with a '/', a '//' is added to it
+ # @param [RestClient::Response] response Response from API
+ # @param [Object] path Xpath, JSONPath or other path identifying how to find element
+ # @param [String] attribute Generic attribute to find. Will override path
+ # @return [String] Value at Xpath
+ 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
+
+ # @return [Enumerable] List of values returned from path
+ 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
+
+ # @return [RestClient::Request] Request of API call. Either intended request or actual request
+ def request(response)
+ return 'Request not yet sent' if response.nil?
+
+ response.request
+ end
+
+ private
+
+ # Work out data to send based upon payload, template_name, or body
+ # @return [String] Payload to send in REST request
+ 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
+
+ # @return [Hash] Hash used in REST request based on data conversion
+ 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
+ end
+end