# 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. def initialize(options = {}) super(options) @messages = [] end # @!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 # @!attribute [r] messages # The array of service messages (instances of {Hexx::Service::Message}) # with +text+ and +type+ attributes. # # @example # class Test < Hexx::Service # def run # add_message "info", :ok # end # end # # service = Test.new # service.run # adds message # service.messages # # => [#] # # @return [Array] The array of messages. attr_reader :messages 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 +StandardError+ exceptions # * adds error message to the service # * re-raises the Service::Invalid exception # # @example # class GetItem < Hexx::Service # def run # escape { do_something_unsafe } # rescue => err # publish :error, err.messages # end # publish :success # end # end # # @yield the block. # @raise [Hexx::Service::Invalid] if the block raised the +StandardError+. # @return the value returned by the block. def escape yield rescue Invalid => err raise err rescue => err errors.add :base, err.message raise Invalid.new(self) 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 attr_writer :messages # 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