mirror of
https://github.com/aasm/aasm
synced 2023-03-27 23:22:41 -04:00
chore(invokers): Refactor callback invokers, add class-callbacks support
This commit is contained in:
parent
c3153d56ad
commit
e43b87444c
16 changed files with 934 additions and 103 deletions
18
README.md
18
README.md
|
@ -130,16 +130,26 @@ the transition succeeds :
|
|||
|
||||
### Callbacks
|
||||
|
||||
You can define a number of callbacks for your transitions. These methods will be
|
||||
called, when certain criteria are met, like entering a particular state:
|
||||
You can define a number of callbacks for your events, transitions and states. These methods, Procs or classes will be
|
||||
called, when certain criteria are met, like entering a particular state (note that class must respond to `call` method):
|
||||
|
||||
```ruby
|
||||
class LogRunTime
|
||||
def initialize(resource)
|
||||
@resource = resource
|
||||
end
|
||||
|
||||
def call
|
||||
# Do whatever you want with @resource
|
||||
end
|
||||
end
|
||||
|
||||
class Job
|
||||
include AASM
|
||||
|
||||
aasm do
|
||||
state :sleeping, :initial => true, :before_enter => :do_something
|
||||
state :running
|
||||
state :running, before_enter: Proc.new { do_something && notify_somebody }
|
||||
state :finished
|
||||
|
||||
after_all_transitions :log_status_change
|
||||
|
@ -195,6 +205,8 @@ is finished.
|
|||
|
||||
AASM will also initialize `LogRunTime` and run the `call` method for you after the transition from `running` to `finished` in the example above. You can pass arguments to the class by defining an initialize method on it, like this:
|
||||
|
||||
Note that Procs are executed in the context of a record, it means that you don't need to expect the record as an argument, just call the methods you need.
|
||||
|
||||
```ruby
|
||||
class LogRunTime
|
||||
# optional args parameter can be omitted, but if you define initialize
|
||||
|
|
|
@ -9,6 +9,11 @@ require 'aasm/instance_base'
|
|||
require 'aasm/core/transition'
|
||||
require 'aasm/core/event'
|
||||
require 'aasm/core/state'
|
||||
require 'aasm/core/invoker'
|
||||
require 'aasm/core/invokers/base_invoker'
|
||||
require 'aasm/core/invokers/class_invoker'
|
||||
require 'aasm/core/invokers/literal_invoker'
|
||||
require 'aasm/core/invokers/proc_invoker'
|
||||
require 'aasm/localizer'
|
||||
require 'aasm/state_machine_store'
|
||||
require 'aasm/state_machine'
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AASM::Core
|
||||
class Event
|
||||
include DslHelper
|
||||
|
@ -156,27 +158,9 @@ module AASM::Core
|
|||
end
|
||||
|
||||
def invoke_callbacks(code, record, args)
|
||||
case code
|
||||
when Symbol, String
|
||||
unless record.respond_to?(code, true)
|
||||
raise NoMethodError.new("NoMethodError: undefined method `#{code}' for #{record.inspect}:#{record.class}")
|
||||
end
|
||||
arity = record.__send__(:method, code.to_sym).arity
|
||||
record.__send__(code, *(arity < 0 ? args : args[0...arity]))
|
||||
true
|
||||
|
||||
when Proc
|
||||
arity = code.arity
|
||||
record.instance_exec(*(arity < 0 ? args : args[0...arity]), &code)
|
||||
true
|
||||
|
||||
when Array
|
||||
code.each {|a| invoke_callbacks(a, record, args)}
|
||||
true
|
||||
|
||||
else
|
||||
false
|
||||
end
|
||||
Invoker.new(code, record, args)
|
||||
.with_default_return_value(false)
|
||||
.invoke
|
||||
end
|
||||
end
|
||||
end # AASM
|
||||
|
|
129
lib/aasm/core/invoker.rb
Normal file
129
lib/aasm/core/invoker.rb
Normal file
|
@ -0,0 +1,129 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AASM
|
||||
module Core
|
||||
##
|
||||
# main invoker class which encapsulates the logic
|
||||
# for invoking literal-based, proc-based, class-based
|
||||
# and array-based callbacks for different entities.
|
||||
class Invoker
|
||||
DEFAULT_RETURN_VALUE = true
|
||||
|
||||
##
|
||||
# Initialize a new invoker instance.
|
||||
# NOTE that invoker must be used per-subject/record
|
||||
# (one instance per subject/record)
|
||||
#
|
||||
# ==Options:
|
||||
#
|
||||
# +subject+ - invoking subject, may be Proc,
|
||||
# Class, String, Symbol or Array
|
||||
# +record+ - invoking record
|
||||
# +args+ - arguments which will be passed to the callback
|
||||
|
||||
def initialize(subject, record, args)
|
||||
@subject = subject
|
||||
@record = record
|
||||
@args = args
|
||||
@options = {}
|
||||
@failures = []
|
||||
@default_return_value = DEFAULT_RETURN_VALUE
|
||||
end
|
||||
|
||||
##
|
||||
# Pass additional options to concrete invoker
|
||||
#
|
||||
# ==Options:
|
||||
#
|
||||
# +options+ - hash of options which will be passed to
|
||||
# concrete invokers
|
||||
#
|
||||
# ==Example:
|
||||
#
|
||||
# with_options(guard: proc {...})
|
||||
|
||||
def with_options(options)
|
||||
@options = options
|
||||
self
|
||||
end
|
||||
|
||||
##
|
||||
# Collect failures to a specified buffer
|
||||
#
|
||||
# ==Options:
|
||||
#
|
||||
# +failures+ - failures buffer to collect failures
|
||||
|
||||
def with_failures(failures)
|
||||
@failures = failures
|
||||
self
|
||||
end
|
||||
|
||||
##
|
||||
# Change default return value of #invoke method
|
||||
# if none of invokers processed the request.
|
||||
#
|
||||
# The default return value is #DEFAULT_RETURN_VALUE
|
||||
#
|
||||
# ==Options:
|
||||
#
|
||||
# +value+ - default return value for #invoke method
|
||||
|
||||
def with_default_return_value(value)
|
||||
@default_return_value = value
|
||||
self
|
||||
end
|
||||
|
||||
##
|
||||
# Find concrete invoker for specified subject and invoker it,
|
||||
# or return default value set by #DEFAULT_RETURN_VALUE or
|
||||
# overridden by #with_default_return_value
|
||||
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
def invoke
|
||||
return invoke_array if subject.is_a?(Array)
|
||||
return literal_invoker.invoke if literal_invoker.may_invoke?
|
||||
return proc_invoker.invoke if proc_invoker.may_invoke?
|
||||
return class_invoker.invoke if class_invoker.may_invoke?
|
||||
default_return_value
|
||||
end
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
|
||||
private
|
||||
|
||||
attr_reader :subject, :record, :args, :options, :failures,
|
||||
:default_return_value
|
||||
|
||||
def invoke_array
|
||||
return subject.all? { |item| sub_invoke(item) } if options[:guard]
|
||||
return subject.all? { |item| !sub_invoke(item) } if options[:unless]
|
||||
subject.map { |item| sub_invoke(item) }
|
||||
end
|
||||
|
||||
def sub_invoke(new_subject)
|
||||
self.class.new(new_subject, record, args)
|
||||
.with_failures(failures)
|
||||
.with_options(options)
|
||||
.invoke
|
||||
end
|
||||
|
||||
def proc_invoker
|
||||
@proc_invoker ||= Invokers::ProcInvoker
|
||||
.new(subject, record, args)
|
||||
.with_failures(failures)
|
||||
end
|
||||
|
||||
def class_invoker
|
||||
@class_invoker ||= Invokers::ClassInvoker
|
||||
.new(subject, record, args)
|
||||
.with_failures(failures)
|
||||
end
|
||||
|
||||
def literal_invoker
|
||||
@literal_invoker ||= Invokers::LiteralInvoker
|
||||
.new(subject, record, args)
|
||||
.with_failures(failures)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
75
lib/aasm/core/invokers/base_invoker.rb
Normal file
75
lib/aasm/core/invokers/base_invoker.rb
Normal file
|
@ -0,0 +1,75 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AASM
|
||||
module Core
|
||||
module Invokers
|
||||
##
|
||||
# Base concrete invoker class which contain basic
|
||||
# invoking and logging definitions
|
||||
class BaseInvoker
|
||||
attr_reader :failures, :subject, :record, :args, :result
|
||||
|
||||
##
|
||||
# Initialize a new concrete invoker instance.
|
||||
# NOTE that concrete invoker must be used per-subject/record
|
||||
# (one instance per subject/record)
|
||||
#
|
||||
# ==Options:
|
||||
#
|
||||
# +subject+ - invoking subject comparable with this invoker
|
||||
# +record+ - invoking record
|
||||
# +args+ - arguments which will be passed to the callback
|
||||
|
||||
def initialize(subject, record, args)
|
||||
@subject = subject
|
||||
@record = record
|
||||
@args = args
|
||||
@result = false
|
||||
@failures = []
|
||||
end
|
||||
|
||||
##
|
||||
# Collect failures to a specified buffer
|
||||
#
|
||||
# ==Options:
|
||||
#
|
||||
# +failures+ - failures buffer to collect failures
|
||||
|
||||
def with_failures(failures_buffer)
|
||||
@failures = failures_buffer
|
||||
self
|
||||
end
|
||||
|
||||
##
|
||||
# Execute concrete invoker, log the error and return result
|
||||
|
||||
def invoke
|
||||
return unless may_invoke?
|
||||
log_failure unless invoke_subject
|
||||
result
|
||||
end
|
||||
|
||||
##
|
||||
# Check if concrete invoker may be invoked for a specified subject
|
||||
|
||||
def may_invoke?
|
||||
raise NoMethodError, '"#may_invoke?" is not implemented'
|
||||
end
|
||||
|
||||
##
|
||||
# Log failed invoking
|
||||
|
||||
def log_failure
|
||||
raise NoMethodError, '"#log_failure" is not implemented'
|
||||
end
|
||||
|
||||
##
|
||||
# Execute concrete invoker
|
||||
|
||||
def invoke_subject
|
||||
raise NoMethodError, '"#invoke_subject" is not implemented'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
52
lib/aasm/core/invokers/class_invoker.rb
Normal file
52
lib/aasm/core/invokers/class_invoker.rb
Normal file
|
@ -0,0 +1,52 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AASM
|
||||
module Core
|
||||
module Invokers
|
||||
##
|
||||
# Class invoker which allows to use classes which respond to #call
|
||||
# to be used as state/event/transition callbacks.
|
||||
class ClassInvoker < BaseInvoker
|
||||
def may_invoke?
|
||||
subject.is_a?(Class) && subject.instance_methods.include?(:call)
|
||||
end
|
||||
|
||||
def log_failure
|
||||
return log_source_location if Method.method_defined?(:source_location)
|
||||
log_method_info
|
||||
end
|
||||
|
||||
def invoke_subject
|
||||
@result = retrieve_instance.call
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def log_source_location
|
||||
failures << instance.method(:call).source_location.join('#')
|
||||
end
|
||||
|
||||
def log_method_info
|
||||
failures << instance.method(:call)
|
||||
end
|
||||
|
||||
def instance
|
||||
@instance ||= retrieve_instance
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
def retrieve_instance
|
||||
return subject.new if subject_arity.zero?
|
||||
return subject.new(record) if subject_arity == 1
|
||||
return subject.new(record, *args) if subject_arity < 0
|
||||
subject.new(record, *args[0..(subject_arity - 2)])
|
||||
end
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
|
||||
def subject_arity
|
||||
@arity ||= subject.instance_method(:initialize).arity
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
47
lib/aasm/core/invokers/literal_invoker.rb
Normal file
47
lib/aasm/core/invokers/literal_invoker.rb
Normal file
|
@ -0,0 +1,47 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AASM
|
||||
module Core
|
||||
module Invokers
|
||||
##
|
||||
# Literal invoker which allows to use strings or symbols to call
|
||||
# record methods as state/event/transition callbacks.
|
||||
class LiteralInvoker < BaseInvoker
|
||||
def may_invoke?
|
||||
subject.is_a?(String) || subject.is_a?(Symbol)
|
||||
end
|
||||
|
||||
def log_failure
|
||||
failures << subject
|
||||
end
|
||||
|
||||
def invoke_subject
|
||||
@result = exec_subject
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def subject_arity
|
||||
@arity ||= record.__send__(:method, subject.to_sym).arity
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
def exec_subject
|
||||
raise(*record_error) unless record.respond_to?(subject, true)
|
||||
return record.__send__(subject) if subject_arity.zero?
|
||||
return record.__send__(subject, *args) if subject_arity < 0
|
||||
record.__send__(subject, *args[0..(subject_arity - 1)])
|
||||
end
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
|
||||
def record_error
|
||||
[
|
||||
NoMethodError,
|
||||
'NoMethodError: undefined method ' \
|
||||
"`#{subject}' for #{record.inspect}:#{record.class}"
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
59
lib/aasm/core/invokers/proc_invoker.rb
Normal file
59
lib/aasm/core/invokers/proc_invoker.rb
Normal file
|
@ -0,0 +1,59 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AASM
|
||||
module Core
|
||||
module Invokers
|
||||
##
|
||||
# Proc invoker which allows to use Procs as
|
||||
# state/event/transition callbacks.
|
||||
class ProcInvoker < BaseInvoker
|
||||
def may_invoke?
|
||||
subject.is_a?(Proc)
|
||||
end
|
||||
|
||||
def log_failure
|
||||
return log_source_location if Method.method_defined?(:source_location)
|
||||
log_proc_info
|
||||
end
|
||||
|
||||
def invoke_subject
|
||||
@result = if support_parameters?
|
||||
exec_proc(parameters_to_arity)
|
||||
else
|
||||
exec_proc(subject.arity)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def support_parameters?
|
||||
subject.respond_to?(:parameters)
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
def exec_proc(parameters_size)
|
||||
return record.instance_exec(&subject) if parameters_size.zero?
|
||||
return record.instance_exec(*args, &subject) if parameters_size < 0
|
||||
record.instance_exec(*args[0..(parameters_size - 1)], &subject)
|
||||
end
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
|
||||
def log_source_location
|
||||
failures << subject.source_location.join('#')
|
||||
end
|
||||
|
||||
def log_proc_info
|
||||
failures << subject
|
||||
end
|
||||
|
||||
def parameters_to_arity
|
||||
subject.parameters.inject(0) do |memo, parameter|
|
||||
memo += 1
|
||||
memo *= -1 if parameter[0] == :rest && memo > 0
|
||||
memo
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AASM::Core
|
||||
class State
|
||||
attr_reader :name, :state_machine, :options
|
||||
|
@ -13,7 +15,13 @@ module AASM::Core
|
|||
def initialize_copy(orig)
|
||||
super
|
||||
@options = {}
|
||||
orig.options.each_pair { |name, setting| @options[name] = setting.is_a?(Hash) || setting.is_a?(Array) ? setting.dup : setting }
|
||||
orig.options.each_pair do |name, setting|
|
||||
@options[name] = if setting.is_a?(Hash) || setting.is_a?(Array)
|
||||
setting.dup
|
||||
else
|
||||
setting
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def ==(state)
|
||||
|
@ -75,14 +83,7 @@ module AASM::Core
|
|||
end
|
||||
|
||||
def _fire_callbacks(action, record, args)
|
||||
case action
|
||||
when Symbol, String
|
||||
arity = record.__send__(:method, action.to_sym).arity
|
||||
record.__send__(action, *(arity < 0 ? args : args[0...arity]))
|
||||
when Proc
|
||||
arity = action.arity
|
||||
action.call(record, *(arity < 0 ? args : args[0...arity]))
|
||||
end
|
||||
Invoker.new(action, record, args).invoke
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AASM::Core
|
||||
class Transition
|
||||
include DslHelper
|
||||
|
@ -67,77 +69,14 @@ module AASM::Core
|
|||
record.aasm(event.state_machine.name).to_state = @to if record.aasm(event.state_machine.name).respond_to?(:to_state=)
|
||||
end
|
||||
|
||||
case code
|
||||
when Symbol, String
|
||||
result = (record.__send__(:method, code.to_sym).arity == 0 ? record.__send__(code) : record.__send__(code, *args))
|
||||
failures << code unless result
|
||||
result
|
||||
when Proc
|
||||
if code.respond_to?(:parameters)
|
||||
# In Ruby's Proc, the 'arity' method is not a good condidate to know if
|
||||
# we should pass the arguments or not, since it does return 0 even in
|
||||
# presence of optional parameters.
|
||||
result = (code.parameters.size == 0 ? record.instance_exec(&code) : record.instance_exec(*args, &code))
|
||||
|
||||
failures << code.source_location.join('#') unless result
|
||||
else
|
||||
# In RubyMotion's Proc, the 'parameter' method does not exists, however its
|
||||
# 'arity' method works just like the one from Method, only returning 0 when
|
||||
# there is no parameters whatsoever, optional or not.
|
||||
result = (code.arity == 0 ? record.instance_exec(&code) : record.instance_exec(*args, &code))
|
||||
|
||||
# Sadly, RubyMotion's Proc does not define the method 'source_location' either.
|
||||
failures << code unless result
|
||||
end
|
||||
|
||||
result
|
||||
when Class
|
||||
arity = code.instance_method(:initialize).arity
|
||||
if arity == 0
|
||||
instance = code.new
|
||||
elsif arity == 1
|
||||
instance = code.new(record)
|
||||
else
|
||||
instance = code.new(record, *args)
|
||||
end
|
||||
result = instance.call
|
||||
|
||||
if Method.method_defined?(:source_location)
|
||||
failures << instance.method(:call).source_location.join('#') unless result
|
||||
else
|
||||
# RubyMotion support ('source_location' not defined for Method)
|
||||
failures << instance.method(:call) unless result
|
||||
end
|
||||
|
||||
result
|
||||
when Array
|
||||
if options[:guard]
|
||||
# invoke guard callbacks
|
||||
code.all? {|a| invoke_callbacks_compatible_with_guard(a, record, args)}
|
||||
elsif options[:unless]
|
||||
# invoke unless callbacks
|
||||
code.all? {|a| !invoke_callbacks_compatible_with_guard(a, record, args)}
|
||||
else
|
||||
# invoke after callbacks
|
||||
code.map {|a| invoke_callbacks_compatible_with_guard(a, record, args)}
|
||||
end
|
||||
else
|
||||
true
|
||||
end
|
||||
Invoker.new(code, record, args)
|
||||
.with_options(options)
|
||||
.with_failures(failures)
|
||||
.invoke
|
||||
end
|
||||
|
||||
def _fire_callbacks(code, record, args)
|
||||
case code
|
||||
when Symbol, String
|
||||
arity = record.send(:method, code.to_sym).arity
|
||||
record.send(code, *(arity < 0 ? args : args[0...arity]))
|
||||
when Proc
|
||||
code.arity == 0 ? record.instance_exec(&code) : record.instance_exec(*args, &code)
|
||||
when Array
|
||||
code.map {|a| _fire_callbacks(a, record, args)}
|
||||
else
|
||||
true
|
||||
end
|
||||
Invoker.new(code, record, args).invoke
|
||||
end
|
||||
|
||||
end
|
||||
|
|
189
spec/unit/invoker_spec.rb
Normal file
189
spec/unit/invoker_spec.rb
Normal file
|
@ -0,0 +1,189 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe AASM::Core::Invoker do
|
||||
let(:target) { nil }
|
||||
let(:record) { double }
|
||||
let(:args) { [] }
|
||||
|
||||
subject { described_class.new(target, record, args) }
|
||||
|
||||
describe '#with_options' do
|
||||
context 'when passing array as a subject' do
|
||||
context 'and "guard" option is set to true' do
|
||||
let(:target) { [subject_1, subject_2] }
|
||||
|
||||
before { subject.with_options(guard: true) }
|
||||
|
||||
context 'and all the subjects are truthy' do
|
||||
let(:subject_1) { Proc.new { true } }
|
||||
let(:subject_2) { Proc.new { true } }
|
||||
|
||||
it 'then returns "true" while invoking' do
|
||||
expect(subject.invoke).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'and any subject is falsely' do
|
||||
let(:subject_1) { Proc.new { false } }
|
||||
let(:subject_2) { Proc.new { true } }
|
||||
|
||||
it 'then returns "false" while invoking' do
|
||||
expect(subject.invoke).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'and "unless" option is set to true' do
|
||||
let(:target) { [subject_1, subject_2] }
|
||||
|
||||
before { subject.with_options(unless: true) }
|
||||
|
||||
context 'and all the subjects are falsely' do
|
||||
let(:subject_1) { Proc.new { false } }
|
||||
let(:subject_2) { Proc.new { false } }
|
||||
|
||||
it 'then returns "true" while invoking' do
|
||||
expect(subject.invoke).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'and any subject is truthy' do
|
||||
let(:subject_1) { Proc.new { false } }
|
||||
let(:subject_2) { Proc.new { true } }
|
||||
|
||||
it 'then returns "false" while invoking' do
|
||||
expect(subject.invoke).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#with_failures' do
|
||||
let(:concrete_invoker) { AASM::Core::Invokers::ProcInvoker }
|
||||
let(:target) { Proc.new {} }
|
||||
|
||||
it 'then sets failures buffer for concrete invokers' do
|
||||
expect_any_instance_of(concrete_invoker)
|
||||
.to receive(:with_failures)
|
||||
.and_call_original
|
||||
|
||||
subject.invoke
|
||||
end
|
||||
end
|
||||
|
||||
describe '#with_default_return_value' do
|
||||
context 'when return value is "true"' do
|
||||
before { subject.with_default_return_value(true) }
|
||||
|
||||
it 'then returns "true" when was not picked up by any invoker' do
|
||||
expect(subject.invoke).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when return value is "false"' do
|
||||
before { subject.with_default_return_value(false) }
|
||||
|
||||
it 'then returns "false" when was not picked up by any invoker' do
|
||||
expect(subject.invoke).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#invoke' do
|
||||
context 'when subject is a proc' do
|
||||
let(:concrete_invoker) { AASM::Core::Invokers::ProcInvoker }
|
||||
let(:target) { Proc.new {} }
|
||||
|
||||
it 'then calls proc invoker' do
|
||||
expect_any_instance_of(concrete_invoker)
|
||||
.to receive(:invoke)
|
||||
.and_call_original
|
||||
|
||||
expect(record).to receive(:instance_exec)
|
||||
|
||||
subject.invoke
|
||||
end
|
||||
end
|
||||
|
||||
context 'when subject is a class' do
|
||||
let(:concrete_invoker) { AASM::Core::Invokers::ClassInvoker }
|
||||
let(:target) { Class.new { def call; end } }
|
||||
|
||||
it 'then calls proc invoker' do
|
||||
expect_any_instance_of(concrete_invoker)
|
||||
.to receive(:invoke)
|
||||
.and_call_original
|
||||
|
||||
expect_any_instance_of(target).to receive(:call)
|
||||
|
||||
subject.invoke
|
||||
end
|
||||
end
|
||||
|
||||
context 'when subject is a literal' do
|
||||
let(:concrete_invoker) { AASM::Core::Invokers::LiteralInvoker }
|
||||
let(:record) { double(invoke_me: nil) }
|
||||
let(:target) { :invoke_me }
|
||||
|
||||
it 'then calls literal invoker' do
|
||||
expect_any_instance_of(concrete_invoker)
|
||||
.to receive(:invoke)
|
||||
.and_call_original
|
||||
|
||||
expect(record).to receive(:invoke_me)
|
||||
|
||||
subject.invoke
|
||||
end
|
||||
end
|
||||
|
||||
context 'when subject is an array of procs' do
|
||||
let(:subject_1) { Proc.new {} }
|
||||
let(:subject_2) { Proc.new {} }
|
||||
let(:target) { [subject_1, subject_2] }
|
||||
|
||||
it 'then calls each proc' do
|
||||
expect(record).to receive(:instance_exec).twice
|
||||
|
||||
subject.invoke
|
||||
end
|
||||
end
|
||||
|
||||
context 'when subject is an array of classes' do
|
||||
let(:subject_1) { Class.new { def call; end } }
|
||||
let(:subject_2) { Class.new { def call; end } }
|
||||
let(:target) { [subject_1, subject_2] }
|
||||
|
||||
it 'then calls each class' do
|
||||
expect_any_instance_of(subject_1).to receive(:call)
|
||||
|
||||
expect_any_instance_of(subject_2).to receive(:call)
|
||||
|
||||
subject.invoke
|
||||
end
|
||||
end
|
||||
|
||||
context 'when subject is an array of literals' do
|
||||
let(:subject_1) { :method_one }
|
||||
let(:subject_2) { :method_two }
|
||||
let(:record) { double(method_one: nil, method_two: nil) }
|
||||
let(:target) { [subject_1, subject_2] }
|
||||
|
||||
it 'then calls each class' do
|
||||
expect(record).to receive(:method_one)
|
||||
|
||||
expect(record).to receive(:method_two)
|
||||
|
||||
subject.invoke
|
||||
end
|
||||
end
|
||||
|
||||
context 'when subject is not supported' do
|
||||
let(:target) { nil }
|
||||
|
||||
it 'then just returns default value' do
|
||||
expect(subject.invoke).to eq(described_class::DEFAULT_RETURN_VALUE)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
72
spec/unit/invokers/base_invoker_spec.rb
Normal file
72
spec/unit/invokers/base_invoker_spec.rb
Normal file
|
@ -0,0 +1,72 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe AASM::Core::Invokers::BaseInvoker do
|
||||
let(:target) { double }
|
||||
let(:record) { double }
|
||||
let(:args) { [] }
|
||||
|
||||
subject { described_class.new(target, record, args) }
|
||||
|
||||
describe '#may_invoke?' do
|
||||
it 'then raises NoMethodError' do
|
||||
expect { subject.may_invoke? }.to raise_error(NoMethodError)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#log_failure' do
|
||||
it 'then raises NoMethodError' do
|
||||
expect { subject.log_failure }.to raise_error(NoMethodError)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#invoke_subject' do
|
||||
it 'then raises NoMethodError' do
|
||||
expect { subject.log_failure }.to raise_error(NoMethodError)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#with_failures' do
|
||||
it 'then sets failures buffer' do
|
||||
buffer = [1, 2, 3]
|
||||
subject.with_failures(buffer)
|
||||
|
||||
expect(subject.failures).to eq(buffer)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#invoke' do
|
||||
context 'when #may_invoke? respond with "false"' do
|
||||
before { allow(subject).to receive(:may_invoke?).and_return(false) }
|
||||
|
||||
it 'then returns "nil"' do
|
||||
expect(subject.invoke).to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when #invoke_subject respond with "false"' do
|
||||
before do
|
||||
allow(subject).to receive(:may_invoke?).and_return(true)
|
||||
allow(subject).to receive(:invoke_subject).and_return(false)
|
||||
end
|
||||
|
||||
it 'then calls #log_failure' do
|
||||
expect(subject).to receive(:log_failure)
|
||||
|
||||
subject.invoke
|
||||
end
|
||||
end
|
||||
|
||||
context 'when #invoke_subject succeed' do
|
||||
before do
|
||||
allow(subject).to receive(:may_invoke?).and_return(true)
|
||||
allow(subject).to receive(:invoke_subject).and_return(true)
|
||||
end
|
||||
|
||||
it 'then returns result' do
|
||||
expect(subject).to receive(:result)
|
||||
|
||||
subject.invoke
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
95
spec/unit/invokers/class_invoker_spec.rb
Normal file
95
spec/unit/invokers/class_invoker_spec.rb
Normal file
|
@ -0,0 +1,95 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe AASM::Core::Invokers::ClassInvoker do
|
||||
let(:target) { Class.new { def call; end } }
|
||||
let(:record) { double }
|
||||
let(:args) { [] }
|
||||
|
||||
subject { described_class.new(target, record, args) }
|
||||
|
||||
describe '#may_invoke?' do
|
||||
context 'when subject is a Class and responds to #call' do
|
||||
it 'then returns "true"' do
|
||||
expect(subject.may_invoke?).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when subject is not a class or not respond to #call' do
|
||||
let(:target) { Class.new {} }
|
||||
|
||||
it 'then returns "false"' do
|
||||
expect(subject.may_invoke?).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#log_failure' do
|
||||
context 'when subject respond to #source_location' do
|
||||
it 'then adds "source_location" to a failures buffer' do
|
||||
subject.log_failure
|
||||
|
||||
expect(subject.failures)
|
||||
.to eq([target.instance_method(:call).source_location.join('#')])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when subject does not respond to #source_location' do
|
||||
before do
|
||||
Method.__send__(:alias_method, :original_source_location, :source_location)
|
||||
Method.__send__(:undef_method, :source_location)
|
||||
end
|
||||
|
||||
after do
|
||||
Method.__send__(
|
||||
:define_method,
|
||||
:source_location,
|
||||
Method.instance_method(:original_source_location)
|
||||
)
|
||||
end
|
||||
|
||||
it 'then adds the subject to a failures buffer' do
|
||||
subject.log_failure
|
||||
|
||||
expect(subject.failures.first).to be_a(Method)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#invoke_subject' do
|
||||
context 'when passing no arguments' do
|
||||
let(:args) { [1, 2 ,3] }
|
||||
let(:target) { Class.new { def call; end } }
|
||||
|
||||
it 'then correctly uses passed arguments' do
|
||||
expect { subject.invoke_subject }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when passing single argument' do
|
||||
let(:args) { [1, 2 ,3, 4, 5, 6] }
|
||||
let(:target) { Class.new { def initialize(_a); end; def call; end } }
|
||||
|
||||
it 'then correctly uses passed arguments' do
|
||||
expect { subject.invoke_subject }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when passing variable number arguments' do
|
||||
let(:args) { [1, 2 ,3, 4, 5, 6] }
|
||||
let(:target) { Class.new { def initialize(_a, _b, *_c); end; def call; end } }
|
||||
|
||||
it 'then correctly uses passed arguments' do
|
||||
expect { subject.invoke_subject }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when passing one or more arguments' do
|
||||
let(:args) { [1, 2 ,3, 4, 5, 6] }
|
||||
let(:target) { Class.new { def initialize(_a, _b, _c); end; def call; end } }
|
||||
|
||||
it 'then correctly uses passed arguments' do
|
||||
expect { subject.invoke_subject }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
86
spec/unit/invokers/literal_invoker_spec.rb
Normal file
86
spec/unit/invokers/literal_invoker_spec.rb
Normal file
|
@ -0,0 +1,86 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe AASM::Core::Invokers::LiteralInvoker do
|
||||
let(:target) { nil }
|
||||
let(:record) { double }
|
||||
let(:args) { [] }
|
||||
|
||||
subject { described_class.new(target, record, args) }
|
||||
|
||||
describe '#may_invoke?' do
|
||||
context 'when subject is a Symbol' do
|
||||
let(:target) { :i_am_symbol }
|
||||
|
||||
it 'then returns "true"' do
|
||||
expect(subject.may_invoke?).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when subject is a String' do
|
||||
let(:target) { 'i_am_string' }
|
||||
|
||||
it 'then returns "true"' do
|
||||
expect(subject.may_invoke?).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when subject is neither a String nor Symbol' do
|
||||
let(:target) { double }
|
||||
|
||||
it 'then returns "false"' do
|
||||
expect(subject.may_invoke?).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#log_failure' do
|
||||
let(:target) { Proc.new { false } }
|
||||
|
||||
it 'then adds the subject to a failures buffer' do
|
||||
subject.log_failure
|
||||
|
||||
expect(subject.failures).to eq([target])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#invoke_subject' do
|
||||
context 'when passing no arguments' do
|
||||
let(:record) { Class.new { def my_method; end }.new }
|
||||
let(:args) { [1, 2 ,3] }
|
||||
let(:target) { :my_method }
|
||||
|
||||
it 'then correctly uses passed arguments' do
|
||||
expect { subject.invoke_subject }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when passing variable number arguments' do
|
||||
let(:record) { Class.new { def my_method(_a, _b, *_c); end }.new }
|
||||
let(:args) { [1, 2 ,3, 4, 5, 6] }
|
||||
let(:target) { :my_method }
|
||||
|
||||
it 'then correctly uses passed arguments' do
|
||||
expect { subject.invoke_subject }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when passing one or more arguments' do
|
||||
let(:record) { Class.new { def my_method(_a, _b, _c); end }.new }
|
||||
let(:args) { [1, 2 ,3, 4, 5, 6] }
|
||||
let(:target) { :my_method }
|
||||
|
||||
it 'then correctly uses passed arguments' do
|
||||
expect { subject.invoke_subject }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when record does not respond to subject' do
|
||||
let(:record) { Class.new { }.new }
|
||||
let(:target) { :my_method }
|
||||
|
||||
it 'then raises uses passed arguments' do
|
||||
expect { subject.invoke_subject }.to raise_error(NoMethodError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
86
spec/unit/invokers/proc_invoker_spec.rb
Normal file
86
spec/unit/invokers/proc_invoker_spec.rb
Normal file
|
@ -0,0 +1,86 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe AASM::Core::Invokers::ProcInvoker do
|
||||
let(:target) { Proc.new {} }
|
||||
let(:record) { double }
|
||||
let(:args) { [] }
|
||||
|
||||
subject { described_class.new(target, record, args) }
|
||||
|
||||
describe '#may_invoke?' do
|
||||
context 'when subject is a Proc' do
|
||||
it 'then returns "true"' do
|
||||
expect(subject.may_invoke?).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when subject is not a Proc' do
|
||||
let(:target) { nil }
|
||||
|
||||
it 'then returns "false"' do
|
||||
expect(subject.may_invoke?).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#log_failure' do
|
||||
context 'when subject respond to #source_location' do
|
||||
it 'then adds "source_location" to a failures buffer' do
|
||||
subject.log_failure
|
||||
|
||||
expect(subject.failures)
|
||||
.to eq([target.source_location.join('#')])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when subject does not respond to #source_location' do
|
||||
before do
|
||||
Method.__send__(:alias_method, :original_source_location, :source_location)
|
||||
Method.__send__(:undef_method, :source_location)
|
||||
end
|
||||
|
||||
after do
|
||||
Method.__send__(
|
||||
:define_method,
|
||||
:source_location,
|
||||
Method.instance_method(:original_source_location)
|
||||
)
|
||||
end
|
||||
|
||||
it 'then adds the subject to a failures buffer' do
|
||||
subject.log_failure
|
||||
|
||||
expect(subject.failures).to eq([target])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#invoke_subject' do
|
||||
context 'when passing no arguments' do
|
||||
let(:args) { [1, 2 ,3] }
|
||||
let(:target) { ->() {} }
|
||||
|
||||
it 'then correctly uses passed arguments' do
|
||||
expect { subject.invoke_subject }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when passing variable number arguments' do
|
||||
let(:args) { [1, 2 ,3, 4, 5, 6] }
|
||||
let(:target) { ->(_a, _b, *_c) {} }
|
||||
|
||||
it 'then correctly uses passed arguments' do
|
||||
expect { subject.invoke_subject }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when passing one or more arguments' do
|
||||
let(:args) { [1, 2 ,3, 4, 5, 6] }
|
||||
let(:target) { ->(_a, _b, _c) {} }
|
||||
|
||||
it 'then correctly uses passed arguments' do
|
||||
expect { subject.invoke_subject }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -64,7 +64,7 @@ describe AASM::Core::State do
|
|||
expect(record).to receive(:c)
|
||||
expect(record).to receive(:foobar)
|
||||
|
||||
state.fire_callbacks(:entering, record)
|
||||
state.fire_callbacks(:entering, record, record)
|
||||
end
|
||||
|
||||
it "should stop calling actions if one of them raises :halt_aasm_chain" do
|
||||
|
@ -84,6 +84,6 @@ describe AASM::Core::State do
|
|||
record = double('record')
|
||||
expect(record).to receive(:foobar)
|
||||
|
||||
state.fire_callbacks(:entering, record)
|
||||
state.fire_callbacks(:entering, record, record)
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue