# typed: false # frozen_string_literal: true require 'active_support/json' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/string/access' module Setsuzoku module Service module WebService module ApiStrategies # Defines all necessary methods for handling interfacing with a REST API. class RestStrategy < WebService::ApiStrategy extend T::Sig extend T::Helpers def request_class RestAPIRequest end def self.required_instance_methods [] end # Make a REST API request. # 1. Format the request and send it via the appropriate HTTP method. # 2. Format the response and return it. # # @param request [RestAPIRequest] the constructed API request object. # @param action_details [Hash] the action details for the action to execute. # @param options [Any] additional options needed to pass to correctly perform the request. # options are: # media_type - 'json' # # @return [Hash] the parsed response object. sig { override.params(request: T.untyped, action_details: T::Hash[T.untyped, T.untyped], options: T.untyped).returns(Faraday::Response) } def perform_external_call(request:, action_details:, **options) request_properties = self.get_request_properties(action_name: request.action, action_details: action_details, req_params: request.body) request_options = self.request_options(request_properties[:request_format], action_details[:actions][request.action]) full_request = self.formulate_request(request_properties, request_options) @faraday = Faraday.new(url: request_properties[:formatted_full_url], request: { params_encoder: Faraday::FlatParamsEncoder }) do |faraday| faraday.request(:multipart) if options[:attachments].present? faraday.request(:url_encoded) # faraday.basic_auth(request_options[:basic_auth][:username], request_options[:basic_auth][:password]) if request_options[:basic_auth] faraday.request(:basic_auth, request_options[:basic_auth][:username], request_options[:basic_auth][:password]) if request_options[:basic_auth] # faraday.response request_properties[:response_format] if request_properties[:response_format] faraday.authorization(:Bearer, request_properties[:token]) if request_properties[:token] # request_options[:headers][:Authorization].prepend('Bearer ') if request_options[:headers][:Authorization] faraday.adapter Faraday.default_adapter end if options[:attachments].present? resp = @faraday.post do |req| io = StringIO.new(full_request) payload = {} payload[:json] = Faraday::UploadIO.new(io, 'application/json') payload[:attachment] = options[:attachments].map{ |file| Faraday::UploadIO.new(file[0], file[1], file[2]) } req.body = payload end else @faraday.headers = request_options[:headers] if request_options[:headers] resp = @faraday.send(request_properties[:request_method], request_properties[:formatted_full_url], full_request) end resp end sig do params( action_name: Symbol, for_stub: T::Boolean, req_params: T::Hash[T.untyped, T.untyped], action_details: T::Hash[Symbol, T.untyped] ).returns(T::Hash[Symbol, T.untyped]) end def get_request_properties(action_name:, for_stub: false, req_params: {}, action_details: { actions: self.plugin.api_actions, url: self.plugin.api_base_url }) action = action_details[:actions][action_name] url = action.has_key?(:request_url) ? action[:request_url] : action_details[:url] request_method, endpoint = action.first request_method = request_method.downcase.to_sym request_format = action[:request_type] response_format = action[:response_type] token = action[:token] stub_data = action[:stub_data] if for_stub full_url = url + endpoint formatted_full_url, req_params = self.replace_dynamic_vars(full_url: full_url, req_params: req_params) { request_method: request_method, endpoint: endpoint, request_format: request_format, response_format: response_format, formatted_full_url: formatted_full_url, req_params: req_params, stub_data: stub_data, token: token } end # Create the proper request body based on the request_properties and request_options passed # # @param request_properties [Hash] information pertaining to the body of the request # @param request_options [Hash] information pertainint to the headers of the request # # @return full_request [Hash/String] returns the request body in the format required def formulate_request(request_properties = {}, request_options = {}) request_format = request_properties.dig(:request_format).to_s if request_properties[:request_method] == :get || request_properties[:req_params].empty? request_properties[:req_params] else # if the header or request format include urlencoded return the body as a hash if request_format.include?('urlencoded') request_properties[:req_params] else # return either xml or json if request_properties[:request_format] == :xml convert_hash_to_xml(request_properties[:req_params]) else request_properties[:req_params].to_json end end end end def request_options(request_format = nil, action_details = {}) request_options = self.auth_strategy.auth_headers.merge(self.plugin.api_headers).merge(action_details[:request_options] || {}) content_type = case request_format when :json 'application/json' when :xml 'application/xml' when :text, :file 'text/plain' when :pdf 'application/pdf' when :'x-www-form-urlencoded;charset=UTF-8' 'application/x-www-form-urlencoded;charset=UTF-8' else 'application/json' end (request_options[:headers] ||= {})[:'Content-Type'] = content_type request_options end def replace_dynamic_vars(full_url:, req_params: {}) # replace matching vars in the action with matching params. # scans the string for variables like {{number_sid}} and replaces it with the matching key in params # removes the params variable, as it's probably intended to be in the url only. If you encounter a need to have # it in the body and the url, then you should put it in params twice with different names. req_params = req_params.dup full_url = full_url.dup dynamic_vars = self.plugin.dynamic_url_params.merge(req_params).with_indifferent_access full_url.scan(/({{.*?}})/).flatten.each do |var| var_name = var.tr('{{ }}', '') next unless dynamic_vars[var_name] full_url.gsub!(var, dynamic_vars[var_name]) req_params.delete(var_name) req_params.delete(var_name.to_sym) end [full_url, req_params] end end end end end end