class HaveAPI::Action

Attributes

action_name[W]
authorization[R]
examples[R]
resource[RW]
current_user[R]
errors[R]
flags[RW]
message[R]
request[R]
version[R]

Public Class Methods

action_name() click to toggle source
# File lib/haveapi/action.rb, line 192
def action_name
  (@action_name ? @action_name.to_s : to_s).demodulize
end
add_pre_authorize_blocks(authorization, context) click to toggle source
# File lib/haveapi/action.rb, line 286
def add_pre_authorize_blocks(authorization, context)
  ret = Action.call_hooks(
    :pre_authorize,
    args: [context],
    initial: { blocks: [] }
  )

  ret[:blocks].reverse_each do |block|
    authorization.prepend_block(block)
  end
end
authorize(&) click to toggle source
# File lib/haveapi/action.rb, line 181
def authorize(&)
  @authorization = Authorization.new(&)
end
build_route(prefix) click to toggle source
# File lib/haveapi/action.rb, line 198
def build_route(prefix)
  route = @route || action_name.underscore
  if @route
    @route
  elsif action_name
    action_name.to_s.demodulize.underscore
  else
    to_s.demodulize.underscore
  end

  if !route.is_a?(String) && route.respond_to?(:call)
    route = route.call(resource)
  end

  prefix + format(route, resource: resource.resource_name.underscore)
end
delayed_inherited(subclass) click to toggle source
# File lib/haveapi/action.rb, line 57
def delayed_inherited(subclass)
  resource = subclass.resource || Kernel.const_get(subclass.to_s.deconstantize)

  inherit_attrs(subclass)
  inherit_attrs_from_resource(subclass, resource, [:auth])

  i = @input.clone
  i.action = subclass

  o = @output.clone
  o.action = subclass

  m = {}

  @meta.each do |k, v|
    m[k] = v && v.clone
    next unless v

    m[k].action = subclass
  end

  subclass.instance_variable_set(:@input, i)
  subclass.instance_variable_set(:@output, o)
  subclass.instance_variable_set(:@meta, m)

  begin
    subclass.instance_variable_set(:@resource, resource)
    subclass.instance_variable_set(:@model, resource.model)
    resource.action_defined(subclass)
  rescue NoMethodError
    nil
  end
end
describe(context) click to toggle source
# File lib/haveapi/action.rb, line 215
def describe(context)
  authorization = (@authorization && @authorization.clone) || Authorization.new
  add_pre_authorize_blocks(authorization, context)

  if (context.endpoint || context.current_user) \
      && !authorization.authorized?(context.current_user, context.path_params_from_args)
    return false
  end

  route_method = context.action.http_method.to_s.upcase
  context.authorization = authorization

  if context.endpoint
    context.action_instance = context.action.from_context(context)

    ret = catch(:return) do
      context.action_prepare = context.action_instance.prepare
    end

    return false if ret == false
  end

  {
    auth: @auth,
    description: @desc,
    aliases: @aliases,
    blocking: @blocking ? true : false,
    input: @input ? @input.describe(context) : { parameters: {} },
    output: @output ? @output.describe(context) : { parameters: {} },
    meta: @meta ? @meta.merge(@meta) { |_, v| v && v.describe(context) } : nil,
    examples: @examples ? @examples.describe(context) : [],
    scope: context.action_scope,
    path: context.resolved_path,
    method: route_method,
    help: "#{context.path}?method=#{route_method}"
  }
end
example(title = '', &) click to toggle source
# File lib/haveapi/action.rb, line 185
def example(title = '', &)
  @examples ||= ExampleList.new
  e = Example.new(title)
  e.instance_eval(&)
  @examples << e
end
from_context(c) click to toggle source
# File lib/haveapi/action.rb, line 266
def from_context(c)
  ret = new(nil, c.version, c.params, nil, c)
  ret.instance_exec do
    @safe_params = @params.dup
    @authorization = c.authorization
    @current_user = c.current_user
  end

  ret
end
inherit_attrs_from_resource(action, r, attrs) click to toggle source

