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