Skip to content

Instantly share code, notes, and snippets.

@joegaudet
Created June 28, 2019 21:28
Show Gist options
  • Save joegaudet/6efb6e63148a9bd4c8c49ea80873c379 to your computer and use it in GitHub Desktop.
Save joegaudet/6efb6e63148a9bd4c8c49ea80873c379 to your computer and use it in GitHub Desktop.
dependency
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
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