Class: RageController::API

Inherits:
Object
  • Object
show all
Defined in:
lib/rage/controller/api.rb

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.after_action(action_name = nil, **opts, &block) ⇒ Object

Register a new after_action hook. Calls with the same action_name will overwrite the previous ones.

Examples:

after_action :log_detailed_metrics, only: :create

Parameters:

  • action_name (Symbol, nil) (defaults to: nil)

    the name of the callback to add

  • opts (Hash)

    action options

Options Hash (**opts):

  • :only (Symbol, Array<Symbol>)

    restrict the callback to run only for specific actions

  • :except (Symbol, Array<Symbol>)

    restrict the callback to run for all actions except specified

  • :if (Symbol, Proc)

    only run the callback if the condition is true

  • :unless (Symbol, Proc)

    only run the callback if the condition is false



342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/rage/controller/api.rb', line 342

def after_action(action_name = nil, **opts, &block)
  action = prepare_action_params(action_name, **opts, &block)

  if @__after_actions && @__after_actions.frozen?
    @__after_actions = @__after_actions.dup
  end

  if @__after_actions.nil?
    @__after_actions = [action]
  elsif (i = @__after_actions.find_index { |a| a[:name] == action_name })
    @__after_actions[i] = action
  else
    @__after_actions << action
  end
end

.around_action(action_name = nil, **opts, &block) ⇒ Object

Register a new around_action hook. Calls with the same action_name will overwrite the previous ones.

Examples:

around_action :wrap_in_transaction

def wrap_in_transaction
  ActiveRecord::Base.transaction do
    yield
  end
end

Parameters:

  • action_name (Symbol, nil) (defaults to: nil)

    the name of the callback to add

  • opts (Hash)

    action options

Options Hash (**opts):

  • :only (Symbol, Array<Symbol>)

    restrict the callback to run only for specific actions

  • :except (Symbol, Array<Symbol>)

    restrict the callback to run for all actions except specified

  • :if (Symbol, Proc)

    only run the callback if the condition is true

  • :unless (Symbol, Proc)

    only run the callback if the condition is false



315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/rage/controller/api.rb', line 315

def around_action(action_name = nil, **opts, &block)
  action = prepare_action_params(action_name, **opts, &block)
  action.merge!(around: true, wrapper: define_maybe_yield(action[:name]))

  if @__before_actions && @__before_actions.frozen?
    @__before_actions = @__before_actions.dup
  end

  if @__before_actions.nil?
    @__before_actions = [action]
  elsif (i = @__before_actions.find_index { |a| a[:name] == action_name })
    @__before_actions[i] = action
  else
    @__before_actions << action
  end
end

.before_action(action_name = nil, **opts, &block) ⇒ Object

Note:

The block form doesn't receive an argument and is executed on the controller level as if it was a regular method.

Register a new before_action hook. Calls with the same action_name will overwrite the previous ones.

Examples:

before_action :find_photo, only: :show

def find_photo
  Photo.first
end
before_action :require_user, unless: :logged_in?
before_action :set_locale, if: -> { params[:locale] != "en-US" }
before_action do
  unless logged_in? # would be `controller.send(:logged_in?)` in Rails
    head :unauthorized
  end
end

Parameters:

  • action_name (Symbol, nil) (defaults to: nil)

    the name of the callback to add

  • opts (Hash)

    action options

Options Hash (**opts):

  • :only (Symbol, Array<Symbol>)

    restrict the callback to run only for specific actions

  • :except (Symbol, Array<Symbol>)

    restrict the callback to run for all actions except specified

  • :if (Symbol, Proc)

    only run the callback if the condition is true

  • :unless (Symbol, Proc)

    only run the callback if the condition is false



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/rage/controller/api.rb', line 283

def before_action(action_name = nil, **opts, &block)
  action = prepare_action_params(action_name, **opts, &block)

  if @__before_actions && @__before_actions.frozen?
    @__before_actions = @__before_actions.dup
  end

  if @__before_actions.nil?
    @__before_actions = [action]
  elsif (i = @__before_actions.find_index { |a| a[:name] == action_name })
    @__before_actions[i] = action
  else
    @__before_actions << action
  end
