Class: Scorpio::Request

Inherits:
Object
  • Object
show all
Includes:
Configurables
Defined in:
lib/scorpio/request.rb

Defined Under Namespace

Modules: Configurables

Constant Summary collapse

SUPPORTED_REQUEST_MEDIA_TYPES =

media types for which Scorpio has implemented generating / parsing between body and body_object (see Configurables#body and Scorpio::Response#body_object)

%w(
  application/json
  application/x-www-form-urlencoded
).map(&:freeze).freeze
FALLBACK_CONTENT_TYPE =
'application/x-www-form-urlencoded'.freeze
METHODS_WITH_BODIES =

see also Faraday::Env::MethodsWithBodies

%w(post put patch options).map(&:freeze).freeze

Instance Attribute Summary collapse

Attributes included from Configurables

#base_url, #body, #body_object, #faraday_adapter, #faraday_builder, #headers, #logger, #media_type, #path_params, #query_params, #scheme, #server, #server_variables, #user_agent

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(configuration = {}, &b) ⇒ Request

Returns a new instance of Request.

Parameters:

  • configuration (#to_hash) (defaults to: {})

    a hash keyed with configurable attributes for the request - instance methods of Scorpio::Request::Configurables, whose values will be assigned for those attributes.



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/scorpio/request.rb', line 146

def initialize(configuration = {}, &b)
  configuration = JSI::Util.stringify_symbol_keys(configuration)
  params_set = Set.new # the set of params that have been set
  # do the Configurables first
  configuration.each do |name, value|
    if Configurables.public_method_defined?("#{name}=")
      Configurables.instance_method("#{name}=").bind(self).call(value)
      params_set << name
    end
  end
  # then do other top-level params
  configuration.reject { |name, _| params_set.include?(name) }.each do |name, value|
    param = param_for(name) || raise(ArgumentError, "unrecognized configuration value passed: #{name.inspect}")
    set_param_from(param['in'], param['name'], value)
  end

  if block_given?
    yield self
  end
end

Instance Attribute Details

#operationScorpio::OpenAPI::Operation (readonly)



168
169
170
# File 'lib/scorpio/request.rb', line 168

def operation
  @operation
end

Class Method Details

.best_media_type(media_types) ⇒ Object



17
18
19
20
21
22
23
# File 'lib/scorpio/request.rb', line 17

def self.best_media_type(media_types)
  if media_types.size == 1
    media_types.first
  else
    SUPPORTED_REQUEST_MEDIA_TYPES.detect { |mt| media_types.include?(mt) }
  end
end

.method_with_body?(http_method) ⇒ Boolean

Parameters:

  • http_method (String)

Returns:

  • (Boolean)

Raises:

  • (ArgumentError)


27
28
29
30
# File 'lib/scorpio/request.rb', line 27

def self.method_with_body?(http_method)
  raise(ArgumentError) unless http_method.is_a?(String)
  METHODS_WITH_BODIES.include?(http_method.downcase)
end

.request_class_by_operation(operation) ⇒ Object



139
140
141
# File 'lib/scorpio/request.rb', line 139

def self.request_class_by_operation(operation)
  @request_class_by_operation[operation]
end

Instance Method Details

#content_type::Ur::ContentType

Content-Type for this request, taken from request headers if present, or the request Scorpio::Request::Configurables#media_type.

Returns:

  • (::Ur::ContentType)


233
234
235
# File 'lib/scorpio/request.rb', line 233

def content_type
  content_type_header || (media_type ? ::Ur::ContentType.new(media_type) : nil)
end

#content_type_header::Ur::ContentType

the value of the request Content-Type header

Returns:

  • (::Ur::ContentType)


223
224
225
226
227
228
# File 'lib/scorpio/request.rb', line 223

def content_type_header
  headers.each do |k, v|
    return ::Ur::ContentType.new(v) if k =~ /\Acontent[-_]type\z/i
  end
  nil
end

#each_page_ur(next_page:, raise_on_http_error: true) {|Scorpio::Ur| ... } ⇒ Enumerator?

Runs this request, passing the resulting Ur to the given block. The next_page callable is then called with that Ur and results in the next page's Ur, or nil. This repeats until the next_page call results in nil.

See OpenAPI::Operation#each_link_page for integration with an OpenAPI Operation.

Parameters:

  • next_page (#call)

    a callable which will take a parameter page_ur, which is a Ur, and must result in an Ur representing the next page, which will be yielded to the block.

Yields:

  • (Scorpio::Ur)

    yields the first page, and each subsequent result of calls to next_page until that results in nil

Returns:

  • (Enumerator, nil)


404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# File 'lib/scorpio/request.rb', line 404

def each_page_ur(next_page: , raise_on_http_error: true)
  return to_enum(__method__, next_page: next_page, raise_on_http_error: raise_on_http_error) unless block_given?
  page_ur = run_ur
  while page_ur
    unless page_ur.is_a?(Scorpio::Ur)
      raise(TypeError, [
        "next_page must result in a #{Scorpio::Ur}",
        "this should be the result of #run_ur from a #{OpenAPI::Operation} or #{Request}",
      ].join("\n"))
    end
    page_ur.raise_on_http_error if raise_on_http_error
    yield page_ur
    page_ur = next_page.call(page_ur)
  end
  nil
end

#faraday_connection(yield_ur = nil) ⇒ ::Faraday::Connection

builds a Faraday connection with this Request's faraday_builder and faraday_adapter. passes a given proc yield_ur to middleware to yield an Ur for requests made with the connection.

Parameters:

  • yield_ur (Proc) (defaults to: nil)

Returns:

  • (::Faraday::Connection)


247
248
249
250
251
252
253
254
255
256
257
# File 'lib/scorpio/request.rb', line 247

def faraday_connection(yield_ur = nil)
  Faraday.new do |faraday_connection|
    faraday_builder.call(faraday_connection)
    if yield_ur
      -> { ::Ur::Faraday }.() # autoload trigger

      faraday_connection.response(:yield_ur, schemas: Set[Scorpio::Ur.schema], logger: self.logger, &yield_ur)
    end
    faraday_connection.adapter(*faraday_adapter)
  end
end

#get_param(name) ⇒ Object

returns the value of the named parameter on this request

Parameters:

  • name (String, Symbol)

    the 'name' property of one applicable parameter

Returns:

  • (Object)

Raises:



275
276
277
278
# File 'lib/scorpio/request.rb', line 275

def get_param(name)
  param = param_for!(name)
  get_param_from(param['in'], param['name'])
end

#get_param_from(param_in, name) ⇒ Object

returns the value of the named parameter from the specified param_in on this request

Parameters:

  • param_in (String, Symbol)

    one of 'path', 'query', 'header', or 'cookie' - where to retrieve the named value

  • name (String, Symbol)

    the parameter name

Returns:

  • (Object)

Raises:

  • (OpenAPI::SemanticError)

    invalid param_in parameter

  • (NotImplementedError)

    cookies aren't implemented



337
338
339
340
341
342
343
344
345
346
347
348
349
350
# File 'lib/scorpio/request.rb', line 337

def get_param_from(param_in, name)
  if param_in == 'path'
    path_params[name]
  elsif param_in == 'query'
    query_params ? query_params[name] : nil
  elsif param_in == 'header'
    _, value = headers.detect { |headername, _| headername.downcase == name.downcase }
    value
  elsif param_in == 'cookie'
    raise(NotImplementedError, "cookies not implemented: #{name.inspect}")
  else
    raise(OpenAPI::SemanticError, "cannot get param from param_in = #{param_in.inspect} (name: #{name.pretty_inspect.chomp})")
  end
end

#http_methodString

the http method for this request

Returns:

  • (String)


177
178
179
# File 'lib/scorpio/request.rb', line 177

def http_method
  operation.http_method
end

#openapi_documentScorpio::OpenAPI::Document



171
172
173
# File 'lib/scorpio/request.rb', line 171

def openapi_document
  operation.openapi_document
end

#param_for(name) ⇒ #to_hash?

Parameters:

  • name (String, Symbol)

    the 'name' property of one applicable parameter

Returns:

  • (#to_hash, nil)

Raises:



283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/scorpio/request.rb', line 283

def param_for(name)
  name = name.to_s if name.is_a?(Symbol)
  params = operation.inferred_parameters.select { |p| p['name'] == name }
  if params.size == 1
    params.first
  elsif params.size == 0
    nil
  else
    raise(AmbiguousParameter.new(
      "There are multiple parameters for #{name}. matched parameters were: #{params.pretty_inspect.chomp}"
    ).tap { |e| e.name = name })
  end
end

#param_for!(name) ⇒ #to_hash

Parameters:

  • name (String, Symbol)

    the 'name' property of one applicable parameter

Returns:

  • (#to_hash)

Raises:



301
302
303
# File 'lib/scorpio/request.rb', line 301

def param_for!(name)
  param_for(name) || raise(ParameterError, "There is no parameter named #{name} on operation #{operation.human_id}:\n#{operation.pretty_inspect.chomp}")
end

#pathAddressable::URI

an Addressable::URI containing only the path to append to the Scorpio::Request::Configurables#base_url for this request

Returns:

  • (Addressable::URI)


190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/scorpio/request.rb', line 190

def path
  path_params = JSI::Util.stringify_symbol_keys(self.path_params)
  missing_variables = path_template.variables - path_params.keys
  if missing_variables.any?
    raise(ArgumentError, "path #{operation.path_template_str} for operation #{operation.human_id} requires path_params " +
      "which were missing: #{missing_variables.inspect}")
  end
  empty_variables = path_template.variables.select { |v| path_params[v].to_s.empty? }
  if empty_variables.any?
    raise(ArgumentError, "path #{operation.path_template_str} for operation #{operation.human_id} requires path_params " +
      "which were empty: #{empty_variables.inspect}")
  end

  path = path_template.expand(path_params)
  if query_params
    path.query_values = query_params
  end
  path.freeze
end

#path_templateAddressable::Template

the template for the request's path, to be expanded with Scorpio::Request::Configurables#path_params and appended to the request's Scorpio::Request::Configurables#base_url

Returns:

  • (Addressable::Template)


184
185
186
# File 'lib/scorpio/request.rb', line 184

def path_template
  operation.path_template
end

#request_schema(media_type: self.media_type) ⇒ ::JSI::Schema

Returns:

  • (::JSI::Schema)


238
239
240
# File 'lib/scorpio/request.rb', line 238

def request_schema(media_type: self.media_type)
  operation.request_schema(media_type: media_type)
end

#runObject

runs this request. returns the response body object - that is, the response body parsed according to an understood media type, and instantiated with the applicable response schema if one is specified. see Scorpio::Response#body_object for more detail.

Raises:



387
388
389
390
391
# File 'lib/scorpio/request.rb', line 387

def run
  ur = run_ur
  ur.raise_on_http_error
  ur.response.body_object
end

#run_urScorpio::Ur

runs this request and returns the full representation of the request that was run and its response.

Returns:



355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
# File 'lib/scorpio/request.rb', line 355

def run_ur
  headers = {}
  if user_agent
    headers['User-Agent'] = user_agent
  end
  if !content_type_header
    if media_type
      headers['Content-Type'] = media_type
    else
      # I'd rather not have a default content-type, but if none is set then the HTTP adapter sets this to 
      # application/x-www-form-urlencoded and issues a warning about it.
      if METHODS_WITH_BODIES.include?(http_method.to_s)
        headers['Content-Type'] = FALLBACK_CONTENT_TYPE
      end
    end
  end
  headers.update(self.headers)
  body = self.body

  ur = nil
  conn = faraday_connection(-> (yur) { ur = yur })
  conn.run_request(http_method.downcase.to_sym, url, body, headers)
  ur.scorpio_request = self
  ur
end

#set_param(name, value) ⇒ Object

if there is only one parameter with the given name, of any sort, this will set it.

Parameters:

  • name (String, Symbol)

    the 'name' property of one applicable parameter

  • value (Object)

    the applicable parameter will be applied to the request with the given value.

Returns:

  • (Object)

    echoes the value param

Raises:



265
266
267
268
269
# File 'lib/scorpio/request.rb', line 265

def set_param(name, value)
  param = param_for!(name)
  set_param_from(param['in'], param['name'], value)
  value
end

#set_param_from(param_in, name, value) ⇒ Object

applies the named value to the appropriate parameter of the request

Parameters:

  • param_in (String, Symbol)

    one of 'path', 'query', 'header', or 'cookie' - where to apply the named value

  • name (String, Symbol)

    the parameter name to apply the value to

  • value (Object)

    the value

Returns:

  • (Object)

    echoes the value param

Raises:

  • (ArgumentError)

    invalid param_in parameter

  • (NotImplementedError)

    cookies aren't implemented



313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/scorpio/request.rb', line 313

def set_param_from(param_in, name, value)
  param_in = param_in.to_s if param_in.is_a?(Symbol)
  name = name.to_s if name.is_a?(Symbol)
  if param_in == 'path'
    self.path_params = self.path_params.merge(name => value)
  elsif param_in == 'query'
    self.query_params = (self.query_params || {}).merge(name => value)
  elsif param_in == 'header'
    self.headers = self.headers.merge(name => value)
  elsif param_in == 'cookie'
    raise(NotImplementedError, "cookies not implemented: #{name.inspect} => #{value.inspect}")
  else
    raise(ArgumentError, "cannot set param from param_in = #{param_in.inspect} (name: #{name.pretty_inspect.chomp}, value: #{value.pretty_inspect.chomp})")
  end
  value
end

#urlAddressable::URI

the full URL for this request

Returns:

  • (Addressable::URI)


212
213
214
215
216
217
218
219
# File 'lib/scorpio/request.rb', line 212

def url
  unless base_url
    raise(ArgumentError, "no base_url has been specified for request")
  end
  # we do not use Addressable::URI#join as the paths should just be concatenated, not resolved.
  # we use File.join just to deal with consecutive slashes.
  Addressable::URI.parse(File.join(base_url, path)).freeze
end