-
Notifications
You must be signed in to change notification settings - Fork 637
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'Advanon-chores/refactor-callback-invokers'
- Loading branch information
Showing
16 changed files
with
933 additions
and
102 deletions.
There are no files selected for viewing
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
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Oops, something went wrong.