diff --git a/CHANGES.md b/CHANGES.md index 811b28f..3becd66 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Change Log +# v1.3.0 + +- Add an `injected_methods(include_super = true)` instance and singleton helper method to track what dependency methods + have been created. This method includes itself in the list of injected methods. The instance method will return both + injected and static injected methods, while the singleton method will only return static injected methods. + # v1.2.0 - Ruby 3.x made it an error to override a class variable in a parent class. There was a bug with `inject_static` where diff --git a/README.md b/README.md index 3a3aab7..eeb5d3f 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ gem 'interjectable' ## Usage -Interjectable has one module (`Interjectable`) and two methods. Use them like so! +Interjectable has one module (`Interjectable`) and two main methods for defining dependencies, +`inject` and `inject_static`. Use them like so! ```ruby class MyClass @@ -28,6 +29,16 @@ class MyClass end ``` +It also includes introspection method `injected_methods(include_super = true)` (both instance and class-level) +to track what dependency methods have been created. + +```ruby +MyClass.injected_methods +# => [:injected_methods, :shared_value, :shared_value=] +MyClass.new.injected_methods +# => [:injected_methods, :dependency, :dependency=, :other_dependency, :other_dependency=, :shared_value, :shared_value=] +``` + This replaces a pattern we've used before, adding default dependencies in the constructor, or as memoized methods. ```ruby @@ -39,7 +50,7 @@ class MyClass @dependency=dependency end - def other_depency + def other_dependency @other_dependency ||= AnotherClass.new end end diff --git a/lib/interjectable.rb b/lib/interjectable.rb index 7342084..1bf69d5 100644 --- a/lib/interjectable.rb +++ b/lib/interjectable.rb @@ -7,10 +7,34 @@ module Interjectable def self.included(mod) mod.send(:extend, ClassMethods) + mod.send(:include, InstanceMethods) end def self.extended(mod) mod.send(:extend, ClassMethods) + mod.send(:include, InstanceMethods) + end + + module InstanceMethods + def injected_methods(include_super = true) + injected = self.class.instance_variable_get(:@injected_methods).to_a + + self.class.instance_variable_get(:@static_injected_methods).to_a + + if include_super + super_injected = self.class.ancestors.flat_map do |klass| + klass.instance_variable_get(:@injected_methods).to_a + + klass.instance_variable_get(:@static_injected_methods).to_a + end + + [ + :injected_methods, + *super_injected, + *injected, + ].uniq + else + [:injected_methods, *injected] + end + end end module ClassMethods @@ -42,6 +66,9 @@ def inject(dependency, &default_block) instance_variable_set(ivar_name, instance_eval(&default_block)) end end + + @injected_methods ||= [] + @injected_methods += [dependency, :"#{dependency}="] end # Defines helper methods on instances that memoize values per-class. @@ -88,6 +115,28 @@ def inject_static(dependency, &default_block) injecting_class.class_variable_set(cvar_name, instance_eval(&default_block)) end end + + @static_injected_methods ||= [] + @static_injected_methods += [dependency, :"#{dependency}="] + end + + # @return [Array] + def injected_methods(include_super = true) + injected = @static_injected_methods.to_a + + if include_super + super_injected = ancestors.flat_map do |klass| + klass.instance_variable_get(:@static_injected_methods).to_a + end + + [ + :injected_methods, + *super_injected, + *injected, + ].uniq + else + [:injected_methods, *injected] + end end end end diff --git a/lib/interjectable/version.rb b/lib/interjectable/version.rb index 67a8492..4ca5efd 100644 --- a/lib/interjectable/version.rb +++ b/lib/interjectable/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Interjectable - VERSION = "1.2.0" + VERSION = "1.3.0" end diff --git a/spec/interjectable_spec.rb b/spec/interjectable_spec.rb index 6a7e0f0..b487955 100644 --- a/spec/interjectable_spec.rb +++ b/spec/interjectable_spec.rb @@ -81,8 +81,24 @@ let(:subclass_instance) { subclass.new } it "does not error if the method exists on the superclass" do - subclass.inject(:dependency) { :some_other_value } - expect(subclass_instance.dependency).to eq(:some_other_value) + subclass.inject(:some_dependency) { :some_other_value } + expect(subclass_instance.some_dependency).to eq(:some_other_value) + end + + it "allows injection on the subclass without injecting on the superclass" do + subclass.inject(:subclass_dependency) { :brand_new_value } + expect(subclass_instance.subclass_dependency).to eq(:brand_new_value) + expect { instance.subclass_dependency }.to raise_error(NoMethodError) + end + + context "with a chain of subclasses" do + let(:lower_subclass) { Class.new(subclass) } + let(:lower_subclass_instance) { lower_subclass.new } + + it "retrieves injected methods from all ancestors when requested" do + subclass.inject(:subclass_dependency) { :subclass_value } + lower_subclass.inject(:lower_subclass_dependency) { :lower_subclass_value } + end end end end @@ -181,6 +197,157 @@ expect(subclass.static_dependency).to eq(:some_value) expect(klass.class_variable_get(:@@static_dependency)).to eq(:some_value) end + + it "allows injection on the subclass without injecting on the superclass" do + subclass.inject_static(:static_subclass_dependency) { :brand_new_value } + expect(subclass_instance.static_subclass_dependency).to eq(:brand_new_value) + expect { klass.static_subclass_dependency }.to raise_error(NoMethodError) + expect { instance.static_subclass_dependency }.to raise_error(NoMethodError) + end + + context "with a chain of subclasses" do + let(:lower_subclass) { Class.new(subclass) } + let(:lower_subclass_instance) { lower_subclass.new } + + it "retrieves injected methods from all ancestors when requested" do + subclass.inject_static(:static_subclass_dependency) { :subclass_value } + lower_subclass.inject_static(:static_lower_subclass_dependency) { :lower_subclass_value } + end + end + end + end + + describe "#injected_methods" do + before do + klass.inject(:a) { :a } + klass.inject_static(:b) { :b } + end + + it "lists injected methods on the instance and static ones too" do + injected_methods = instance.injected_methods + + expect(injected_methods).to match_array( + [ + :injected_methods, :a, :a=, :b, :b=, + ], + ) + end + + context "with a subclass" do + let(:subclass) do + Class.new(klass) do + inject(:c) { :c } + end + end + let(:include_super) { true } + let(:subclass_instance) { subclass.new } + + it "includes super methods by default" do + injected_methods = subclass_instance.injected_methods(include_super) + + expect(injected_methods).to match_array( + [ + :injected_methods, + :a, + :a=, + :b, + :b=, + :c, + :c=, + ], + ) + end + + context "with include_super = false" do + let(:include_super) { false } + + it "does not include super methods" do + injected_methods = subclass_instance.injected_methods(include_super) + + expect(injected_methods).to_not include(:a) + expect(injected_methods).to_not include(:a=) + expect(injected_methods).to_not include(:b) + expect(injected_methods).to_not include(:b=) + + expect(injected_methods).to match_array( + [ + :injected_methods, + :c, + :c=, + ], + ) + end + end + end + end + + describe ".injected_methods" do + before do + klass.inject(:a) { :a } + klass.inject_static(:b) { :b } + end + + it "lists static injected methods class" do + injected_methods = klass.injected_methods + + expect(injected_methods).to match_array( + [ + :injected_methods, :b, :b=, + ], + ) + expect(injected_methods).to_not include(:a) + expect(injected_methods).to_not include(:a=) + end + + context "with a subclass" do + let(:subclass) do + Class.new(klass) do + inject(:c) { :c } + inject_static(:d) { :d } + end + end + let(:include_super) { true } + + it "includes super methods by default" do + injected_methods = subclass.injected_methods(include_super) + + expect(injected_methods).to match_array( + [ + :injected_methods, + :b, + :b=, + :d, + :d= + ], + ) + expect(injected_methods).to_not include(:a), 'skips instance methods' + expect(injected_methods).to_not include(:a=), 'skips instance methods' + expect(injected_methods).to_not include(:c), 'skips instance methods' + expect(injected_methods).to_not include(:c=), 'skips instance methods' + end + + context "with include_super = false" do + let(:include_super) { false } + + it "does not include super methods" do + injected_methods = subclass.injected_methods(include_super) + + expect(injected_methods).to_not include(:a), 'skips instance methods' + expect(injected_methods).to_not include(:a=), 'skips instance methods' + expect(injected_methods).to_not include(:b), 'skips super methods' + expect(injected_methods).to_not include(:b=), 'skips super methods' + expect(injected_methods).to_not include(:c), 'skips instance methods' + expect(injected_methods).to_not include(:c=), 'skips instance methods' + + expect(injected_methods).to match_array( + [ + :injected_methods, + :d, + :d=, + ], + ) + end + end end end end