File: http_client.rb

package info (click to toggle)
ruby-asana 2.0.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,020 kB
  • sloc: ruby: 3,314; javascript: 8; makefile: 3
file content (249 lines) | stat: -rw-r--r-- 8,735 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# frozen_string_literal: true

require 'faraday'
require 'faraday/follow_redirects'

require_relative 'http_client/error_handling'
require_relative 'http_client/environment_info'
require_relative 'http_client/response'

module Asana
  # Internal: Wrapper over Faraday that abstracts authentication, request
  # parsing and common options.
  class HttpClient
    # Internal: The API base URI.
    BASE_URI = 'https://app.asana.com/api/1.0'

    # Public: Initializes an HttpClient to make requests to the Asana API.
    #
    # authentication - [Asana::Authentication] An authentication strategy.
    # adapter        - [Symbol, Proc] A Faraday adapter, eiter a Symbol for
    #                  registered adapters or a Proc taking a builder for a
    #                  custom one. Defaults to Faraday.default_adapter.
    # user_agent     - [String] The user agent. Defaults to "ruby-asana vX.Y.Z".
    # config         - [Proc] An optional block that yields the Faraday builder
    #                  object for customization.
    def initialize(authentication: required('authentication'),
                   adapter: nil,
                   user_agent: nil,
                   debug_mode: false,
                   log_asana_change_warnings: true,
                   default_headers: nil,
                   &config)
      @authentication             = authentication
      @adapter                    = adapter || Faraday.default_adapter
      @environment_info           = EnvironmentInfo.new(user_agent)
      @debug_mode                 = debug_mode
      @log_asana_change_warnings  = log_asana_change_warnings
      @default_headers            = default_headers
      @config                     = config
    end

    # Public: Performs a GET request against the API.
    #
    # resource_uri - [String] the resource URI relative to the base Asana API
    #                URL, e.g "/users/me".
    # params       - [Hash] the request parameters
    # options      - [Hash] the request I/O options
    #
    # Returns an [Asana::HttpClient::Response] if everything went well.
    # Raises [Asana::Errors::APIError] if anything went wrong.
    def get(resource_uri, params: {}, options: {})
      opts = options.reduce({}) do |acc, (k, v)|
        acc.tap do |hash|
          hash[:"opt_#{k}"] = v.is_a?(Array) ? v.join(',') : v
        end
      end
      perform_request(:get, resource_uri, params.merge(opts), options[:headers])
    end

    # Public: Performs a PUT request against the API.
    #
    # resource_uri - [String] the resource URI relative to the base Asana API
    #                URL, e.g "/users/me".
    # body         - [Hash] the body to PUT.
    # options      - [Hash] the request I/O options
    #
    # Returns an [Asana::HttpClient::Response] if everything went well.
    # Raises [Asana::Errors::APIError] if anything went wrong.
    def put(resource_uri, body: {}, options: {})
      opts = options.reduce({}) do |acc, (k, v)|
        acc.tap do |hash|
          hash[:"opt_#{k}"] = v.is_a?(Array) ? v.join(',') : v
        end
      end
      options.merge(opts)
      params = { data: body }.merge(options.empty? ? {} : { options: options })
      perform_request(:put, resource_uri, params, options[:headers])
    end

    # Public: Performs a POST request against the API.
    #
    # resource_uri - [String] the resource URI relative to the base Asana API
    #                URL, e.g "/tags".
    # body         - [Hash] the body to POST.
    # upload       - [Faraday::UploadIO] an upload object to post as multipart.
    #                Defaults to nil.
    # options      - [Hash] the request I/O options
    #
    # Returns an [Asana::HttpClient::Response] if everything went well.
    # Raises [Asana::Errors::APIError] if anything went wrong.
    def post(resource_uri, body: {}, upload: nil, options: {})
      opts = options.reduce({}) do |acc, (k, v)|
        acc.tap do |hash|
          hash[:"opt_#{k}"] = v.is_a?(Array) ? v.join(',') : v
        end
      end
      options.merge(opts)
      params = { data: body }.merge(options.empty? ? {} : { options: options })
      if upload
        perform_request(:post, resource_uri, params.merge(file: upload), options[:headers]) do |c|
          c.request :multipart
        end
      else
        perform_request(:post, resource_uri, params, options[:headers])
      end
    end

    # Public: Performs a DELETE request against the API.
    #
    # resource_uri - [String] the resource URI relative to the base Asana API
    #                URL, e.g "/tags".
    # options      - [Hash] the request I/O options
    #
    # Returns an [Asana::HttpClient::Response] if everything went well.
    # Raises [Asana::Errors::APIError] if anything went wrong.
    def delete(resource_uri, params: {}, options: {})
      opts = options.reduce({}) do |acc, (k, v)|
        acc.tap do |hash|
          hash[:"opt_#{k}"] = v.is_a?(Array) ? v.join(',') : v
        end
      end
      perform_request(:delete, resource_uri, params.merge(opts), options[:headers])
    end

    private

    def connection(&request_config)
      Faraday.new do |builder|
        @authentication.configure(builder)
        @environment_info.configure(builder)
        yield builder if request_config
        configure_format(builder)
        add_middleware(builder)
        configure_redirects(builder)
        @config&.call(builder)
        use_adapter(builder, @adapter)
      end
    end

    def perform_request(method, resource_uri, body = {}, headers = {}, &request_config)
      handling_errors do
        url = BASE_URI + resource_uri
        headers = (@default_headers || {}).merge(headers || {})
        log_request(method, url, body) if @debug_mode
        result = Response.new(connection(&request_config).public_send(method, url, body, headers))
        log_asana_change_headers(headers, result.headers) if @log_asana_change_warnings
        result
      end
    end

    def configure_format(builder)
      builder.request :json
      builder.response :json
    end

    def add_middleware(builder)
      builder.use Faraday::Response::RaiseError
    end

    def configure_redirects(builder)
      builder.response :follow_redirects
    end

    def use_adapter(builder, adapter)
      case adapter
      when Symbol
        builder.adapter(adapter)
      when Proc
        adapter.call(builder)
      end
    end

    def handling_errors(&request)
      ErrorHandling.handle(&request)
    end

    def log_request(method, url, body)
      warn format('[%<klass>s] %<method>s %<url>s (%<body>s)',
                  klass: self.class,
                  method: method.to_s.upcase,
                  url: url,
                  body: body.inspect)
    end

    # rubocop:disable Metrics/AbcSize
    # rubocop:disable Metrics/MethodLength
    def log_asana_change_headers(request_headers, response_headers)
      change_header_key = nil

      response_headers.each_key do |key|
        change_header_key = key if key.downcase == 'asana-change'
      end

      return if change_header_key.nil?

      accounted_for_flags = []

      request_headers = {} if request_headers.nil?
      # Grab the request's asana-enable flags
      request_headers.each_key do |req_header|
        case req_header.downcase
        when 'asana-enable', 'asana-disable'
          request_headers[req_header].split(',').each do |flag|
            accounted_for_flags.push(flag)
          end
        end
      end

      changes = response_headers[change_header_key].split(',')

      changes.each do |unsplit_change|
        change = unsplit_change.split(';')

        name = nil
        info = nil
        affected = nil

        change.each do |unsplit_field|
          field = unsplit_field.split('=')

          field[0].strip!
          field[1].strip!
          case field[0]
          when 'name'
            name = field[1]
          when 'info'
            info = field[1]
          when 'affected'
            affected = field[1]
          end

          # Only show the error if the flag was not in the request's asana-enable header
          next unless !(accounted_for_flags.include? name) && (affected == 'true')

          message1 = 'This request is affected by the "%s" ' \
                     'deprecation. Please visit this url for more info: %s'
          message2 = 'Adding "%s" to your "Asana-Enable" or ' \
                     '"Asana-Disable" header will opt in/out to this deprecation ' \
                     'and suppress this warning.'

          warn format(message1, name, info)
          warn format(message2, name)
        end
      end
    end
    # rubocop:enable Metrics/AbcSize
    # rubocop:enable Metrics/MethodLength
  end
end