Skip to content

Commit

Permalink
Support (re)defining chain specific failure chains
Browse files Browse the repository at this point in the history
Such a failure chain will get invoked in case any
of the chain's handlers doesn't rescue from an
exception it raises.

Chain specific failure chains differ from processor
specific failure chains, in that they are meant to
be invoked for exceptional use cases only. They
make a chain "failsafe" by making sure that
Chain#call always returns a valid response, even if
the underlying code raised an exception.
  • Loading branch information
snusnu committed Jul 10, 2013
1 parent 2548953 commit 44a8262
Show file tree
Hide file tree
Showing 18 changed files with 424 additions and 143 deletions.
42 changes: 35 additions & 7 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
ENV = Substation::Environment.build do
register :validate, Substation::Processor::Evaluator::Data
register :call, Substation::Processor::Evaluator::Pivot
register :wrap, Substation::Processor::Wrapper
register :render, Substation::Processor::Transformer
end

class Error
Expand All @@ -23,17 +25,18 @@
@data = data
end

ValidationError = Class.new(self)
InternalError = Class.new(self)
ValidationError = Class.new(self)
ApplicationError = Class.new(self)
InternalError = Class.new(self)
end

module App
VALIDATION_ERROR = Demo::ENV.chain { wrap Error::ValidationError }
INTERNAL_ERROR = Demo::ENV.chain { wrap Error::InternalError }
VALIDATION_ERROR = Demo::ENV.chain { wrap Error::ValidationError }
APPLICATION_ERROR = Demo::ENV.chain { wrap Error::ApplicationError }

SOME_ACTION = Demo::ENV.chain do
validate Vanguard::Validator, VALIDATION_ERROR
call Some::Action, INTERNAL_ERROR
call Some::Action, APPLICATION_ERROR
end
end

Expand All @@ -42,8 +45,8 @@
render Renderer::ValidationError
end

INTERNAL_ERROR = Demo::ENV.chain(App::INTERNAL_ERROR) do
render Renderer::InternalError
APPLICATION_ERROR = Demo::ENV.chain(App::APPLICATION_ERROR) do
render Renderer::ApplicationError
end

# in case of success, returns an instance of Views::Person
Expand All @@ -58,6 +61,31 @@
end
end

* [feature] Support (re)defining chain specific failure chains in case of uncaught exceptions.

module Demo

module App
INTERNAL_ERROR = Demo::ENV.chain { wrap Error::InternalError }
end

module Web

INTERNAL_ERROR = Demo::ENV.chain(App::INTERNAL_ERROR) do
render Renderer::InternalError
end

# The INTERNAL_ERROR chain will be called if an exception
# isn't rescued by the responsible handler
SOME_ACTION = Demo::ENV.chain(App::SOME_ACTION, INTERNAL_ERROR) do
failure_chain :validate, VALIDATION_ERROR
failure_chain :call, INTERNAL_ERROR
wrap Presenters::Person
wrap Views::ShowPerson
end
end
end

