# typed: ignore # frozen_string_literal: true module Setsuzoku module Service # Defines all common methods for handling interfacing with a Web Service API. module WebService class ApiStrategy include Setsuzoku::ApiStrategy extend T::Sig extend T::Helpers # These are interface methods that a plugin that implements this strategy must implement. module InterfaceMethods extend T::Sig extend T::Helpers interface! # The base Web Service API URL for the plugin. # # @return [String] the API base url. sig { abstract.returns(String) } def api_base_url; end # the base webhook api url # # @return [String] the webhook base url def webhook_base_url; end # The collection of all implemented endpoints for the API. # Formatted as: { api_action: { 'HTTP_VERB': 'api.com/endpoints' } } # # @return [Hash] all endpoint definitions for the API. sig { abstract.returns(T::Hash[T.untyped, T.untyped]) } def api_actions; end # All dynamic url parameters provided by the plugin. # # @return [Hash(String)] all parameters that need to be replaced dynamically for url requests. sig { abstract.returns(T::Hash[Symbol, T.nilable(String)]) } def dynamic_url_params; end end # # api_headers sig { overridable.returns(T::Hash[Symbol, T.untyped]) } # # Specific API headers an API strategy must have when making requests. # # @return [Hash] the additional required headers for a request. def api_headers {} end # Perform the external call for the external API. # Each WebService::ApiStrategy must define how this works. # # It should: # 1. Make the external request # 2. Parse the response # 3. Handle any bad response # 4. Format the response # # @param request [APIRequest] the request object to be used for the request. Each strategy defines its request structure. # @param action_details [Hash] the action_details for the action to execute. # @param options [Any] any additional options needed to pass to correctly perform the request. # # @return [Any] the formatted response object. sig { override.params(request: T.untyped, action_details: T::Hash[T.untyped, T.untyped], options: T.untyped).returns(T.untyped) } def perform_external_call(request:, action_details:, **options); end # Parse the response from the API for the given request. # This should just convert JSON strings/XML/SQL rows to a formatted response Hash. # # @param response [Faraday::Response] the response from the HTTP request. # @param options [Hash] the parsing options. Generally the response_type. e.g. :xml, :json # # @return [Hash] the parsed hash of the response object. sig { override.params(response: Faraday::Response, options: T.untyped).returns(T.untyped) } def parse_response(response:, **options) case options[:response_type] when :json JSON.parse(response.body).deep_symbolize_keys when :xml convert_xml_to_hash(response.body) when :html response.body else JSON.parse(response.body).deep_symbolize_keys end end private sig { params(hash: T::Hash[Symbol, T.untyped], mutate_keys: T::Boolean).returns(String)} def convert_hash_to_xml(hash, mutate_keys = true) hash = hash.map do |k, v| text = if v.is_a?(Hash) convert_hash_to_xml(v) elsif v.is_a?(Array) v.map do |elem| convert_hash_to_xml(elem) end.join else v end k = k.to_s.camelize if mutate_keys "<%s>%s" % [k, text, k] end.join hash end # Convert an XML string to a usable hash. sig { params(xml: String).returns(T::Hash[Symbol, T.untyped])} def convert_xml_to_hash(xml) begin result = Nokogiri::XML(xml) return { result.root.name.underscore.to_sym => xml_node_to_hash(result.root)} rescue Exception => e {} end end def xml_node_to_hash(node) # If we are at the root of the document, start the hash if node.element? result_hash = {} if node.attributes != {} attributes = {} node.attributes.keys.each do |key| attributes[node.attributes[key].name.underscore.to_sym] = node.attributes[key].value end end if node.children.size > 0 node.children.each do |child| result = xml_node_to_hash(child) if child.name == "text" unless child.next_sibling || child.previous_sibling return result unless attributes result_hash[child.name.underscore.to_sym] = result end elsif result_hash[child.name.to_sym] if result_hash[child.name.to_sym].is_a?(Array) result_hash[child.name.underscore.to_sym] << result else result_hash[child.name.underscore.to_sym] = [result_hash[child.name.to_sym]] << result end else stripped_children = node.children.reject{ |n| n.text.strip.blank? } if stripped_children.length > 1 && stripped_children.combination(2).all? { |a,b| a.name == b.name } return result_hash[node.name.underscore.to_sym] = stripped_children.map{|n| xml_node_to_hash(n) } else result_hash[child.name.underscore.to_sym] = result end end end if attributes #add code to remove non-data attributes e.g. xml schema, namespace here #if there is a collision then node content supersets attributes result_hash = attributes.merge(result_hash) end return result_hash else return attributes end else return node.content.to_s end end end end end end