class HaveAPI::Action
Attributes
Public Class Methods
# File lib/haveapi/action.rb, line 192 def action_name (@action_name ? @action_name.to_s : to_s).demodulize end
# 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
# 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
# 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
# File lib/haveapi/action.rb, line 185 def example(title = '', &) @examples ||= ExampleList.new e = Example.new(title) e.instance_eval(&) @examples << e end
# 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 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
# 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
# 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
# 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
# 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
# File lib/haveapi/action.rb, line 144 def model_adapter(layout) ModelAdapter.for(layout, resource.model) end
# 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
# 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
# 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
# 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
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
# File lib/haveapi/action.rb, line 338 def input @safe_params[self.class.input.namespace] if self.class.input end
# File lib/haveapi/action.rb, line 342 def meta @metadata end
# File lib/haveapi/action.rb, line 334 def params @safe_params end
# File lib/haveapi/action.rb, line 359 def pre_exec; end
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
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
# 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
# File lib/haveapi/action.rb, line 346 def set_meta(hash) @reply_meta[:global].update(hash) end
# File lib/haveapi/action.rb, line 409 def v?(v) @version == v end
# File lib/haveapi/action.rb, line 323 def validate! @params = validate rescue ValidationError => e error!(e.message, e.to_hash) end
Protected Instance Methods
@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
@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
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
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
# 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
@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
# 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