[Compare v0.0.8..master](https://github.com/snusnu/substation/compare/v0.0.8...master)

# v0.0.8 2013-06-19
Expand Down
4 changes: 2 additions & 2 deletions config/flay.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
threshold: 8
total_score: 114
threshold: 9
total_score: 134
2 changes: 2 additions & 0 deletions config/reek.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ LongParameterList:
exclude:
- Substation::Dispatcher#call
- Substation::Chain::DSL::Builder#define_dsl_method
- Substation::Chain#self.failure_response
- Substation::Chain#on_failure
max_params: 2
LongYieldList:
enabled: true
Expand Down
2 changes: 1 addition & 1 deletion config/rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ SingleLineMethods:
Enabled: false

LineLength:
Max: 88 # the offending lines are in specs, sadly this means global disabling for now
Max: 106 # the offending lines are in specs, sadly this means global disabling for now

MethodLength:
Max: 12 # reek performs these checks anyway
115 changes: 107 additions & 8 deletions lib/substation/chain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,89 @@ module Substation
class Chain

include Enumerable
include Concord.new(:processors)
include Concord.new(:processors, :failure_chain)
include Adamantium::Flat

# Empty chain
EMPTY = Class.new(self).new(EMPTY_ARRAY)
EMPTY = Class.new(self).new(EMPTY_ARRAY, EMPTY_ARRAY)

# Wraps response data and an exception not caught from a handler
class FailureData
include Equalizer.new(:data)

# Return the data available when +exception+ was raised
#
# @return [Object]
#
# @api private
attr_reader :data

# Return the exception instance
#
# @return [Class<StandardError>]
#
# @api private
attr_reader :exception

# Initialize a new instance
#
# @param [Object] data
# the data available when +exception+ was raised
#
# @param [Class<StandardError>] exception
# the exception instance raised from a handler
#
# @return [undefined]
#
# @api private
def initialize(data, exception)
@data, @exception = data, exception
end

# Return the hash value
#
# @return [Fixnum]
#
# @api private
def hash
super ^ exception.class.hash
end

private

# Tests wether +other+ is comparable using +comparator+
#
# @param [Symbol] comparator
# the operation used for comparison
#
# @param [Object] other
# the object to test
#
# @return [Boolean]
#
# @api private
def cmp?(comparator, other)
super && exception.class.send(comparator, other.exception.class)
end
end

# Return a failure response
#
# @param [Request] request
# the initial request passed into the chain
#
# @param [Object] data
# the processed data available when the exception was raised
#
# @param [Class<StandardError>] exception
# the exception instance that was raised
#
# @return [Response::Failure]
#
# @api private
def self.failure_response(request, data, exception)
Response::Failure.new(request, FailureData.new(data, exception))
end

# Call the chain
#
Expand Down Expand Up @@ -111,15 +189,16 @@ class Chain
# @return [Response::Failure]
# the response returned from the failing processor's failure chain
#
# @raise [Exception]
# any exception that isn't explicitly rescued in client code
#
# @api public
def call(request)
processors.reduce(request) { |result, processor|
response = processor.call(result)
return response unless processor.success?(response)
processor.result(response)
begin
response = processor.call(result)
return response unless processor.success?(response)
processor.result(response)
rescue => exception
return on_failure(request, result, exception)
end
}
end

Expand All @@ -142,5 +221,25 @@ def each(&block)
self
end

private

# Call the failure chain in case of an uncaught exception
#
# @param [Request] request
# the initial request passed into the chain
#
# @param [Object] data
# the processed data available when the exception was raised
#
# @param [Class<StandardError>] exception
# the exception instance that was raised
#
# @return [Response::Failure]
#
# @api private
def on_failure(request, data, exception)
failure_chain.call(self.class.failure_response(request, data, exception))
end

end # class Chain
end # module Substation
15 changes: 10 additions & 5 deletions lib/substation/chain/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,21 +77,26 @@ def define_dsl_method(name, processor, dsl)

end # class Builder

# The processors to be used within a {Chain}
# Build a new {Chain} based on +other+, a +failure_chain+ and a block
#
# @param [#each<#call>] processors
# @param [#each<#call>] other
# the processors to build on top of
#
# @param [Chain] failure_chain
# the failure chain to invoke in case of an uncaught exception
#
# @param [Proc] block
# a block to be instance_eval'ed
#
# @return [Array<#call>]
# @return [Chain]
#
# @api private
def self.processors(chain, &block)
new(chain, &block).processors
def self.build(other, failure_chain, &block)
Chain.new(new(other, &block).processors, failure_chain)
end

include Equalizer.new(:processors)

# Initialize a new instance
#
# @param [#each<#call>] processors
Expand Down
13 changes: 7 additions & 6 deletions lib/substation/environment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ class Environment
#
# @api private
def self.build(&block)
new(DSL.registry(&block))
registry = DSL.registry(&block)
chain_dsl = Chain::DSL::Builder.call(registry)
new(registry, chain_dsl)
end

# Initialize a new instance
Expand All @@ -28,9 +30,8 @@ def self.build(&block)
# @return [undefined]
#
# @api private
def initialize(registry)
@registry = registry
@chain_dsl = Chain::DSL::Builder.call(@registry)
def initialize(registry, chain_dsl)
@registry, @chain_dsl = registry, chain_dsl
end

# Build a new {Chain} instance
Expand All @@ -44,8 +45,8 @@ def initialize(registry)
# @return [Chain]
#
# @api private
def chain(other = Chain::EMPTY, &block)
Chain.new(@chain_dsl.processors(other, &block))
def chain(other = Chain::EMPTY, failure_chain = Chain::EMPTY, &block)
@chain_dsl.build(other, failure_chain, &block)
end

protected
Expand Down

0 comments on commit 44a8262

Please sign in to comment.