Inherit attributes from resource action is defined in.

# File lib/haveapi/action.rb, line 254
def inherit_attrs_from_resource(action, r, attrs)
  begin
    return unless r.obj_type == :resource
  rescue NoMethodError
    return
  end

  attrs.each do |attr|
    action.method(attr).call(r.method(attr).call)
  end
end
inherited(subclass) click to toggle source
Calls superclass method
# File lib/haveapi/action.rb, line 46
def inherited(subclass)
  # puts "Action.inherited called #{subclass} from #{to_s}"
  super
  subclass.instance_variable_set(:@obj_type, obj_type)

  return unless subclass.name

  # not an anonymouse class
  delayed_inherited(subclass)
end
initialize() click to toggle source
# File lib/haveapi/action.rb, line 91
def initialize # rubocop:disable Lint/MissingSuper
  return if @initialized

  check_build("#{self}.input") do
    input.exec
    model_adapter(input.layout).load_validators(model, input) if model
  end

  check_build("#{self}.output") do
    output.exec
  end

  model_adapter(input.layout).used_by(:input, self)
  model_adapter(output.layout).used_by(:output, self)

  if blocking
    meta(:global) do
      output do
        integer :action_state_id,
                label: 'Action state ID',
                desc: 'ID of ActionState object for state querying. When null, the action ' \
                      'is not blocking for the current invocation.'
      end
    end
  end

  if @meta
    @meta.each_value do |m|
      next unless m

      check_build("#{self}.meta.input") do
        m.input && m.input.exec
      end

      check_build("#{self}.meta.output") do
        m.output && m.output.exec
      end
    end
  end

  @initialized = true
end
input(layout = nil, namespace: nil, &block) click to toggle source
# File lib/haveapi/action.rb, line 148
def input(layout = nil, namespace: nil, &block)
  if block
    @input ||= Params.new(:input, self)
    @input.layout = layout
    @input.namespace = namespace
    @input.add_block(block)
  else
    @input
  end
end
meta(type = :object, &block) click to toggle source
# File lib/haveapi/action.rb, line 170
def meta(type = :object, &block)
  if block
    @meta ||= { object: nil, global: nil }
    @meta[type] ||= Metadata::ActionMetadata.new
    @meta[type].action = self
    @meta[type].instance_exec(&block)
  else
    @meta[type]
  end
end
model_adapter(layout) click to toggle source
# File lib/haveapi/action.rb, line 144
def model_adapter(layout)
  ModelAdapter.for(layout, resource.model)
end
new(request, version, params, body, context) click to toggle source
Calls superclass method
# File lib/haveapi/action.rb, line 299
def initialize(request, version, params, body, context)
  super()
  @request = request
  @version = version
  @params = params
  @params.update(body) if body
  @context = context
  @context.action = self.class
  @context.action_instance = self
  @metadata = {}
  @reply_meta = { object: {}, global: {} }
  @flags = {}

  class_auth = self.class.authorization

  @authorization = if class_auth
                     class_auth.clone
                   else
                     Authorization.new {}
                   end

  self.class.add_pre_authorize_blocks(@authorization, @context)
end
output(layout = nil, namespace: nil, &block) click to toggle source
# File lib/haveapi/action.rb, line 159
def output(layout = nil, namespace: nil, &block)
  if block
    @output ||= Params.new(:output, self)
    @output.layout = layout
    @output.namespace = namespace
    @output.add_block(block)
  else
    @output
  end
end
resolve_path_params(object) click to toggle source
# File lib/haveapi/action.rb, line 277
def resolve_path_params(object)
  if resolve
    resolve.call(object)

  else
    object.respond_to?(:id) ? object.id : nil
  end
end
validate_build() click to toggle source
# File lib/haveapi/action.rb, line 134
def validate_build
  check_build("#{self}.input") do
    input.validate_build
  end

  check_build("#{self}.output") do
    output.validate_build
  end
end

Public Instance Methods

