# encoding: utf-8 module Hexx # @abstract # The base class for service objects. # # @example # require "hexx" # class GetItem < Hexx::Service # allow_params :name # def run # publish :found, item = Item.where(name: name).first # end # end # # service = GetItem.new name: name # service.subscribe listener, prefix: :on # service.run # # => This will call the listener's method #on_found(item). class Service include Wisper::Publisher include ActiveModel::Validations include Parameters # @!scope class # @!method new(options = {}) # Constructs the service object. # # @example # service = Hexx::Service.new name: name # # @param [Hash] options The options to be assigned to the {#params}. # @return [Hexx::Service] The service object. # @!scope class # @!visibility private # @!method allow_params(*params) # Whitelists {#params} and declares a parameter for corresponding keys. # # @example (see Hexx::Service#params) # # @example Defines corresponding readonly attributes. # class GetItem < Hexx::Service # allow_params :name # end # # service = GetItem.new name: "Олег", family: "Рюрикович" # service.name # => "Олег" # # @param [Array] params The list of allowed keys. # @!scope class # @!method validates(attribute, options) # Adds a standard validation for the attribute. # @note The method is defined in the {ActiveModel::Validations} module. # @!scope class # @!method validate(method, options) # Adds a custom validation (calls given method). # @note The method is defined in the {ActiveModel::Validations} module. # @!attribute params [r] The list of service object parameters. # The attribute is assigned via the {.new} method options. # The keys should be explicitly declared by the {.allow_params} helper. # # @example Only whitelisted params are being assigned. # class GetItem < Hexx::Service # allow_params :name # end # # service = GetItem.new name: "Олег", family: "Рюрикович" # service.params # => { "name" => "Олег" } # @!method subscribe(listener, options = {}) # Subscribes the listener to service object's notifications. # The :prefix sets the prefix to be added to a notification name # to provide a corresponding listener method, that should be called by # the publisher. # # @example (see Hexx::Service) # @param [Object] listener The object that should receive notifications from # the service object. # @param [Hash] options The list of the subscription options. # @option options [Symbol] :prefix The prefix for the listener's callbacks. # @abstract # Runs the service object. # @note The method must be reloaded by a specific service class, # inherited from Hexx::Service. # @raise [NotImplementedError] if a child class hasn't redefined the method. def run fail NotImplementedError.new "#{ self.class.name }#run not implemented." end # Makes private methods with given prefix public. # # @example Opens private methods. # def GetItem < Hexx::Service # private # def on_success # publish :success # end # end # # service = GetItem.new # service.respond_to? :on_success # # => false # # service_with_callbacks = service.with_callbacks # service_with_callbacks.respond_to? :on_success # # => true # # @return [Hexx::Service::WithCallbacks] # The decorator that allows access to the service's private methods. def with_callbacks(prefix: nil) WithCallbacks.new(self, prefix: prefix) end private # The helper runs another service object and subscribes +self+ for the # service object's notifications. # # @example # class AddItem < Hexx::Service # allow_params :id # # # Runs a service for finding an item. # # Service notifications to be received with a prefix :on_item # def find_item # run_service GetItem, :on_item, id: params["id"] # end # # private # # attr_reader :item # # # Receives GetItem's :found notification # def on_item_found(item) # @item = item # publish :found, item # end # # # Receives GetItem's :not_found notification # def on_item_not_found(*) # # ... do some stuff here # end # end # # @param [Class] service_class The service class to instantiate and run # a service object. # @param [Symbol] prefix The prefix for callbacks to receive the service # object's notifications. # @param [Hash] options ({}) The options for the service object initializer. # @raise [TypeError] when the service_class is not a Hexx::Service. def run_service(service_class, prefix, options = {}) fail TypeError unless service_class.ancestors.include? Hexx::Service service = service_class.new(options) service.subscribe with_callbacks, prefix: prefix service.run end # @!method escape # # The method rescues block runtime errors and publishes the :error # notification. # # @example # class GetItem < Hexx::Service # def run # escape do # errors.add :base, :error # fail Invalid.new self # end # end # end # # service = GetItem.new # service.subscribe listener # service.run # # => the listener will be sent the error(messages). # # @yield the block. # @return the value returned by the block. def escape yield rescue Service::Invalid => err publish :error, Message.from(err.service) rescue => err publish :error, [Message.new(type: "error", text: err.message)] end # Translates given key in current service's scope. # # @note The method uses I18n.t library method. # # @example Returns a translation if the first argument is a symbol. # class Test < Hexx::Service # end # service = Test.new # service.t :name # # => "translation not found: en.activemodel.messages.models.test.name" # # @example Returns the string argument. # service = Hexx::Service.new # service.t "name" # # => "name" # # @param [Symbol, String] text The text to be translated. # @param [Hash] options The translation options. # @return [String] The translation. def t(text, options = {}) return text unless text.is_a? Symbol scope = %w(activemodel messages models) << self.class.name.underscore I18n.t text, options.merge(scope: scope) end # The array of service messages, added by the {#add_message} helper. # # @example # class Test < Hexx::Service # def run # add_message "info", :ok # messages # end # end # result = Test.new.run.first # result.type # # => "info" # result.text # # => "translation not found: en.activemodel.messages.models.test.ok" # # @return [Array] The array of message objects. def messages @messages ||= [] end # Adds the translated message to the {#messages} array. # @example (see Service#messages) # @param [String] type The type of the message ("error", "info", "success") # @param [String, Symbol] text The text of the message. The symbol will # be translated using the {#t} method. # @param [Hash] options The translation options. # @return The updated {#messages} array. def add_message(type, text, options = {}) messages << Message.new(type: type, text: t(text, options)) end # Runs validations and fails if the service is invalid. # # @example (see Hexx::Service::Invalid) # # @example Safe usage (recommended) with the {#escape} wrapper. # service GetItem < Hexx::Service # allow_params :uuid # validates :uuid, presence: true # def run # escape { validate! } # end # end # # service = GetItem.new # service.run # => publishes :error notification # # @raise [Hexx::Service::Invalid] when the service object isn't valid. def validate! fail Invalid.new(self) unless valid? end end end