Class: Miasma::Types::Api

Inherits:
Object
  • Object
show all
Includes:
Bogo::Logger::Helpers, Utils::Lazy, Utils::Memoization
Defined in:
lib/miasma/types/api.rb

Overview

Remote API connection

Constant Summary collapse

VALID_REQUEST_RETRY_METHODS =

HTTP request methods that are allowed retry

[:get, :head]
MAX_REQUEST_RETRIES =

Maximum allowed HTTP request retries (for non-HTTP related errors)

5

Instance Method Summary collapse

Constructor Details

#initialize(creds) ⇒ self

Create new API connection

Parameters:

  • creds (Smash)

    credentials



25
26
27
28
29
30
31
32
33
34
35
# File 'lib/miasma/types/api.rb', line 25

def initialize(creds)
  custom_setup(creds)
  if creds.is_a?(Hash)
    load_data(creds)
  else
    raise TypeError, "Expecting `credentials` to be of type `Hash`. " \
          "Received: `#{creds.class}`"
  end
  after_setup(creds)
  connect
end

Instance Method Details

#after_setup(creds) ⇒ TrueClass

Simple hook for concrete APIs to make adjustments after attribute population and prior to connection

Parameters:

  • creds (Hash)

Returns:

  • (TrueClass)


51
52
53
# File 'lib/miasma/types/api.rb', line 51

def after_setup(creds)
  true
end

#api_for(type) ⇒ Api

Build new API for specified type using current provider / creds

Parameters:

  • type (Symbol)

    api type

Returns:



71
72
73
74
75
76
77
78
79
80
81
# File 'lib/miasma/types/api.rb', line 71

def api_for(type)
  memoize(type) do
    Miasma.api(
      Smash.new(
        :type => type,
        :provider => provider,
        :credentials => attributes,
      )
    )
  end
end

#connectself

Connect to the remote API

Returns:

  • (self)


63
64
65
# File 'lib/miasma/types/api.rb', line 63

def connect
  self
end

#connectionHTTP

Returns:

  • (HTTP)


84
85
86
# File 'lib/miasma/types/api.rb', line 84

def connection
  HTTP.headers("User-Agent" => "miasma/v#{Miasma::VERSION}")
end

#custom_setup(creds) ⇒ TrueClass

Simple hook for concrete APIs to make adjustments prior to initialization and connection

Parameters:

  • creds (Hash)

Returns:

  • (TrueClass)


42
43
44
# File 'lib/miasma/types/api.rb', line 42

def custom_setup(creds)
  true
end

#endpointString

Returns url endpoint.

Returns:

  • (String)

    url endpoint



89
90
91
# File 'lib/miasma/types/api.rb', line 89

def endpoint
  "http://api.example.com"
end

#format_response(result, extract_body = true) ⇒ Smash

Makes best attempt at formatting response

Parameters:

  • result (HTTP::Response)
  • extract_body (TrueClass, FalseClass) (defaults to: true)

    automatically extract body

Returns:

  • (Smash)


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
# File 'lib/miasma/types/api.rb', line 188

def format_response(result, extract_body = true)
  extracted_headers = Smash[result.headers.map { |k, v| [Utils.snake(k), v] }]
  if extract_body
    logger.debug("extracting HTTP response body")
    body_content = result.body.to_s
    body_content.encode!("UTF-8", "binary",
                         :invalid => :replace,
                         :undef => :replace,
                         :replace => "")
    if extracted_headers[:content_type].to_s.include?("json")
      logger.debug("unpacking HTTP response body using JSON")
      extracted_body = from_json(body_content) || body_content
    elsif extracted_headers[:content_type].to_s.include?("xml")
      logger.debug("unpacking HTTP response body using XML")
      extracted_body = from_xml(body_content) || body_content
    else
      logger.debug("unpacking HTTP response body using best effort")
      extracted_body = from_json(body_content) ||
                       from_xml(body_content) ||
                       body_content
    end
  end
  unless extracted_body
    # @note if body is over 100KB, do not extract
    if extracted_headers[:content_length].to_i < 102400
      logger.warn("skipping body extraction and formatting due to size")
      extracted_body = result.body.to_s
    else
      extracted_body = result.body
    end
  end
  Smash.new(
    :response => result,
    :headers => extracted_headers,
    :body => extracted_body,
  )
end

#from_json(string) ⇒ Hash, ...

Convert from JSON

Parameters:

  • string (String)

Returns:

  • (Hash, Array, NilClass)


230
231
232
233
234
235
236
237
238
239
# File 'lib/miasma/types/api.rb', line 230

def from_json(string)
  begin
    MultiJson.load(string).to_smash
  rescue MultiJson::ParseError => err
    logger.error("failed to load JSON string - #{err.class}: #{err}")
    logger.debug("JSON string load failure: #{string.inspect}")
    logger.debug("JSON error - #{err.class}: #{err}\n#{err.backtrace.join("\n")}")
    nil
  end
