# frozen_string_literal: true require_relative 'result' module Upgrow # Actions represent the entry points to the app's core logic. These objects # coordinate workflows in order to get operations and activities done. # Ultimately, Actions are the public interface of the app's business layers. # # Rails controllers talk to the app's internals by sending messages to # specific Actions, optionally with the required inputs. Actions have a # one-to-one relationship with incoming requests: they are paired # symmetrically with end-user intents and demands. This is quite a special # requirement from this layer: any given HTTP request handled by the app # should be handled by a single Action. # # The fact that each Action represents a meaningful and complete # request-response cycle forces modularization for the app's business logic, # exposing immediately complex relationships between objects at the same time # that frees up the app from scenarios such as interdependent requests. In # other words, Actions do not have knowledge or coupling between other Actions # whatsoever. # # Actions respond to a single public method perform. Each Action defines its # own set of required arguments for perform, as well what can be expected as # the result of that method. class Action # Module to be prepended to subclasses of Action. This allows Action to # decorate methods implemented by subclasses so they can have additional # behaviour. module Decorator # Calls the original `perform` method of the Action object and returns its # Result. In case the Action throws a `:failure`, catches and returns its # value, which is supposed to be a failed Result generated by the Action. # # @return [Result] the Action Result. def perform(...) catch(:failure) do super result end end end class << self attr_writer :exposures # Each Action class has its own list of exposed instance variables to be # included in the returned Result when the Action is called. # # @return [Array] the list of exposed instance variables. def exposures @exposures ||= [] end # Sets the given instance variable names as exposed in the Result. # # @param names [Array] the list of instance variable names to be # exposed as part of the Result. def expose(*names) @exposures += names end private def inherited(subclass) super subclass.exposures = exposures.dup subclass.prepend(Decorator) end end # Throws a Result populated with the given errors. # # @param errors [Array, ActiveModel::Errors] the errors object to # be set as the Result errors. If an ActiveModel::Errors object is # provided, it will be converted to an Array of Errors. # # @return [:failure, Result] the Result instance populated with the given # errors. def failure(*errors) errors = errors.first if errors.first.respond_to?(:each) if errors.respond_to?(:full_message) errors = errors.map do |error| Error.new( attribute: error.attribute, code: error.type, message: error.full_message ) end end throw(:failure, result(errors: errors)) end private def result(members = {}) result_class = Result.new(*self.class.exposures) values = self.class.exposures.to_h do |name| [name, instance_variable_get("@#{name}")] end result_class.new(**values.merge(members)) end end end