Created
October 30, 2019 17:57
-
-
Save wojtha/980c548364175d8c232abab58b15e6e1 to your computer and use it in GitHub Desktop.
Interactor based on Hanami::Interactor, adapted for Rails/ActiveSupport.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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