end

#from_xml(string) ⇒ Hash, ...

Convert from JSON

Parameters:

  • string (String)

Returns:

  • (Hash, Array, NilClass)


245
246
247
248
249
250
251
252
253
254
# File 'lib/miasma/types/api.rb', line 245

def from_xml(string)
  begin
    MultiXml.parse(string).to_smash
  rescue MultiXml::ParseError => err
    logger.error("failed to load XML string - #{err.class}: #{err}")
    logger.debug("XML string load failure: #{string.inspect}")
    logger.debug("XML error - #{err.class}: #{err}\n#{err.backtrace.join("\n")}")
    nil
  end
end

#make_request(connection, http_method, request_args) ⇒ HTTP::Response

Note:

this is mainly here for concrete APIs to override if things need to be done prior to the actual request (like signature generation)

Perform request

Parameters:

  • connection (HTTP)
  • http_method (Symbol)
  • request_args (Array)

Returns:

  • (HTTP::Response)


178
179
180
181
# File 'lib/miasma/types/api.rb', line 178

def make_request(connection, http_method, request_args)
  logger.debug("making #{http_method.to_s.upcase} request - #{request_args.inspect}")
  connection.send(http_method, *request_args)
end

#perform_request_retry(exception) ⇒ TrueClass, FalseClass

Determine if a retry on the request should be performed

Parameters:

  • exception (Exception)

Returns:

  • (TrueClass, FalseClass)


165
166
167
# File 'lib/miasma/types/api.rb', line 165

def perform_request_retry(exception)
  true
end

#providerSymbol

Returns name of provider.

Returns:

  • (Symbol)

    name of provider



56
57
58
# File 'lib/miasma/types/api.rb', line 56

def provider
  Utils.snake(self.class.to_s.split("::").last).to_sym
end

#request(args) ⇒ Smash

Perform request to remote API

Parameters:

  • args (Hash)

    options

Options Hash (args):

  • :method (String, Symbol)

    HTTP request method

  • :path (String)

    request path

  • :expects (Integer, Array<Integer>)

    expected response status code

  • :disable_body_extraction (TrueClass, FalseClass)

    do not auto-parse response body

Returns:

  • (Smash)

    => HTTP::Response, :headers => Smash, :body => Object



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
# File 'lib/miasma/types/api.rb', line 102

def request(args)
  args = args.to_smash
  http_method = args.fetch(:method, "get").to_s.downcase.to_sym
  unless HTTP::Request::METHODS.include?(http_method)
    raise ArgumentError.new "Invalid request method provided!"
  end
  request_args = [].tap do |ary|
    _endpoint = args.delete(:endpoint) || endpoint
    ary.push(File.join(_endpoint, args[:path].to_s))
    options = {}.tap do |opts|
      [:form, :params, :json, :body].each do |key|
        opts[key] = args[key] if args[key]
      end
    end
    ary.push(options) unless options.empty?
  end
  if args[:headers]
    _connection = connection.headers(args[:headers])
    args.delete(:headers)
  else
    _connection = connection
  end
  result = retryable_request(http_method) do
    res = make_request(_connection, http_method, request_args)
    unless [args.fetch(:expects, 200)].flatten.compact.map(&:to_i).include?(res.code)
      raise Error::ApiError::RequestError.new(res.reason, :response => res)
    end
    res
  end
  format_response(result, !args[:disable_body_extraction])
end

#retryable_allowed?(http_method) ⇒ TrueClass, FalseClass

Determine if request type is allowed to be retried

Parameters:

  • http_method (Symbol)

    request type

Returns:

  • (TrueClass, FalseClass)


157
158
159
# File 'lib/miasma/types/api.rb', line 157

def retryable_allowed?(http_method)
  VALID_REQUEST_RETRY_METHODS.include?(http_method)
end

#retryable_request(http_method) { ... } ⇒ Object

If HTTP request method is allowed to be retried then retry request on non-response failures. Otherwise just re-raise immediately

Parameters:

  • http_method (Symbol)

    HTTP request method

Yields:

  • request to be retried if allowed

Returns:

  • (Object)

    result of block



141
142
143
144
145
146
147
148
149
150
151
# File 'lib/miasma/types/api.rb', line 141

def retryable_request(http_method, &block)
  Bogo::Retry.build(
    data.fetch(:retry_type, :exponential),
    :max_attempts => retryable_allowed?(http_method) ?
      data.fetch(:retry_max, MAX_REQUEST_RETRIES) : 0,
    :wait_interval => data[:retry_interval],
    :ui => data[:retry_ui],
    :auto_run => false,
    &block
  ).run! { |e| perform_request_retry(e) }
end