Created
June 28, 2019 21:28
-
-
Save joegaudet/6efb6e63148a9bd4c8c49ea80873c379 to your computer and use it in GitHub Desktop.
dependency
This file contains hidden or 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
module DependencySupport | |
def self.included(base) | |
base.extend(ClassMethods) | |
end | |
# Substitutes a give dependency with a provided value | |
# will raise an exception if a provided dependency is missing | |
# @param [Sumbol] dependency_name | |
# @param [*] value | |
# @param [DependencySupport] instance of dependant to allow for chaining | |
def substitute_dependency(dependency_name, dependency_value) | |
legal_dep = self.class.dependencies.map(&:name).any? {|dep| dep == dependency_name} | |
unless legal_dep | |
raise "'#{dependency_name}' is not a declared dependency of #{self.class.to_s}" | |
end | |
if dependency_value.respond_to? :identify_substitute | |
dependency_value.identify_substitute(dependency_name) | |
end | |
instance_variable_set("@__#{dependency_name}".to_sym, dependency_value) | |
self | |
end | |
# Substitutes a hash of dependencies with the provided dependencies | |
# will raise an exception if a provided dependency is missing | |
# @param [Object] dependencies | |
# @param [DependencySupport] instance of dependant to allow for chaining | |
def substitute(dependencies = {}) | |
dependencies.each do |dependency_name, dependency_value| | |
substitute_dependency(dependency_name, dependency_value) | |
end | |
self | |
end | |
def report_test_failure | |
self.class.dependencies.map(&:name).each do |dependency_name, _dependency_value| | |
dep = instance_variable_get("@__#{dependency_name}".to_sym) | |
if dep.respond_to? :report_test_failure | |
Rails.logger.error "Dependency #{dependency_name}" | |
dep.report_test_failure | |
end | |
end | |
end | |
# Basic Inert Class maps args to return values | |
class InertCallable | |
attr_reader :call_history, :return | |
def initialize | |
@return = {} | |
@call_history = [] | |
end | |
def call(*args) | |
@call_history << args | |
@return[args] | |
end | |
def setup(*args, ret) | |
@return[args] = ret | |
end | |
def called_with?(*args) | |
@call_history.include? args | |
end | |
def called_only_with?(*args) | |
# This calls #called_with? but also ensures | |
# that the call history has only one entry | |
called_with?(*args) && @call_history.length == 1 | |
end | |
def never_called? | |
@call_history.length == 0 | |
end | |
def never_called_with?(*args) | |
!called_with?(*args) | |
end | |
def report_test_failure | |
Rails.logger.error 'Called With' | |
@call_history.each do |call| | |
Rails.logger.error call | |
end | |
end | |
end | |
class InertNullObject | |
def initialize(parent_klass) | |
@methods = {} | |
@parent_klass = parent_klass | |
end | |
def method_missing(m, *args, &block) | |
method = @parent_klass.instance_method(m) rescue @parent_klass.singleton_method(m) rescue nil | |
raise NoMethodError, "The parent class #{@parent_klass} does not define a #{m} method" if method.nil? | |
if block_given? | |
yield self[m] | |
else | |
ret = self[m] | |
ret.(*args) | |
end | |
end | |
def [](method) | |
@methods[method] ||= InertCallable.new | |
end | |
def report_test_failure | |
@methods.each do |m| | |
Rails.logger.error "Method: #{m}" | |
@methods[m].report_test_failure | |
end | |
end | |
end | |
module NullSubstitute | |
def self.included(base) | |
base.extend(ClassMethods) | |
end | |
module ClassMethods | |
def inert | |
InertNullObject.new(self) | |
end | |
end | |
end | |
module ClassMethods | |
def substitute(dependencies = {}) | |
self.new.substitute(dependencies) | |
end | |
# Constructor that initializes the dependent object with default substitutes from | |
# the registry, or at most | |
def as_subject(*args) | |
Rails.logger.debug "Building Subject '#{self.to_s}' in inert mode" | |
subject = self.new(*args) | |
self.dependencies.each do |dependency| | |
if dependency.klass.respond_to?(:inert) | |
Rails.logger.debug "Initializing Dependency '#{dependency.name}' in inert mode using it's provided substitute" | |
klass_or_instance = dependency.klass.inert | |
substitute = klass_or_instance.is_a?(Class) ? klass_or_instance.new : klass_or_instance | |
else | |
Rails.logger.debug "Defaulting Dependency '#{dependency.name}' to an InertCallable" | |
substitute = InertCallable.new | |
end | |
subject.substitute_dependency(dependency.name, substitute) | |
end | |
subject | |
end | |
# Defines a new dependency | |
# @param [Symbol] dependency_name | |
# @param [Class] dependency_class | |
# @param [?Proc] dependency_initializer | |
# @param [?Object] instance | |
# @param [Boolean] static | |
# @!macro [attach] dependency | |
# @return [$2] the $1 $0 | |
def dependency( | |
dependency_name, | |
dependency_class, | |
dependency_initializer: nil, | |
instance: nil, | |
static: false | |
) | |
dependencies.push(OpenStruct.new(name: dependency_name, klass: dependency_class)) | |
dependency_variable_name = "@__#{dependency_name}".to_sym | |
if static && instance.present? | |
raise 'You cannot declare a static dependency and then provide an instance' | |
end | |
if static && dependency_initializer | |
raise 'You cannot declare a static dependency and then provide a custom initializer' | |
end | |
if dependency_initializer && instance.present? | |
raise 'You cannot declare a initializer and provide an instance' | |
end | |
if dependency_initializer && dependency_initializer.arity != 1 | |
raise 'A custom initializer may only have 1 argument' | |
end | |
define_method(dependency_name) do | |
ret = instance_variable_get(dependency_variable_name) | |
if ret | |
ret | |
else | |
if dependency_initializer.nil? | |
# We are a standard dependency_initializer | |
if instance.present? || | |
!dependency_class.is_a?(Class) || | |
dependency_class.ancestors.include?(ActiveRecord::Base) || | |
dependency_class.ancestors.include?(ActiveModel::Model) || | |
dependency_class.ancestors.include?(ActionMailer::Base) || | |
static | |
dependency_instance = instance || dependency_class | |
else | |
dependency_instance = dependency_class.new | |
end | |
else | |
# User overridden dependency_initializer | |
begin | |
dependency_instance = dependency_initializer.(self) | |
rescue NoMethodError => e | |
if e.message.include?(self.to_s) | |
raise "There was an error executing your custom initializer for '#{dependency_name}'" | |
else | |
raise e | |
end | |
end | |
end | |
instance_variable_set(dependency_variable_name, dependency_instance) | |
end | |
end | |
end | |
# @return [Array<Object>] | |
def dependencies | |
@dependencies ||= [] | |
end | |
end | |
end |
This file contains hidden or 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
class DependencySupportTest < MiniTest::Test | |
describe DependencySupport do | |
class ARBaz < ActiveRecord::Base | |
end | |
class Bar | |
end | |
class NullBar | |
include DependencySupport::NullSubstitute | |
def foo(bar) | |
raise 'Should not be called' | |
end | |
def zero_arg | |
raise 'Should not be called' | |
end | |
end | |
class Qux | |
attr_accessor :bar | |
def initialize(bar) | |
@bar = bar | |
end | |
def self.inert | |
self.new('Inert') | |
end | |
end | |
class Static | |
def self.instance_bar | |
@instance_bar ||= Bar.new | |
end | |
end | |
class Foo | |
include DependencySupport | |
dependency :bar, Bar | |
dependency :null_bar, NullBar | |
dependency :static_bar, Bar, static: true | |
dependency :ar_baz_dao, ARBaz | |
dependency :instance_bar, Bar, instance: Static.instance_bar | |
dependency :qux, Qux, dependency_initializer: ->(receiver) {Qux.new(receiver.bar)} | |
end | |
it 'includes DependencySupport' do | |
assert Foo.ancestors.include?(DependencySupport) | |
end | |
it 'creates a dependency accessor for a class by initializing it' do | |
foo = Foo.new | |
bar = foo.bar | |
assert bar.is_a?(Bar) | |
end | |
it 'creates a dependency accessor for a AR Class' do | |
foo = Foo.new | |
baz_dao = foo.ar_baz_dao | |
assert_equal baz_dao, ARBaz | |
end | |
it 'creates a dependency accessor for instance references' do | |
foo = Foo.new | |
assert_equal foo.instance_bar, Static.instance_bar | |
end | |
it 'initializes dependencies using a provided custom initializer' do | |
foo = Foo.new | |
assert_equal foo.qux.bar, foo.bar | |
end | |
it 'creates static accessor when the static option is set' do | |
foo = Foo.new | |
assert_equal foo.static_bar, Bar | |
end | |
it 'allows for the substitution of dependencies' do | |
foo = Foo.new | |
foo.substitute(bar: 1) | |
assert_equal foo.bar, 1 | |
foo.substitute_dependency(:bar, 2) | |
assert_equal foo.bar, 2 | |
end | |
it 'raises an exception when you try and substitute a non existent dependency' do | |
foo = Foo.new | |
err = assert_raises do | |
foo.substitute(joe: 1) | |
end | |
assert_equal err.message, "'joe' is not a declared dependency of DependencySupportTest::Foo" | |
end | |
it 'raises an exception if the initializer fails' do | |
err = assert_raises do | |
class FailingFoo | |
include DependencySupport | |
dependency :qux, Qux, dependency_initializer: ->(receiver) { | |
Qux.new(receiver.bazo) | |
} | |
dependency :bar, Bar | |
end | |
FailingFoo.new.qux | |
end | |
assert_equal err.message, "There was an error executing your custom initializer for 'qux'" | |
end | |
it 'raises an exception if the initializer has the wrong number of arguments' do | |
err = assert_raises do | |
class FailingFoo2 | |
include DependencySupport | |
dependency :qux, Qux, dependency_initializer: ->() {Qux.new(receiver.bar)} | |
end | |
end | |
assert_equal err.message, 'A custom initializer may only have 1 argument' | |
end | |
it 'raises an exception when you declare both a static dependency and provide an instance' do | |
err = assert_raises do | |
class FailingFoo3 | |
include DependencySupport | |
dependency :qux, Qux, static: true, instance: Static.instance_bar | |
end | |
end | |
assert_equal err.message, 'You cannot declare a static dependency and then provide an instance' | |
end | |
it 'raises an exception when you declare both a static dependency and provide a custom initializer' do | |
err = assert_raises do | |
class FailingFoo4 | |
include DependencySupport | |
dependency :qux, Qux, static: true, dependency_initializer: ->(foo) {} | |
end | |
end | |
assert_equal err.message, 'You cannot declare a static dependency and then provide a custom initializer' | |
end | |
it 'raises an exception when you declare both a static dependency and provide a custom initializer' do | |
err = assert_raises do | |
class FailingFoo5 | |
include DependencySupport | |
dependency :qux, Qux, instance: Static.instance_bar, dependency_initializer: ->(foo) {} | |
end | |
end | |
assert_equal err.message, 'You cannot declare a initializer and provide an instance' | |
end | |
it 'allows the dependent to be constructed as a subject with inert dependencies' do | |
sut = Foo.as_subject | |
# Setup the inert to work a certain way | |
sut.bar.setup(1, 2, 3, 4) | |
assert sut.bar.(1, 2, 3) == 4 | |
end | |
it 'uses the inert substitute if a default one is provided' do | |
sut = Foo.as_subject | |
assert sut.qux.bar == 'Inert' | |
end | |
it 'uses the inert null substitute if one is provided' do | |
sut = Foo.as_subject | |
sut.null_bar.foo('bar') | |
assert sut.null_bar.foo {|m| m.called_only_with? 'bar'} | |
end | |
it 'allows multiple setups of a method' do | |
sut = Foo.as_subject | |
sut.null_bar.foo {|m| m.setup('a', 'b')} | |
sut.null_bar.foo {|m| m.setup('d', 'c')} | |
assert sut.null_bar.foo('a') == 'b' | |
assert sut.null_bar.foo('d') == 'c' | |
assert sut.null_bar.foo('e') == nil | |
end | |
it 'allows the setup of zero arg null method' do | |
sut = Foo.as_subject | |
class AnObject | |
attr_accessor :call_me | |
end | |
sut.null_bar[:zero_arg].setup(AnObject.new) | |
sut.null_bar.zero_arg.call_me | |
end | |
it 'allows the setup of any method by providing a block' do | |
sut = Foo.as_subject | |
class AnObject | |
attr_accessor :call_me | |
end | |
class AnotherObject | |
attr_accessor :call_me_too | |
end | |
sut.null_bar[:zero_arg].setup(AnObject.new) | |
sut.null_bar.zero_arg.call_me | |
sut.null_bar[:zero_arg].setup(AnotherObject.new) | |
sut.null_bar.zero_arg.call_me_too | |
end | |
end | |
describe DependencySupport::InertCallable do | |
describe 'expecting arguments' do | |
it 'registers return types' do | |
i = DependencySupport::InertCallable.new | |
i.setup(1, 2, 3, 4) | |
assert i.call(1, 2, 3) == 4 | |
end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment