injectable 0.0.5 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +35 -0
- data/LICENSE.txt +21 -0
- data/README.md +291 -142
- data/Rakefile +2 -26
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/injectable.gemspec +28 -0
- data/lib/injectable.rb +57 -46
- data/lib/injectable/class_methods.rb +157 -0
- data/lib/injectable/dependencies_graph.rb +43 -0
- data/lib/injectable/dependencies_proxy.rb +29 -0
- data/lib/injectable/dependency.rb +40 -0
- data/lib/injectable/instance_methods.rb +53 -0
- data/lib/injectable/missing_dependencies_exception.rb +4 -0
- data/lib/injectable/version.rb +1 -2
- metadata +82 -31
- data/LICENSE +0 -20
- data/lib/injectable/container.rb +0 -127
- data/lib/injectable/inflector.rb +0 -30
- data/lib/injectable/macros.rb +0 -60
- data/lib/injectable/registerable.rb +0 -26
- data/lib/injectable/registry.rb +0 -89
data/Rakefile
CHANGED
@@ -1,30 +1,6 @@
|
|
1
|
-
require "bundler"
|
2
|
-
Bundler.setup
|
3
|
-
|
4
|
-
require "rake"
|
1
|
+
require "bundler/gem_tasks"
|
5
2
|
require "rspec/core/rake_task"
|
6
3
|
|
7
|
-
|
8
|
-
require "injectable/version"
|
9
|
-
|
10
|
-
task :gem => :build
|
11
|
-
task :build do
|
12
|
-
system "gem build injectable.gemspec"
|
13
|
-
end
|
14
|
-
|
15
|
-
task :install => :build do
|
16
|
-
system "sudo gem install injectable-#{Injectable::VERSION}.gem"
|
17
|
-
end
|
18
|
-
|
19
|
-
task :release => :build do
|
20
|
-
system "git tag -a v#{Injectable::VERSION} -m 'Tagging #{Injectable::VERSION}'"
|
21
|
-
system "git push --tags"
|
22
|
-
system "gem push injectable-#{Injectable::VERSION}.gem"
|
23
|
-
system "rm injectable-#{Injectable::VERSION}.gem"
|
24
|
-
end
|
25
|
-
|
26
|
-
RSpec::Core::RakeTask.new("spec") do |spec|
|
27
|
-
spec.pattern = "spec/**/*_spec.rb"
|
28
|
-
end
|
4
|
+
RSpec::Core::RakeTask.new(:spec)
|
29
5
|
|
30
6
|
task :default => :spec
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "injectable"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/injectable.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "injectable/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'injectable'
|
8
|
+
spec.version = Injectable::VERSION
|
9
|
+
spec.authors = %w(Papipo iovis9 jantequera amrocco)
|
10
|
+
spec.email = %w(rodrigo@rubiconmd.com david.marchante@rubiconmd.com julio@rubiconmd.com anthony@rubiconmd.com)
|
11
|
+
|
12
|
+
spec.summary = %q{A library to help you build nice service objects with dependency injection.}
|
13
|
+
spec.homepage = 'https://github.com/rubiconmd/injectable'
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
# Specify which files should be added to the gem when it is released.
|
17
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
18
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
19
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
20
|
+
end
|
21
|
+
spec.bindir = 'exe'
|
22
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
|
+
spec.require_paths = ['lib']
|
24
|
+
|
25
|
+
spec.add_development_dependency 'bundler', '~> 2.0'
|
26
|
+
spec.add_development_dependency 'rake', '~> 12.0'
|
27
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
28
|
+
end
|
data/lib/injectable.rb
CHANGED
@@ -1,51 +1,62 @@
|
|
1
|
-
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
1
|
+
require 'injectable/version'
|
2
|
+
require 'injectable/class_methods'
|
3
|
+
require 'injectable/dependencies_graph'
|
4
|
+
require 'injectable/dependencies_proxy'
|
5
|
+
require 'injectable/dependency'
|
6
|
+
require 'injectable/instance_methods'
|
7
|
+
require 'injectable/missing_dependencies_exception'
|
6
8
|
|
7
|
-
#
|
8
|
-
# container, and removes some basic boilerplate code of creating basic
|
9
|
-
# constructors that set instance variables on the class. Constructor injection
|
10
|
-
# is the only available option here.
|
9
|
+
# Convert your class into an injectable service
|
11
10
|
#
|
12
|
-
# @
|
11
|
+
# @example
|
12
|
+
# You would create a service like this:
|
13
|
+
#
|
14
|
+
# class AddPlayerToTeamRoster
|
15
|
+
# include Injectable
|
16
|
+
#
|
17
|
+
# dependency :team_query
|
18
|
+
# dependency :player_query, class: UserQuery
|
19
|
+
#
|
20
|
+
# argument :team_id
|
21
|
+
# argument :player_id
|
22
|
+
#
|
23
|
+
# def call
|
24
|
+
# player_must_exist!
|
25
|
+
# team_must_exist!
|
26
|
+
# team_must_accept_players!
|
27
|
+
#
|
28
|
+
# team.add_to_roster(player)
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# private
|
32
|
+
#
|
33
|
+
# def player
|
34
|
+
# @player ||= player_query.call(player_id)
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# def team
|
38
|
+
# @team ||= team_query.call(team_id)
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# def player_must_exist!
|
42
|
+
# player.present? || raise UserNotFoundException
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# def team_must_exist!
|
46
|
+
# team.present? || raise TeamNotFoundException
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# def team_must_accept_players!
|
50
|
+
# team.accepts_players? || raise TeamFullException
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# And use it like this:
|
55
|
+
#
|
56
|
+
# AddPlayerToTeamRoster.call(player_id: player.id, team_id: team.id)
|
13
57
|
module Injectable
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
# Configure the global injectable registry. Simply just yields to the
|
18
|
-
# registry itself. If no block is provided returns the registry.
|
19
|
-
#
|
20
|
-
# @example Configure the registry.
|
21
|
-
# Injectable.configure do |registry|
|
22
|
-
# registry.register_implementation(:user, User)
|
23
|
-
# end
|
24
|
-
#
|
25
|
-
# @return [ Injectable::Registry ] The registry.
|
26
|
-
#
|
27
|
-
# @since 0.0.3
|
28
|
-
def configure
|
29
|
-
block_given? ? yield(Registry) : Registry
|
30
|
-
end
|
31
|
-
|
32
|
-
# Including the Injectable module will extend the class with the necessary
|
33
|
-
# macros.
|
34
|
-
#
|
35
|
-
# @example Include the module.
|
36
|
-
# class UserService
|
37
|
-
# include Injectable
|
38
|
-
# end
|
39
|
-
#
|
40
|
-
# @param [ Class ] klass The including class.
|
41
|
-
#
|
42
|
-
# @since 0.0.0
|
43
|
-
def included(klass)
|
44
|
-
Registry.register_implementation(
|
45
|
-
Inflector.underscore(klass.name).to_sym,
|
46
|
-
klass
|
47
|
-
)
|
48
|
-
klass.extend(Macros)
|
49
|
-
end
|
58
|
+
def self.included(base)
|
59
|
+
base.extend(Injectable::ClassMethods)
|
60
|
+
base.prepend(Injectable::InstanceMethods)
|
50
61
|
end
|
51
62
|
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
module Injectable
|
2
|
+
module ClassMethods
|
3
|
+
def self.extended(base)
|
4
|
+
base.class_eval do
|
5
|
+
simple_class_attribute :dependencies,
|
6
|
+
:call_arguments,
|
7
|
+
:initialize_arguments
|
8
|
+
|
9
|
+
self.dependencies = DependenciesGraph.new(namespace: base)
|
10
|
+
self.initialize_arguments = {}
|
11
|
+
self.call_arguments = {}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def inherited(base)
|
16
|
+
base.class_eval do
|
17
|
+
self.dependencies = dependencies.with_namespace(base)
|
18
|
+
self.initialize_arguments = initialize_arguments.dup
|
19
|
+
self.call_arguments = call_arguments.dup
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Blatantly stolen from rails' ActiveSupport.
|
24
|
+
# This is a simplified version of class_attribute
|
25
|
+
def simple_class_attribute(*attrs)
|
26
|
+
attrs.each do |name|
|
27
|
+
define_singleton_method(name) { nil }
|
28
|
+
|
29
|
+
ivar = "@#{name}"
|
30
|
+
|
31
|
+
define_singleton_method("#{name}=") do |val|
|
32
|
+
singleton_class.class_eval do
|
33
|
+
define_method(name) { val }
|
34
|
+
end
|
35
|
+
|
36
|
+
if singleton_class?
|
37
|
+
class_eval do
|
38
|
+
define_method(name) do
|
39
|
+
if instance_variable_defined? ivar
|
40
|
+
instance_variable_get ivar
|
41
|
+
else
|
42
|
+
singleton_class.send name
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
val
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Use the service with the params declared with '.argument'
|
53
|
+
# @param args [Hash] parameters needed for the Service
|
54
|
+
# @example MyService.call(foo: 'first_argument', bar: 'second_argument')
|
55
|
+
def call(args = {})
|
56
|
+
new.call(args)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Declare dependencies for the service
|
60
|
+
# @param name [Symbol] the name of the service
|
61
|
+
# @option options [Class] :class The class to use if it's different from +name+
|
62
|
+
# @option options [Symbol, Array<Symbol>] :depends_on if the dependency has more dependencies
|
63
|
+
# @yield explicitly declare the dependency
|
64
|
+
#
|
65
|
+
# @return [Object] the injected dependency
|
66
|
+
#
|
67
|
+
# @example Using the same name as the service object
|
68
|
+
# dependency :team_query
|
69
|
+
# # => @team_query = TeamQuery.new
|
70
|
+
#
|
71
|
+
# @example Specifying a different class
|
72
|
+
# dependency :player_query, class: UserQuery
|
73
|
+
# # => @player_query = UserQuery.new
|
74
|
+
#
|
75
|
+
# @example With a block
|
76
|
+
# dependency :active_players do
|
77
|
+
# ->(players) { players.select(&:active?) }
|
78
|
+
# end
|
79
|
+
# # => @active_players = [lambda]
|
80
|
+
#
|
81
|
+
# @example With more dependencies
|
82
|
+
# dependency :counter
|
83
|
+
# dependency :team_service
|
84
|
+
# dependency :player_counter, depends_on: [:counter, :team_service]
|
85
|
+
# # => @counter = Counter.new
|
86
|
+
# # => @team_service = TeamService.new
|
87
|
+
# # => @player_counter = PlayerCounter.new(counter: @counter, team_service: @team_service)
|
88
|
+
#
|
89
|
+
# @example Dependencies that don't accept keyword arguments
|
90
|
+
# dependency :counter
|
91
|
+
# dependency :player_counter, depends_on: :counter do |counter:|
|
92
|
+
# PlayerCounter.new(counter)
|
93
|
+
# end
|
94
|
+
# # => @counter = Counter.new
|
95
|
+
# # => @player_counter = PlayerCounter.new(@counter)
|
96
|
+
def dependency(name, options = {}, &block)
|
97
|
+
options[:block] = block if block_given?
|
98
|
+
options[:depends_on] = Array(options.fetch(:depends_on, []))
|
99
|
+
options[:name] = name
|
100
|
+
dependencies.add(options)
|
101
|
+
define_method name do
|
102
|
+
instance_variable_get("@#{name}") || dependencies_proxy.get(name)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Declare the arguments for `#call` and initialize the accessors
|
107
|
+
# This helps us clean up the code for memoization:
|
108
|
+
#
|
109
|
+
# ```
|
110
|
+
# private
|
111
|
+
#
|
112
|
+
# def player
|
113
|
+
# # player_id exists in the context because we added it as an argument
|
114
|
+
# @player ||= player_query.call(player_id)
|
115
|
+
# end
|
116
|
+
# ```
|
117
|
+
#
|
118
|
+
# Every argument is required unless given an optional default value
|
119
|
+
# @param name Name of the argument
|
120
|
+
# @option options :default The default value of the argument
|
121
|
+
# @example
|
122
|
+
# argument :player_id
|
123
|
+
# # => def call(player_id:)
|
124
|
+
# # => @player_id = player_id
|
125
|
+
# # => end
|
126
|
+
# @example with default arguments
|
127
|
+
# argument :team_id, default: 1
|
128
|
+
# # => def call(team_id: 1)
|
129
|
+
# # => @team_id = team_id
|
130
|
+
# # => end
|
131
|
+
def argument(name, options = {})
|
132
|
+
call_arguments[name] = options
|
133
|
+
attr_accessor name
|
134
|
+
end
|
135
|
+
|
136
|
+
def initialize_with(name, options = {})
|
137
|
+
initialize_arguments[name] = options
|
138
|
+
attr_accessor name
|
139
|
+
end
|
140
|
+
|
141
|
+
# Get the #initialize arguments declared with '.initialize_with' with no default
|
142
|
+
# @private
|
143
|
+
def required_initialize_arguments
|
144
|
+
find_required_arguments initialize_arguments
|
145
|
+
end
|
146
|
+
|
147
|
+
# Get the #call arguments declared with '.argument' with no default
|
148
|
+
# @private
|
149
|
+
def required_call_arguments
|
150
|
+
find_required_arguments call_arguments
|
151
|
+
end
|
152
|
+
|
153
|
+
def find_required_arguments(hash)
|
154
|
+
hash.reject { |_arg, options| options.key?(:default) }.keys
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Injectable
|
2
|
+
# Holds the dependency signatures of the service object
|
3
|
+
class DependenciesGraph
|
4
|
+
attr_reader :graph, :dependency_class, :proxy_class
|
5
|
+
attr_accessor :namespace
|
6
|
+
|
7
|
+
def initialize(namespace:,
|
8
|
+
proxy_class: ::Injectable::DependenciesProxy,
|
9
|
+
dependency_class: ::Injectable::Dependency)
|
10
|
+
@namespace = namespace
|
11
|
+
@graph = {}
|
12
|
+
@proxy_class = proxy_class
|
13
|
+
@dependency_class = dependency_class
|
14
|
+
end
|
15
|
+
|
16
|
+
def names
|
17
|
+
graph.keys
|
18
|
+
end
|
19
|
+
|
20
|
+
def with_namespace(namespace)
|
21
|
+
dup.tap { |dupe| dupe.namespace = namespace }
|
22
|
+
end
|
23
|
+
|
24
|
+
# Adds the signature of a dependency to the graph
|
25
|
+
def add(name:, depends_on:, **kwargs)
|
26
|
+
check_for_missing_dependencies!(depends_on)
|
27
|
+
graph[name] = dependency_class.new(kwargs.merge(name: name, depends_on: depends_on))
|
28
|
+
end
|
29
|
+
|
30
|
+
def proxy
|
31
|
+
proxy_class.new(graph: graph, namespace: namespace)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def check_for_missing_dependencies!(deps)
|
37
|
+
missing = deps.reject { |dep| graph.key?(dep) }
|
38
|
+
return if missing.empty?
|
39
|
+
|
40
|
+
raise Injectable::MissingDependenciesException, "missing dependencies: #{missing.join(', ')}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Injectable
|
2
|
+
# Memoizes the Dependencies generated by Dependency based on DependencyGraph
|
3
|
+
class DependenciesProxy
|
4
|
+
attr_reader :graph, :namespace
|
5
|
+
|
6
|
+
def initialize(graph:, namespace: nil)
|
7
|
+
@graph = graph
|
8
|
+
@namespace = namespace
|
9
|
+
@instances = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
# Get the instance of the dependency +name+
|
13
|
+
def get(name)
|
14
|
+
@instances[name] ||= graph[name].instance(args: memoized_dependencies_of(name), namespace: namespace)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def memoized_dependencies_of(name)
|
20
|
+
return [] if dependencies_of(name).empty?
|
21
|
+
|
22
|
+
dependencies_of(name).each_with_object({}) { |dep, hash| hash[dep] = get(dep) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def dependencies_of(name)
|
26
|
+
graph[name].depends_on
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Injectable
|
2
|
+
# Initialize a dependency based on the options or the block passed
|
3
|
+
Dependency = Struct.new(:name, :block, :class, :call, :with, :depends_on, keyword_init: true) do
|
4
|
+
def instance(args: [], namespace: nil)
|
5
|
+
args = wrap_args(args)
|
6
|
+
wrap_call build_instance(args, namespace: namespace)
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def wrap_args(args)
|
12
|
+
args = with unless with.nil?
|
13
|
+
args.is_a?(Array) ? args : [args]
|
14
|
+
end
|
15
|
+
|
16
|
+
def wrap_call(the_instance)
|
17
|
+
return the_instance unless call
|
18
|
+
|
19
|
+
lambda do |*args|
|
20
|
+
the_instance.public_send(call, *args)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def build_instance(args, namespace:)
|
25
|
+
block.nil? ? klass(namespace: namespace).new(*args) : block.call(*args)
|
26
|
+
end
|
27
|
+
|
28
|
+
def klass(namespace:)
|
29
|
+
self.class || resolve(namespace: namespace)
|
30
|
+
end
|
31
|
+
|
32
|
+
def resolve(namespace:)
|
33
|
+
(namespace || Object).const_get(camelcased)
|
34
|
+
end
|
35
|
+
|
36
|
+
def camelcased
|
37
|
+
@camelcased ||= name.to_s.split('_').map(&:capitalize).join
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|