authorized?(user) click to toggle source
# File lib/haveapi/action.rb, line 329
def authorized?(user)
  @current_user = user
  @authorization.authorized?(user, extract_path_params)
end
exec() click to toggle source

This method must be reimplemented in every action. It must not be invoked directly, only via safe_exec, which restricts output.

# File lib/haveapi/action.rb, line 363
def exec
  ['not implemented']
end
input() click to toggle source
# File lib/haveapi/action.rb, line 338
def input
  @safe_params[self.class.input.namespace] if self.class.input
end
meta() click to toggle source
# File lib/haveapi/action.rb, line 342
def meta
  @metadata
end
params() click to toggle source
# File lib/haveapi/action.rb, line 334
def params
  @safe_params
end
pre_exec() click to toggle source
# File lib/haveapi/action.rb, line 359
def pre_exec; end
prepare() click to toggle source

Prepare object, set instance variables from URL parameters. This method should return queried object. If the method is not implemented or returns nil, action description will not contain link to an associated resource. – FIXME: is this correct behaviour? ++

# File lib/haveapi/action.rb, line 357
def prepare; end
safe_exec() click to toggle source

Calls exec while catching all exceptions and restricting output only to what user can see. Return array +[status, data|error, errors]+

# File lib/haveapi/action.rb, line 370
def safe_exec
  exec_ret = catch(:return) do
    validate!
    prepare
    pre_exec
    exec
  rescue Exception => e # rubocop:disable Lint/RescueException
    tmp = call_class_hooks_as_for(Action, :exec_exception, args: [@context, e])

    if tmp.empty?
      p e.message
      puts e.backtrace
      error!('Server error occurred')
    end

    unless tmp[:status]
      error!(tmp[:message], {}, http_status: tmp[:http_status] || 500)
    end
  end

  begin
    output_ret = safe_output(exec_ret)
  rescue Exception => e # rubocop:disable Lint/RescueException
    tmp = call_class_hooks_as_for(Action, :exec_exception, args: [@context, e])

    p e.message
    puts e.backtrace

    return [
      tmp[:status] || false,
      tmp[:message] || 'Server error occurred',
      {},
      tmp[:http_status] || 500
    ]
  end

  output_ret
end
safe_output(ret) click to toggle source
# File lib/haveapi/action.rb, line 413
def safe_output(ret)
  if ret
    output = self.class.output

    if output
      safe_ret = nil
      adapter = self.class.model_adapter(output.layout)
      out_params = self.class.output.params

      case output.layout
      when :object
        out = adapter.output(@context, ret)
        safe_ret = @authorization.filter_output(
          out_params,
          out,
          true
        )
        @reply_meta[:global].update(out.meta)

      when :object_list
        safe_ret = []

        ret.each do |obj|
          out = adapter.output(@context, obj)

          safe_ret << @authorization.filter_output(
            out_params,
            out,
            true
          )
          safe_ret.last.update({ Metadata.namespace => out.meta }) unless meta[:no]
        end

      when :hash
        safe_ret = @authorization.filter_output(
          out_params,
          adapter.output(@context, ret),
          true
        )

      when :hash_list
        safe_ret = ret
        safe_ret.map! do |hash|
          @authorization.filter_output(
            out_params,
            adapter.output(@context, hash),
            true
          )
        end

      else
        safe_ret = ret
      end

      if self.class.blocking
        @reply_meta[:global][:action_state_id] = state_id
      end

      ns = { output.namespace => safe_ret }
      ns[Metadata.namespace] = @reply_meta[:global] unless meta[:no]

      [true, ns]

    else
      [true, {}]
    end

  else
    [false, @message, @errors, @http_status]
  end
end
set_meta(hash) click to toggle source
# File lib/haveapi/action.rb, line 346
def set_meta(hash)
  @reply_meta[:global].update(hash)
end
v?(v) click to toggle source
# File lib/haveapi/action.rb, line 409
def v?(v)
  @version == v
end
validate!() click to toggle source
# File lib/haveapi/action.rb, line 323
def validate!
  @params = validate
rescue ValidationError => e
  error!(e.message, e.to_hash)
