require "active_model" require "wisper" module Hexx # = About # # Use cases are a core part of a domain. They implement case-specific business # rules (unlike Entities) and named as an imperative (_Add_Doc_, _Get_Doc_). # # Typical use case provides 5 methods: # # +new+:: class method that initializes use case instance. # +subscribe+:: subscribes listeners for the use case notifications. # +run+:: implements the use case. # +run!+:: raises exceptions in case of errors. # +errors+:: collects use case errors. # # The +run+ method returns a corresponding Value object, and notifies # subscribers about the results (following The Observer Pattern). # # = Usage # # Inherit a use case from the Hexx::UseCase class: # # # app/my_domain/use_cases/do_something.rb # require "hexx" # # module MyDomain # class DoSomething < Hexx::UseCase # end # end # # Then add a run! instance method to the Use Case. # # class DoSomething < Hexx::UseCase # # def run! # validate! # # do something # end # end # # The +run+ (without a bang) method is defined by default (see below). If # you need catching some exceptions specifically, do it in your run! # method. # # Unless the run! method defined, calling the +run+ raises # the NotImplementedError. # # == Allow params # # Use case constructor takes one argument with a parameters hash. # # use_case = DoSomething.new id: 1, name: "name" # # This sets the private argument +params+ to be blank hash. # # use_case.send :params # => {} # # For options to be assigned to +params+, their keys should be whitelisted: # # class DoSomething < Hexx::UseCase # # allow_params :id, :name # end # # This will allow assigning values. Note that all the keys are stringified: # # use_case = DoSomething.new id: 1, name: "name", wrong_key: :value # use_case.send :params # => { "id" => 1, "name" => "name" } # # == Validations # # You can use ActiveRecord validations. # # Be careful! Both the valid?, and invalid? are private. # It is expected validations to be used implicitly in a course of use case # running. # # To do this a private method validate! is available. It raises the # Hexx::UseCaseInvalid exception in case of validation fails. # # Note the validate! private method call from the run! # method in the example above. # # == Running a use case # # The run method is defined in a base class. This method # catches some exceptions and publishes corresponding notifications: # # Hexx::NotFoundError:: publishes the not_found(messages); # Hexx::UseCaseInvalid:: publishes the error(messages); # Hexx::EntityInvalid:: publishes the error(messages); # StandardError:: any other runtume exception. # # == Notifications publishing # # A use case is expected to publish notifications for its subscribers. # # class DoSomething < Hexx::UseCase # # def run! # validate! # # do something (raise in case of any error) # publish :done, result # return result # end # end # # This will call a +done+ method of any subscriber. For details see the # {wisper gem documentation}[https://github.com/krisleech/wisper]. # # == Calling a use case # # Use cases can be called in two styles: # # === Observer Pattern style (main usage) # # From a controller you can call a use case: # # # app/controllers/my_controller.rb # class MyController < ActionController::Base # # def my_action # # initialize a use case # use_case = DoSomething.new params # # subscribe both controller (in a presenter role) and other services # # such as mailers etc. to receive notifications. # use_case.subscribe self # use_case.subscribe MyMailer.new # # run a use_case # use_case.run # end # # # the method will be called by use_case.run in case of # # success (see the Notification publishing example above). # def done(result) # # return a response 200 to the user # end # # # the method will be called by use_case.run in case of # # NotFoundError raised. # def not_found(options = {}) # # return a response 404 to the user # end # # # this method will be called by use_case in case of any error # def error(messages = []) # # return a response 400 to the user # end # end # # === Procedural style # # When you use a case from another case it can be useful to get value directly # without a subscription. # # use_case = DoSomething.new params # result = use_case.run # # The +run+ method returns nil in case of any error. # # = Dependencies notes # # Use cases depends on: # # * *Entities* and their *Repositories*; # * *Values* as an interfaces to external services (controllers, mailers etc.) # # Use cases should not depend from external services outside of the domain # model: controllers, mailers, databases etc. # class UseCase include ActiveModel::Validations, Wisper::Publisher class << self def allow_params(*keys) @params = keys.map(&:to_s) end private def params @params ||= [] end end def initialize(options = {}) if options.is_a? Hash @params = options.stringify_keys.slice(*self.class.send(:params)) else @params = {} end end # Runs a case and raises an exceptions. # # Expected to be called from another use cases. For example, in the case # below calling a GetItem#run! will raise an exception if item # not found. Further on the exception will be catched in # DeleteItem#run method as its own. # # class DeleteItem < Hexx::UseCase # # allow_params :id # # def run! # item = GetItem.new(params).run! # item.delete! # end # end # def run! fail(NotImplementedError.new "#{ self.class.name }#run! not implemented") end # Runs a case and works out exceptions with sending corresponding # notifications to listeners. # # Expected to be called from a controller action. # def run run! rescue Hexx::NotFoundError => error finish_with :not_found, error.messages rescue Hexx::RuntimeError => error finish_with :error, error.messages rescue StandardError => error finish_with :error, [error.message] end private :valid?, :invalid? private attr_reader :params def validate! return if valid? fail UseCaseInvalid.new(self) end def finish_with(name, errors) publish name, errors.map { |text| Message.new type: :error, text: text } nil end def t(key, options = {}) return key unless key.is_a? Symbol scope = %w(activemodel messages models) << self.class.name.underscore I18n.t key, options.merge(scope: scope) end end end