end

.rescue_from(*klasses, with: nil, &block) ⇒ Object

Register a global exception handler. Handlers are inherited and matched from bottom to top.

Examples:

rescue_from User::NotAuthorized, with: :deny_access

def deny_access
  head :forbidden
end
rescue_from User::NotAuthorized do |exception|
  render json: { message: exception.message }, status: :forbidden
end

Parameters:

  • klasses (Class, Array<Class>)

    exception classes to watch on

  • with (Symbol) (defaults to: nil)

    the name of a handler method. Alternatively, you can pass a block.



240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/rage/controller/api.rb', line 240

def rescue_from(*klasses, with: nil, &block)
  unless with
    if block_given?
      with = define_dynamic_method(block)
    else
      raise ArgumentError, "No handler provided. Pass the `with` keyword argument or provide a block."
    end
  end

  if @__rescue_handlers.nil?
    @__rescue_handlers = []
  elsif @__rescue_handlers.frozen?
    @__rescue_handlers = @__rescue_handlers.dup
  end

  @__rescue_handlers.unshift([klasses, with])
end

.skip_before_action(action_name, only: nil, except: nil) ⇒ Object

Prevent a before_action hook from running.

Examples:

skip_before_action :find_photo, only: :create

Parameters:

  • action_name (Symbol)

    the name of the callback to skip

  • only (Symbol, Array<Symbol>) (defaults to: nil)

    restrict the callback to be skipped only for specific actions

  • except (Symbol, Array<Symbol>) (defaults to: nil)

    restrict the callback to be skipped for all actions except specified

Raises:

  • (ArgumentError)


365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
# File 'lib/rage/controller/api.rb', line 365

def skip_before_action(action_name, only: nil, except: nil)
  i = @__before_actions&.find_index { |a| a[:name] == action_name && !a[:around] }
  raise ArgumentError, "The following action was specified to be skipped but couldn't be found: #{self}##{action_name}" unless i

  @__before_actions = @__before_actions.dup if @__before_actions.frozen?

  if only.nil? && except.nil?
    @__before_actions.delete_at(i)
    return
  end

  action = @__before_actions[i].dup
  if only
    action[:except] ? action[:except] |= Array(only) : action[:except] = Array(only)
  end
  if except
    action[:only] = Array(except)
  end

  @__before_actions[i] = action
end

.wrap_parameters(key, include: [], exclude: []) ⇒ Object

Wraps the parameters hash into a nested hash. This will allow clients to submit requests without having to specify any root elements. Params get wrapped only if the Content-Type header is present and the params hash doesn't contain a param with the same name as the wrapper key.

Examples:

wrap_parameters :user, include: %i[name age]
wrap_parameters :user, exclude: %i[address]

Parameters:

  • key (Symbol)

    the wrapper key

  • include (Symbol, Array<Symbol>) (defaults to: [])

    the list of attribute names which parameters wrapper will wrap into a nested hash

  • exclude (Symbol, Array<Symbol>) (defaults to: [])

    the list of attribute names which parameters wrapper will exclude from a nested hash



397
398
399
400
# File 'lib/rage/controller/api.rb', line 397

def wrap_parameters(key, include: [], exclude: [])
  @__wrap_parameters_key = key
  @__wrap_parameters_options = { include:, exclude: }
end

Instance Method Details

#action_nameString

Get the name of the currently executed action.

Returns:

  • (String)

    the name of the currently executed action



683
684
685
# File 'lib/rage/controller/api.rb', line 683

def action_name
  @__params[:action]
end

#append_info_to_payload(payload) ⇒ Object

Define this method to add more information to request logs.

Examples:

def append_info_to_payload(payload)
  payload[:response] = response.body
end

Parameters:

  • payload (Hash)

    the payload to add additional information to



# File 'lib/rage/controller/api.rb', line 693

#authenticate_or_request_with_http_token {|token| ... } ⇒ Object

Authenticate using an HTTP Bearer token, or otherwise render an HTTP header requesting the client to send a Bearer token. For the authentication to be considered successful, the block should return a non-nil value.