end

Protected Instance Methods

error!(msg, errs = {}, opts = {}) click to toggle source

@param msg [String] error message sent to the client @param errs [Hash<Array>] parameter errors sent to the client @param opts [Hash] options @option opts [Integer] http_status HTTP status code sent to the client

# File lib/haveapi/action.rb, line 567
def error!(msg, errs = {}, opts = {})
  @message = msg
  @errors = errs
  @http_status = opts[:http_status]
  throw(:return, false)
end
ok!(ret = {}, opts = {}) click to toggle source

@param ret [Hash] response @param opts [Hash] options @option opts [Integer] http_status HTTP status code sent to the client

# File lib/haveapi/action.rb, line 558
def ok!(ret = {}, opts = {})
  @http_status = opts[:http_status]
  throw(:return, ret)
end
to_db_names(hash, src = :input) click to toggle source

Convert parameter names to corresponding DB names. By default, input parameters are used for the translation.

# File lib/haveapi/action.rb, line 505
def to_db_names(hash, src = :input)
  return {} unless hash

  params = self.class.method(src).call.params
  ret = {}

  hash.each do |k, v|
    k = k.to_sym
    hit = false

    params.each do |p|
      next unless k == p.name

      ret[p.db_name] = v
      hit = true
      break
    end

    ret[k] = v unless hit
  end

  ret
end
to_param_names(hash, src = :output) click to toggle source

Convert DB names to corresponding parameter names. By default, output parameters are used for the translation.

# File lib/haveapi/action.rb, line 531
def to_param_names(hash, src = :output)
  return {} unless hash

  params = self.class.method(src).call.params
  ret = {}

  hash.each do |k, v|
    k = k.to_sym
    hit = false

    params.each do |p|
      next unless k == p.db_name

      ret[p.name] = v
      hit = true
      break
    end

    ret[k] = v unless hit
  end

  ret
end
with_restricted(**kwargs) click to toggle source
# File lib/haveapi/action.rb, line 495
def with_restricted(**kwargs)
  if kwargs.empty?
    @authorization.restrictions
  else
    kwargs.update(@authorization.restrictions)
  end
end

Private Instance Methods

extract_path_params() click to toggle source

@return <Hash<Symbol, String>> path parameters and their values

# File lib/haveapi/action.rb, line 637
def extract_path_params
  ret = {}

  @context.path.scan(/\{([a-zA-Z\-_]+)\}/) do |match|
    path_param = match.first
    ret[path_param] = @params[path_param]
  end

  ret
end
validate() click to toggle source
# File lib/haveapi/action.rb, line 576
def validate
  # Validate standard input
  @safe_params = @params.dup
  input = self.class.input

  if input
    # First check layout
    input.check_layout(@safe_params)

    # Then filter allowed params
    case input.layout
    when :object_list, :hash_list
      @safe_params[input.namespace].map! do |obj|
        @authorization.filter_input(
          self.class.input.params,
          self.class.model_adapter(self.class.input.layout).input(obj)
        )
      end

    else
      @safe_params[input.namespace] = @authorization.filter_input(
        self.class.input.params,
        self.class.model_adapter(self.class.input.layout).input(@safe_params[input.namespace])
      )
    end

    # Now check required params, convert types and set defaults
    input.validate(@safe_params)
  end

  # Validate metadata input
  auth = Authorization.new { allow }
  @metadata = {}

  return if input && %i[object_list hash_list].include?(input.layout)

  %i[object global].each do |v|
    meta = self.class.meta(v)
    next unless meta

    raw_meta = nil

    [Metadata.namespace, Metadata.namespace.to_s].each do |ns|
      params = v == :object ? (@params[input.namespace] && @params[input.namespace][ns]) : @params[ns]
      next unless params

      raw_meta = auth.filter_input(
        meta.input.params,
        self.class.model_adapter(meta.input.layout).input(params)
      )

      break if raw_meta
    end

    next unless raw_meta

    @metadata.update(meta.input.validate(raw_meta))
  end
end