Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP - Automatically fail context on exception #176

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions interactor.gemspec
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
require "English"

Gem::Specification.new do |spec|
spec.name = "interactor"
spec.name = "interactor"
spec.version = "3.1.1"

spec.author = "Collective Idea"
spec.email = "[email protected]"
spec.author = "Collective Idea"
spec.email = "[email protected]"
spec.description = "Interactor provides a common interface for performing complex user interactions."
spec.summary = "Simple interactor implementation"
spec.homepage = "https://github.com/collectiveidea/interactor"
spec.license = "MIT"
spec.summary = "Simple interactor implementation"
spec.homepage = "https://github.com/collectiveidea/interactor"
spec.license = "MIT"

spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
spec.test_files = spec.files.grep(/^spec/)

spec.add_development_dependency "bundler"
Expand Down
68 changes: 66 additions & 2 deletions lib/interactor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,72 @@ def self.included(base)
extend ClassMethods
include Hooks

# Internal: Expose instance variables of Interactor instance metaclass
# for reading.
class << self
attr_reader :exception_classes
attr_reader :exception_handlers
end
# Public: Gets the Interactor::Context of the Interactor instance.
attr_reader :context
end
end

# Internal: Interactor class methods.
module ClassMethods
# Public: Specify exception classes that should result in failing context
# and provide a custom logic on rescued exception before failing. Failing
# the context is raising Interactor::Failure, this exception is silently
# swallowed by the interactor. Note that any code after failing the context
# will not be evaluated.
#
# Examples
# class MyInteractor
# include Interactor
#
# fail_on_exception StandardErro
# fail_on_exception NameError, NoMethodError
#
# exception_handler = ->(e) { ErrorLogger.log(e) }
#
# fail_on_exception MyBespokeError, exception_handler: exception_handler
#
# def call
# exception_raising_logic
# end
# end
#
# MyInteractor.call
# # => #<Interactor::Context error=#<NameError: undefined local variable
# or method `method_raising_exception' for
# #<MyInteractor:0x0000000003d17330> Did you mean? method_missing>>
#
# MyInteractor.call.success?
# # => false
#
# Returned context holds the rescued exception object
#
# MyInteractor.call.error.class.name
# # => "NameError"
#
# Method accepts object representing exception classes of any type that
# will respond to #to_s and return string, as an argument to
# Kernel.const_get will result in previously initialized constant.
# e.g. constant, symbol, string...

def fail_on_exception(*exceptions_to_fail_on, exception_handler: ->(e) {})
exceptions_to_fail_on = exceptions_to_fail_on.each do |it|
Kernel.const_get(it.to_s)
end
@exception_classes = Array(exception_classes) | exceptions_to_fail_on
return unless exception_handler
exceptions_to_fail_on.each do |exception_class|
@exception_handlers = Hash(exception_handlers).update(
exception_class.name.to_sym => exception_handler
)
end
end

# Public: Invoke an Interactor. This is the primary public API method to an
# interactor.
#
Expand Down Expand Up @@ -140,8 +199,13 @@ def run
# Raises Interactor::Failure if the context is failed.
def run!
with_hooks do
call
context.called!(self)
begin
call
context.called!(self)
rescue *self.class.exception_classes => e
self.class.exception_handlers[e.class.name.to_sym]&.call(e)
context.fail!(error: e)
end
end
rescue
context.rollback!
Expand Down