KDEJewellers/aptly-api

View on GitHub
lib/aptly/connection.rb

Summary

Maintainability
A
35 mins
Test Coverage
# Copyright (C) 2015-2022 Harald Sitter <sitter@kde.org>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library.  If not, see <http://www.gnu.org/licenses/>.

require 'faraday'
require 'faraday/excon'
require 'faraday/multipart'
require 'json'
require 'uri'

require_relative 'configuration'
require_relative 'errors'

module Aptly
  # Connection adaptor.
  # This class wraps HTTP interactions for our purposes and adds general purpose
  # automation on top of the raw HTTP actions.
  class Connection
    DEFAULT_QUERY = {}.freeze
    private_constant :DEFAULT_QUERY
    GETISH_ACTIONS = %i[get delete].freeze
    private_constant :GETISH_ACTIONS
    POSTISH_ACTIONS = %i[post put].freeze
    private_constant :POSTISH_ACTIONS
    WRITE_ACTIONS = (POSTISH_ACTIONS + %i[delete]).freeze
    private_constant :WRITE_ACTIONS
    HTTP_ACTIONS = GETISH_ACTIONS + POSTISH_ACTIONS
    private_constant :HTTP_ACTIONS

    CODE_ERRORS = {
      400 => Errors::ClientError,
      401 => Errors::UnauthorizedError,
      404 => Errors::NotFoundError,
      409 => Errors::ConflictError,
      500 => Errors::ServerError
    }.freeze
    private_constant :CODE_ERRORS

    # New connection.
    # @param config [Configuration] Configuration instance to use
    # @param query [Hash] Default HTTP query paramaters, these get the
    #   specific query parameters merged upon.
    # @param uri [URI] Base URI for the remote (default from
    #   {Configuration#uri}).
    def initialize(config: ::Aptly.configuration, query: DEFAULT_QUERY,
                   uri: config.uri)
      @query = query
      @base_uri = uri
      raise if faraday_uri.nil?
      @config = config
      @connection = Faraday.new(faraday_uri) do |c|
        c.request :multipart
        c.request :url_encoded
        c.adapter :excon, @adapter_options
      end
    end

    HTTP_ACTIONS.each do |action|
      # private api
      define_method(action) do |relative_path = '', kwords = {}|
        # NB: double splat is broken with Ruby 2.2.1's define_method, so we
        #   cannot splat kwords in the signature.
        kwords[:query] = build_query(kwords)
        kwords.delete(:query) if kwords[:query].empty?

        http_call(action, add_api(relative_path), kwords)
      end
    end

    private

    def faraday_uri
      @adapter_options ||= {}
      @faraday_uri ||= begin
        uri = @base_uri.clone
        return uri unless uri.scheme == 'unix'
        # For Unix domain sockets we need to divide the bits apart as Excon
        # expects the path URI without a socket path and the socket path as
        # option.
        @adapter_options[:socket] = uri.path
        uri.host = nil
        uri
      end
    end

    def build_query(kwords)
      query = @query.merge(kwords.delete(:query) { {} })
      if kwords.delete(:query_mangle) { true }
        query = query.map { |k, v| [k.to_s.capitalize, v] }.to_h
      end
      query
    end

    def add_api(relative_path)
      "/api#{relative_path}"
    end

    def mangle_post(body, headers, kwords)
      if body
        headers ||= {}
        headers['Content-Type'] = 'application/json'
      else
        kwords.each do |k, v|
          if k.to_s.start_with?('file_')
            body ||= {}
            body[k] = Faraday::UploadIO.new(v, 'application/binary')
          end
        end
      end
      [body, headers]
    end

    def setup_request(action, request)
      standard_timeout = @config.timeout
      standard_timeout = @config.write_timeout if WRITE_ACTIONS.include?(action)
      request.options.timeout = standard_timeout
    end

    def run_postish(action, path, kwords)
      body = kwords.delete(:body)
      params = kwords.delete(:query)
      headers = kwords.delete(:headers)

      body, headers = mangle_post(body, headers, kwords)

      @connection.send(action, path, body, headers) do |request|
        setup_request(action, request)
        request.params.update(params) if params
      end
    end

    def run_getish(action, path, kwords)
      body = kwords.delete(:body)
      params = kwords.delete(:query)
      headers = kwords.delete(:headers)

      @connection.send(action, path, params, headers) do |request|
        setup_request(action, request)
        if body
          request.headers[:content_type] = 'application/json'
          request.body = body
        end
      end
    end

    def handle_error(response)
      error = CODE_ERRORS.fetch(response.status, nil)
      raise error, response.body if error
      response
    end

    def http_call(action, path, kwords)
      if POSTISH_ACTIONS.include?(action)
        response = run_postish(action, path, kwords)
      elsif GETISH_ACTIONS.include?(action)
        response = run_getish(action, path, kwords)
      else
        raise "Unknown http action: #{action}"
      end
      handle_error(response)
    rescue Faraday::TimeoutError => e
      raise Errors::TimeoutError, e.message
    end
  end
end