Skip to content

Commit

Permalink
Merge pull request #12 from tim-shaker/tshaker/add-injected-methods-v…
Browse files Browse the repository at this point in the history
…ariable

Add injected_methods helpers
  • Loading branch information
zachmargolis authored Jul 24, 2024
2 parents 87e339d + 6e2f036 commit 7af49a5
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 5 deletions.
6 changes: 6 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -39,7 +50,7 @@ class MyClass
@dependency=dependency
end

def other_depency
def other_dependency
@other_dependency ||= AnotherClass.new
end
end
Expand Down
49 changes: 49 additions & 0 deletions lib/interjectable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<Symbol>]
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
2 changes: 1 addition & 1 deletion lib/interjectable/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Interjectable
VERSION = "1.2.0"
VERSION = "1.3.0"
end
171 changes: 169 additions & 2 deletions spec/interjectable_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 7af49a5

Please sign in to comment.