Examples:

before_action :authenticate

def authenticate
  authenticate_or_request_with_http_token do |token|
    ApiToken.find_by(token: token)
  end
end

Yields:

  • (token)

    token value extracted from the Authorization header



619
620
621
# File 'lib/rage/controller/api.rb', line 619

def authenticate_or_request_with_http_token
  authenticate_with_http_token { |token| yield(token) } || request_http_token_authentication
end

#authenticate_with_http_token {|token| ... } ⇒ Object

Authenticate using an HTTP Bearer token. Returns the value of the block if a token is found. Returns nil if no token is found.

Examples:

user = authenticate_with_http_token do |token|
  User.find_by(key: token)
end

Yields:

  • (token)

    token value extracted from the Authorization header



584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
# File 'lib/rage/controller/api.rb', line 584

def authenticate_with_http_token
  auth_header = @__env["HTTP_AUTHORIZATION"]

  payload = if auth_header&.start_with?("Bearer")
    auth_header[7..]
  elsif auth_header&.start_with?("Token")
    auth_header[6..]
  end

  return unless payload

  token = if payload.start_with?("token=")
    payload[6..]
  else
    payload
  end

  token.delete_prefix!('"')
  token.delete_suffix!('"')

  yield token
end

#cookiesRage::Cookies

Get the cookie object. See Rage::Cookies.

Returns:



482
483
484
# File 'lib/rage/controller/api.rb', line 482

def cookies
  @cookies ||= Rage::Cookies.new(@__env, @__headers)
end

#head(status) ⇒ Object

Send a response with no body.

Examples:

head :unauthorized
head 429

Parameters:

  • status (Integer, Symbol)

    set a response status



558
559
560
561
562
563
564
565
566
# File 'lib/rage/controller/api.rb', line 558

def head(status)
  @__rendered = true

  @__status = if status.is_a?(Symbol)
    ::Rack::Utils::SYMBOL_TO_STATUS_CODE[status]
  else
    status
  end
end

#headersHash

Set response headers.

Examples:

headers["Content-Type"] = "application/pdf"

Returns:

  • (Hash)


573
574
575
# File 'lib/rage/controller/api.rb', line 573

def headers
  @__headers
end

#paramsHash{Symbol=>String,Array,Hash,Numeric,NilClass,TrueClass,FalseClass}

Get the request data. The keys inside the hash are symbols, so params.keys returns an array of Symbol.
You can also load Strong Parameters to have Rage automatically wrap params in an instance of ActionController::Parameters.
At the same time, if you are not implementing complex filtering rules or working with nested structures, consider using native Hash#fetch and Hash#slice instead.

For multipart file uploads, the uploaded files are represented by an instance of Rage::UploadedFile.

Examples:

With Strong Parameters

# in the Gemfile:
gem "activesupport", require: "active_support/all"
gem "actionpack", require: "action_controller/metal/strong_parameters"

# in the controller:
params.require(:user).permit(:full_name, :dob)

Without Strong Parameters

params.fetch(:user).slice(:full_name, :dob)

Returns:

  • (Hash{Symbol=>String,Array,Hash,Numeric,NilClass,TrueClass,FalseClass})


646
647
648
# File 'lib/rage/controller/api.rb', line 646

def params
  @__params
end

#render(json: nil, plain: nil, sse: nil, status: nil) ⇒ Object

Note:

render doesn't terminate execution of the action, so if you want to exit an action after rendering, you need to do something like render(...) and return.

