# typed: ignore # 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' # attachment_url_key - 'Symbol' the form field to post the attachment in the form. # attachment_urls - 'Array' List of attachment urls to be attached as form files. # # @return [Hash] the parsed response object. sig { override.params(request: RestAPIRequest, 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]) authorization = request_options.delete(:authorization) 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[:attachment_urls].present? faraday.request(:url_encoded) #TODO: change these to faraday = self.auth_strategy.set_authorization!(request: faraday) unless request.without_headers if authorization if authorization.key?(:token) faraday.authorization(:Bearer, authorization[:token]) elsif authorization.key?(:basic_auth) faraday.request(:basic_auth, authorization[:basic_auth][:username], authorization[:basic_auth][:password]) end end end faraday.adapter Faraday.default_adapter end attachment_urls = options[:attachment_url] || options[:attachment_urls] if attachment_urls.present? resp = @faraday.post do |req| payload = {} # create an array to just iterate over for 1 or many urls attachment_urls = [attachment_urls] if options[:attachment_url] attachments = attachment_urls.map do |url| image = open(url, 'rb') Faraday::UploadIO.new(image, T.must(image).content_type, File.basename(url)) end if request_properties[:request_format] == :json payload[:json] = Faraday::UploadIO.new(StringIO.new(full_request), 'application/json') else payload.merge!(full_request) end # if using the singular "attachment_url" key pull out the first item payload[options[:attachment_url_key]] = options[:attachment_url] ? attachments.first : attachments req.body = payload end else if request.without_headers @faraday.headers = {} elsif request_options[:headers] @faraday.headers = (@faraday.headers || {}).merge(request_options[:headers]) end 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] 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, } 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 params = request_properties[:req_params].merge(request_options.except(:headers)) if request_properties[:request_method] == :get # Faraday expects get requests params to be a hash params elsif request_properties[:req_params].empty? if %i[put patch post].include?(request_properties[:request_method]) params.to_json # Faraday doesn't support an empty hash for PUT/PATCH/POST requests else params # Faraday supports empty hashes for other request types... end else # if the header or request format include urlencoded return the body as a hash if request_format.include?('urlencoded') params else # return either xml or json if request_properties[:request_format] == :xml convert_hash_to_xml(params) else params.to_json end end end end def request_options(request_format = nil, action_details = {}) request_options = {} (request_options = {})[:headers] = {} request_options[:headers] .merge!(self.auth_strategy.get_from_context(:auth_headers).except(:authorization)) .merge!(self.get_from_context(:api_headers)) .merge!(action_details[:request_options] || {}) request_options[:authorization] = action_details[:authorization] || self.auth_strategy.get_from_context(:auth_headers)[:authorization] 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