Skip to content

Instantly share code, notes, and snippets.

@wojtha
Created October 30, 2019 17:57
Show Gist options
  • Save wojtha/980c548364175d8c232abab58b15e6e1 to your computer and use it in GitHub Desktop.
Save wojtha/980c548364175d8c232abab58b15e6e1 to your computer and use it in GitHub Desktop.
Interactor based on Hanami::Interactor, adapted for Rails/ActiveSupport.
# Interactor based on Hanami::Interactor, adapted for Rails/ActiveSupport.
#
# @see https://github.com/hanami/utils/blob/master/lib/hanami/interactor.rb
#
module Interactor
# Result of an operation
#
class Result
# Concrete methods
#
# @api private
#
# @see Interactor::Result#respond_to_missing?
METHODS = ::Hash[initialize: true,
success?: true,
successful?: true,
failure?: true,
fail!: true,
prepare!: true,
errors: true,
error: true].freeze
# Initialize a new result
#
# @param payload [Hash] a payload to carry on
#
# @return [Interactor::Result]
#
# @api private
def initialize(payload = {})
@payload = payload
@errors = []
@success = true
end
# Check if the current status is successful
#
# @return [TrueClass,FalseClass] the result of the check
#
def successful?
@success && errors.empty?
end
alias success? successful?
# Check if the current status is not successful
#
# @return [TrueClass,FalseClass] the result of the check
#
def failure?
!successful?
end
# Force the status to be a failure
#
def fail!
@success = false
end
# Returns all the errors collected during an operation
#
# @return [Array] the errors
#
#
# @see Interactor::Result#error
# @see Interactor#call
# @see Interactor#error
# @see Interactor#error!
def errors
@errors.dup
end
# @api private
def add_error(*errors)
@errors << errors
@errors.flatten!
nil
end
# Returns the first errors collected during an operation
#
# @return [nil,String] the error, if present
#
# @see Interactor::Result#errors
# @see Interactor#call
# @see Interactor#error
# @see Interactor#error!
def error
errors.first
end
# Prepare the result before to be returned
#
# @param payload [Hash] an updated payload
#
# @api private
def prepare!(payload)
@payload.merge!(payload)
self
end
protected
# @api private
def method_missing(m, *)
@payload.fetch(m) { super }
end
# @api private
def respond_to_missing?(method_name, _include_all)
method_name = method_name.to_sym
METHODS[method_name] || @payload.key?(method_name)
end
# @api private
def __inspect
" @success=#{@success} @payload=#{@payload.inspect}"
end
end
# Override for <tt>Module#included</tt>.
#
# @api private
def self.included(base)
super
base.class_eval do
prepend Interface
extend ClassMethods
end
end
# Interactor interface
#
module Interface
# Initialize an interactor
#
# It accepts arbitrary number of arguments.
# Developers can override it.
#
# @param args [Array<Object>] arbitrary number of arguments
#
# @return [Interactor] the interactor
#
# @example Override #initialize
# require 'hanami/interactor'
#
# class UpdateProfile
# include Interactor
#
# def initialize(user, params)
# @user = user
# @params = params
# end
#
# def call
# # ...
# end
# end
def initialize(*args)
super
ensure
@__result = ::Interactor::Result.new
end
# Triggers the operation and return a result.
#
# All the instance variables will be available in the result.
#
# ATTENTION: This must be implemented by the including class.
#
# @return [Interactor::Result] the result of the operation
#
# @raise [NoMethodError] if this isn't implemented by the including class.
#
# @example Expose instance variables in result payload
# require 'hanami/interactor'
#
# class Signup
# include Interactor
# expose :user, :params
#
# def initialize(params)
# @params = params
# @user = User.new(@params)
# @foo = 'bar'
# end
#
# def call
# @user = UserRepository.new.persist(@user)
# end
# end
#
# result = Signup.new(name: 'Luca').call
# result.failure? # => false
# result.successful? # => true
#
# result.user # => #<User:0x007fa311105778 @id=1 @name="Luca">
# result.params # => { :name=>"Luca" }
# result.foo # => raises NoMethodError
#
# @example Failed precondition
# require 'hanami/interactor'
#
# class Signup
# include Interactor
# expose :user
#
# def initialize(params)
# @params = params
# @user = User.new(@params)
# end
#
# # THIS WON'T BE INVOKED BECAUSE #valid? WILL RETURN false
# def call
# @user = UserRepository.new.persist(@user)
# end
#
# private
# def valid?
# @params.valid?
# end
# end
#
# result = Signup.new(name: nil).call
# result.successful? # => false
# result.failure? # => true
#
# result.user # => #<User:0x007fa311105778 @id=nil @name="Luca">
#
# @example Bad usage
# require 'hanami/interactor'
#
# class Signup
# include Interactor
#
# # Method #call is not defined
# end
#
# Signup.new.call # => NoMethodError
def call
_call { super }
end
end
private
# Check if proceed with <tt>#call</tt> invokation.
# By default it returns <tt>true</tt>.
#
# Developers can override it.
#
# @return [TrueClass,FalseClass] the result of the check
#
def valid?
true
end
# Fail and interrupt the current flow.
#
# @example
# require 'hanami/interactor'
#
# class CreateEmailTest
# include Interactor
#
# def initialize(params)
# @params = params
# @email_test = EmailTest.new(@params)
# end
#
# def call
# persist_email_test!
# capture_screenshot!
# end
#
# private
# def persist_email_test!
# @email_test = EmailTestRepository.new.persist(@email_test)
# end
#
# # IF THIS RAISES AN EXCEPTION WE FORCE A FAILURE
# def capture_screenshot!
# Screenshot.new(@email_test).capture!
# rescue
# fail!
# end
# end
#
# result = CreateEmailTest.new(account_id: 1).call
# result.successful? # => false
def fail!
@__result.fail!
throw :fail
end
# Log an error without interrupting the flow.
#
# When used, the returned result won't be successful.
#
# @param message [String] the error message
#
# @return false
#
# @see Interactor#error!
#
# @example
# require 'hanami/interactor'
#
# class CreateRecord
# include Interactor
# expose :logger
#
# def initialize
# @logger = []
# end
#
# def call
# prepare_data!
# persist!
# sync!
# end
#
# private
# def prepare_data!
# @logger << __method__
# error "Prepare data error"
# end
#
# def persist!
# @logger << __method__
# error "Persist error"
# end
#
# def sync!
# @logger << __method__
# end
# end
#
# result = CreateRecord.new.call
# result.successful? # => false
#
# result.errors # => ["Prepare data error", "Persist error"]
# result.logger # => [:prepare_data!, :persist!, :sync!]
def error(message)
@__result.add_error message
false
end
# Log an error AND interrupting the flow.
#
# When used, the returned result won't be successful.
#
# @param message [String] the error message
#
# @see Interactor#error
#
# @example
# require 'hanami/interactor'
#
# class CreateRecord
# include Interactor
# expose :logger
#
# def initialize
# @logger = []
# end
#
# def call
# prepare_data!
# persist!
# sync!
# end
#
# private
# def prepare_data!
# @logger << __method__
# error "Prepare data error"
# end
#
# def persist!
# @logger << __method__
# error! "Persist error"
# end
#
# # THIS WILL NEVER BE INVOKED BECAUSE WE USE #error! IN #persist!
# def sync!
# @logger << __method__
# end
# end
#
# result = CreateRecord.new.call
# result.successful? # => false
#
# result.errors # => ["Prepare data error", "Persist error"]
# result.logger # => [:prepare_data!, :persist!]
def error!(message)
error(message)
fail!
end
# @api private
def _call
catch :fail do
validate!
yield
end
_prepare!
end
def validate!
fail! unless valid?
end
# @api private
def _prepare!
@__result.prepare!(_exposures)
end
# @api private
def _exposures
Hash[].tap do |result|
self.class.exposures.each do |name, ivar|
result[name] = instance_variable_get(ivar)
end
end
end
end
# @api private
module ClassMethods
# @api private
def self.extended(interactor)
interactor.class_eval do
class_attribute :exposures
self.exposures = {}
end
end
# Allow shortcut method to invoke the interactor.
#
# @api public
#
# @see Interactor::Interface#call
#
# @example Shortcut execution
#
# class Signup
# include Interactor
# expose :user
#
# def initialize(params)
# @params = params
# @user = User.new(@params[:user])
# end
#
# def call
# # ...
# end
# end
#
# result = Signup.call(user: { name: "Luca" })
# result = Signup.(user: { name: "Luca" })
#
# @note Inline Manual addition
#
def call(*args)
new(*args).call
end
# Expose local instance variables into the returning value of <tt>#call</tt>
#
# @param instance_variable_names [Symbol,Array<Symbol>] one or more instance
# variable names
#
# @see Interactor::Result
#
# @note attr_reader is Inline Manual addition
#
# @example Expose instance variable
#
# class Signup
# include Interactor
# expose :user
#
# def initialize(params)
# @params = params
# @user = User.new(@params[:user])
# end
#
# def call
# user.save!
# # ...
# end
# end
#
# result = Signup.new(user: { name: "Luca" }).call
#
# result.user # => #<User:0x007fa85c58ccd8 @name="Luca">
# result.params # => NoMethodError
#
def expose(*instance_variable_names)
instance_variable_names.each do |name|
exposures[name.to_sym] = "@#{name}"
# Inline Manual addition to make exposures accessible within the Interactor itself
attr_reader name.to_sym
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment