# encoding: utf-8 require_relative "dependable" 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 # @!method subscribe(listener, options = {}) # @!visibility public # Subscribes the listener to service object's notifications. # # @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. # It defines the prefix to be added to a notification name # to provide a corresponding listener method, that should be called by # the publisher. include Helpers::Parameters # @api hide private :params private_class_method :allow_params # @!scope class # @!method allow_params(*names) # @!visibility private # Sets a list of allowed parameters for the class constructor and # defines the corresponding instance attributes. # # @example # class MyService < Hexx::Service # allows_params :name # end # # service = MyService.new name: "name", code: "code" # service.send :name # => "name" # service.send :params # => { "name" => "name" } # # @param [Symbol, String, Array] names # The list of allowed parameters. # @!attribute [r] params # @!visibility private # The list of service object parameters. # # The attribute is assigned via the {.new} method options. # On initialization the parameters (keys) are stringified and whitelisted. # # Allowed parameters should be explicitly declared via the {.allow_params} # private class helper. # # @example # class GetItem < Hexx::Service # allow_params :name # end # # service = GetItem.new name: "Олег", family: "Рюрикович" # service.params # => { "name" => "Олег" } # # @return [Hash] the service object parameters. include Helpers::Messages # @api hide public :messages # @api hide private :messages=, :add_message, :t # @!attribute [r] messages # @!visibility public # The array of service messages (instances of {Hexx::Message}) # with +text+ and +type+ attributes. # # @note The attribute setter is private! # # @example The messages can be added by the {#add_message} private helper # class EditItem < Hexx::Service # def run # # ... # else # add_message "success", "changed" # publish :changed, messages # end # end # # service = Test.new # service.run # service.messages # # => [#] # # @return [Array] The array of messages. # @!scope instance # @!method add_message(type, text) # @!visibility private # Adds the translated message to the {#messages} array. # # @example # class Hello < Hexx::Service # def run # add_message "success", "Hello!" # publish :hello, messages # end # end # # hello = Hello.new # hello.subscribe listener # hello.run # # # The listener.hello [#] # # will be called. # # @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. # @!scope instance # @!method t(text, options = {}) # @!visibility private # 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 PrintHello < Hexx::Service # def run # puts t(:hello) # end # end # # object = PrintHello.new # object.run # # => $ translation not found: en.activemodel.messages.models.test.name # # @example Returns the string argument. # class PrintHello < Hexx::Service # def run # puts t("hello") # end # end # # object = PrintHello.new # object.run # # => $ name # # @param [Symbol, String] text The text to be translated. # @param [Hash] options ({}) The translation options. # @return [String] The translation. include Helpers::Validations # @api hide private :validate! # @!scope class # @!method validates(attribute, options) # @!visibility private # Adds a standard validation for the attribute. # # @param [Symbol, String] attribute The name of the attribute to validate. # @param [Hash] options The list of validation options. # @see # http://apidock.com/rails/ActiveModel/Validations/ClassMethods/validates # ActiveModel validations APIdocs # @!scope class # @!method validate(method, options) # @!visibility private # Adds a custom validation (calls given method). # # @param [Symbol, String] method The name of the validation method. # @param [Hash] options The list of validation options. # @see # http://apidock.com/rails/ActiveModel/Validations/ClassMethods/validate # ActiveModel validations APIdocs # @!scope instance # @!method validate! # @!visibility private # Runs validations and raises Hexx::ServiceInvalid # when a validation fails. # # @example (see Hexx::ServiceInvalid) # # @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::ServiceInvalid] when the service object isn't valid. include Helpers::Exceptions # @api hide private :escape, :on_error private_class_method :raises # @!scope class # @!method raises(exceptions) # @!visibility private # Declares a list of specific +StandardError+ exceptions. # # @example # class Service < Hexx::Service # raises :NotFound, :NotChanged # # def run # run! # rescue NotFound # publish :not_found # rescue NotChanged # publish :not_changed # rescue => err # # works out any other (unspecified exceptions) # publish :error # else # # works out the main scenario # publish :success # end # end # # Service.const_defined? :NotFound # => true # Service.const_defined? :NotChanged # => true # # exception = Service::NotFound.new # exception.is_a? StandardError # => true # # @param [String, Symbol, Array] exceptions The list of # specific +StandardError+ exceptions. # @!scope instance # @!method escape # @!visibility private # The method re-raises +StandardError+ exceptions as a # Hexx::ServiceInvalid. # # * rescues from a +StandardError+ exceptions # * adds error message to the service # * re-raises the Hexx::ServiceInvalid 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::ServiceInvalid] if the block raised the +StandardError+. # @return the value returned by the block. # @!scope instance # @!method on_error(messages) # @!visibility private # Raises the {Hexx::ServiceInvalid} exception, populated with given # messages. # # @example # class EditItem < Hexx::Service # # allow_params :id, :name # # ... # # def find_item # run_service GetItem, :on_item, id: id # end # # def on_item_not_found(*, messages) # on_error(messages) # Raises Hexx::ServiceInvalid # end # end # # @param [Array] messages The list of error # messages to be added to the exception. # @raise [Hexx::ServiceInvalid] the exception. # @!scope class # @!method new(params = {}) # Constructs a service object with given parameters. # # @example (see Hexx::Service) # @param [Hash] params ({}) The parameters of the service object to be # assigned to the {#params} attribute. # @return [Hexx::Service] The service object. # @abstract # Runs the service object. def run 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 [Hexx::Service] service_class The class for the service object to # run. # @param [Symbol] prefix The prefix for callbacks to receive the service # object's notifications. # @param [Hash] options ({}) The options for the service object initializer. def run_service(service_class, prefix, options = {}) service = service_class.new(options) service.subscribe with_callbacks, prefix: prefix service.run end # @api hide # 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 end end