Send a response to the client. Keywords corresponding to custom renderers (see Rage::Configuration#renderer) will be delegated automatically.

Examples:

Render a JSON object

render json: { hello: "world" }

Set a response status

render status: :ok

Render an SSE stream

render sse: "hello world".each_char

Render a one-off SSE update

render sse: { message: "hello world" }

Write to an SSE connection manually

render sse: ->(connection) do
  connection.write("data: Hello, World!\n\n")
  connection.close
end

Parameters:

  • json (String, #to_json) (defaults to: nil)

    send a json response to the client; objects will be serialized automatically

  • plain (#to_s) (defaults to: nil)

    send a text response to the client

  • sse (#each, Proc, #to_json) (defaults to: nil)

    send an SSE response to the client

  • status (Integer, Symbol) (defaults to: nil)

    set a response status



512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
# File 'lib/rage/controller/api.rb', line 512

def render(json: nil, plain: nil, sse: nil, status: nil)
  raise "Render was called multiple times in this action." if @__rendered
  @__rendered = true

  if json || plain
    @__body << if json
      json.is_a?(String) ? json : json.to_json
    else
      ct = @__headers["content-type"]
      @__headers["content-type"] = "text/plain; charset=utf-8" if ct.nil? || ct == DEFAULT_CONTENT_TYPE
      plain.to_s
    end

    @__status = 200
  end

  if status
    @__status = if status.is_a?(Symbol)
      ::Rack::Utils::SYMBOL_TO_STATUS_CODE[status]
    else
      status
    end
  end

  if sse
    raise ArgumentError, "Cannot render both a standard body and an SSE stream." unless @__body.empty?

    if status
      return if @__status == 204
      raise ArgumentError, "SSE responses only support 200 and 204 statuses." if @__status != 200
    end

    @__env["rack.upgrade?"] = :sse
    @__env["rack.upgrade"] = Rage::SSE::Application.new(sse)
    @__status = 200
    @__headers["content-type"] = "text/event-stream; charset=utf-8"
  end
end

#requestRage::Request

Get the request object. See Rage::Request.

Returns:



470
471
472
# File 'lib/rage/controller/api.rb', line 470

def request
  @request ||= Rage::Request.new(@__env, controller: self)
end

#request_http_token_authenticationObject

Render an HTTP header requesting the client to send a Bearer token for authentication.



624
625
626
627
# File 'lib/rage/controller/api.rb', line 624

def request_http_token_authentication
  headers["www-authenticate"] = "Token"
  render plain: "HTTP Token: Access denied.", status: 401
end

#reset_sessionObject

Reset the entire session. See Rage::Session.



702
703
704
# File 'lib/rage/controller/api.rb', line 702

def reset_session
  session.clear
end

#responseRage::Response

Get the response object. See Rage::Response.

Returns:



476
477
478
# File 'lib/rage/controller/api.rb', line 476

def response
  @response ||= Rage::Response.new(self)
end

#sessionRage::Session

Get the session object. See Rage::Session.

Returns:



488
489
490
# File 'lib/rage/controller/api.rb', line 488

def session
  @session ||= Rage::Session.new(cookies)
end

#stale?(etag: nil, last_modified: nil) ⇒ Boolean

Note:

stale? will set ETag and Last-Modified response headers made of passed arguments in the method. Value for ETag will be additionally hashified using SHA1 algorithm, whereas value for Last-Modified will be converted to the string which represents time as RFC 1123 date of HTTP-date defined by RFC 2616.

Note:

stale? will set the response status to 304 if the request is fresh. This side effect will cause a double render error, if render gets called after this method. Make sure to implement a proper conditional in your action to prevent this from happening:

if stale?(etag: "123")
  render json: { hello: "world" }
end

Checks if the request is stale to decide if the action has to be rendered or the cached version is still valid. Use this method to implement conditional GET.

Examples:

stale?(etag: "123", last_modified: Time.utc(2023, 12, 15))
stale?(last_modified: Time.utc(2023, 12, 15))
stale?(etag: "123")

Parameters:

  • etag (String) (defaults to: nil)

    The etag of the requested resource.

  • last_modified (Time) (defaults to: nil)

    The last modified time of the requested resource.

Returns:

  • (Boolean)

    True if the response is stale, false otherwise.



671
672
673
674
675
676
677
678
679
# File 'lib/rage/controller/api.rb', line 671

def stale?(etag: nil, last_modified: nil)
  response.etag = etag
  response.last_modified = last_modified

  still_fresh = request.fresh?(etag: response.etag, last_modified: last_modified)

  head :not_modified if still_fresh
  !still_fresh
end