little_boxes 0.1.0 → 0.3.7
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 +4 -4
- data/.gitignore +2 -0
- data/Gemfile +1 -0
- data/README.md +187 -2
- data/Rakefile +4 -0
- data/lib/little_boxes.rb +10 -4
- data/lib/little_boxes/box.rb +111 -10
- data/lib/little_boxes/configurable.rb +66 -0
- data/lib/little_boxes/entry.rb +30 -0
- data/lib/little_boxes/entry_definition.rb +28 -0
- data/lib/little_boxes/strategy.rb +79 -0
- data/lib/little_boxes/version.rb +1 -1
- data/little_boxes.gemspec +0 -2
- data/spec/benchmarks_spec.rb +162 -0
- data/spec/docs/readme_spec.rb +32 -0
- data/spec/integration/thread_safety_spec.rb +76 -0
- data/spec/little_boxes/box_spec.rb +222 -234
- data/spec/real_benchmarks_spec.rb +164 -0
- data/spec/spec_helper.rb +24 -6
- metadata +17 -41
- data/lib/little_boxes/defined_dependant.rb +0 -9
- data/lib/little_boxes/dependant.rb +0 -44
- data/lib/little_boxes/dependant_registry.rb +0 -44
- data/lib/little_boxes/memoized_dependant.rb +0 -9
- data/lib/little_boxes/null_logger.rb +0 -8
- data/lib/little_boxes/obtained.rb +0 -13
- data/lib/little_boxes/registry.rb +0 -84
- data/spec/little_boxes/dependant_spec.rb +0 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8d707db9ba1fe89421ed9075a15f9c0de9da1d45
|
4
|
+
data.tar.gz: f70cd242054f28c5aa232d2c0303efb04d7efd40
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3eae4e0071fbaa498d6be5b03b08712b78cc99b5e301177034d32166c6f46b6e262f32bd24b706f04f3fe40c43944cdef10aa7228820f0afe5e4b43242496574
|
7
|
+
data.tar.gz: a5f83ed8ee3ded14d79180e49a78d04fcb9b373620860831fc8ab7d807c960b35ec565cdc4c8aaaa446671174b7208a93d2bf2060091c875ed095347dda471cc
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,192 @@
|
|
1
1
|
# LittleBoxes
|
2
2
|
|
3
|
-
|
3
|
+
Dependency injection library in Ruby.
|
4
|
+
|
5
|
+
## Intro
|
6
|
+
|
7
|
+
LittleBoxes allows you to create a _box_ which is capable of providing already
|
8
|
+
configured objects that depend among each other.
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
module MyApp
|
12
|
+
class MainBox
|
13
|
+
include LittleBoxes::Box
|
14
|
+
|
15
|
+
let(:port) { 80 }
|
16
|
+
letc(:server) { Server.new }
|
17
|
+
end
|
18
|
+
|
19
|
+
class Server
|
20
|
+
include LittleBoxes::Configurable
|
21
|
+
|
22
|
+
dependency :port
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
box = MyApp::MainBox.new
|
27
|
+
# => #<MyApp::MainBox :port, :server>
|
28
|
+
|
29
|
+
box.server.port
|
30
|
+
# => 80
|
31
|
+
```
|
32
|
+
|
33
|
+
The `let` keyword provides lazy evaluated dependencies:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
class MainBox
|
37
|
+
include LittleBoxes::Box
|
38
|
+
|
39
|
+
let(:redis) do
|
40
|
+
require 'redis'
|
41
|
+
Redis.new
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
box = MyApp::MainBox.new
|
46
|
+
box.redis
|
47
|
+
# => #<MyApp::Redis:0x0055acd05b6350>
|
48
|
+
```
|
49
|
+
|
50
|
+
Notice that in this situation `require 'redis'` will not evaluated until we
|
51
|
+
call `box.redis`. This approach, in opposition to initializers brings two
|
52
|
+
benefits:
|
53
|
+
|
54
|
+
* Libraries are only loaded when needed. Unused libraries are not loaded.
|
55
|
+
I. e. when running only a subset of the tests.
|
56
|
+
* There is no need to manually set the order of the initializers, it will be
|
57
|
+
resolved at run-time.
|
58
|
+
|
59
|
+
Dependencies can rely on other dependencies:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
class Publisher
|
63
|
+
attr_accessor :redis
|
64
|
+
|
65
|
+
def initialize(redis: redis)
|
66
|
+
@redis = redis
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class MyApp::MainBox
|
71
|
+
include LittleBoxes::Box
|
72
|
+
|
73
|
+
let(:redis) do
|
74
|
+
require 'redis'
|
75
|
+
Redis.new
|
76
|
+
end
|
77
|
+
|
78
|
+
let(:publisher) do |box|
|
79
|
+
Publisher.new redis: box.redis
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
box = MyApp::MainBox.new
|
84
|
+
box.publisher
|
85
|
+
# => #<MyApp::Publisher:0x0055df32201ae0 @redis=#<MyApp::Redis:0x0055df32201b30>>
|
86
|
+
```
|
87
|
+
|
88
|
+
However, what kind of dependency injection library would this be if dependencies
|
89
|
+
wouldn't be automatically resolved. We can use the `letc` method for that:
|
90
|
+
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
class Publisher
|
94
|
+
include LittleBoxes::Configurable
|
95
|
+
dependency :redis
|
96
|
+
end
|
97
|
+
|
98
|
+
class MainBox
|
99
|
+
# ...
|
100
|
+
|
101
|
+
letc(:publisher) { Publisher.new }
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
Configurable objects accept default values passed as a lambda, which receives
|
106
|
+
the box as an argument:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
class Server
|
110
|
+
include LittleBoxes::Configurable
|
111
|
+
dependency(:port) { 80 }
|
112
|
+
dependency(:log) { |box| box.logger }
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
|
117
|
+
If classes instead of instances are your thing, not that I recommend it, the
|
118
|
+
`class_depency` will do:
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
class UsersApi
|
122
|
+
include LittleBoxes::Configurable
|
123
|
+
class_dependency :logger
|
124
|
+
end
|
125
|
+
|
126
|
+
class MainBox
|
127
|
+
# ...
|
128
|
+
letc(:users_api) { UsersApi }
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
Working with classes you might find the problem that they tend to be accessed
|
133
|
+
directly through the constant, not injected. This totally skips the lazy
|
134
|
+
configuration we discussed before.
|
135
|
+
|
136
|
+
For those cases we can force the Box to eager-configure such dependency:
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
class MainBox
|
140
|
+
# ...
|
141
|
+
eager_letc(:users_api) { UsersApi }
|
142
|
+
end
|
143
|
+
```
|
144
|
+
|
145
|
+
Now the class will be configured as soon as you do `MainBox.new`.
|
146
|
+
|
147
|
+
Sometimes we don't want the box to memoize our dependencies and we want it
|
148
|
+
to execute the lambda each time. This were `get` and `getc` have a role:
|
149
|
+
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
class MainBox
|
153
|
+
# ...
|
154
|
+
get(:config) { { port: 80 } }
|
155
|
+
getc(:new_server) { Server.new }
|
156
|
+
end
|
157
|
+
```
|
158
|
+
|
159
|
+
Boxes can be nested, allowing to create a better arranged tree of dependencies.
|
160
|
+
Very useful as your application grows:
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
class MainBox
|
164
|
+
box(:users) do
|
165
|
+
letc(:users_api) { UsersApi }
|
166
|
+
let(:logger) { Logger.new('/tmp/users.log') }
|
167
|
+
end
|
168
|
+
|
169
|
+
let(:logger) { Logger.new('/tmp/app.log') }
|
170
|
+
end
|
171
|
+
```
|
172
|
+
|
173
|
+
Notice how in this case, any object inside the `users` box will log to
|
174
|
+
`users.log`. Dependencies are resolved recursively up to the root box.
|
175
|
+
Throwing a `LittleBoxes::DependencyNotFound` when missing at root level.
|
176
|
+
|
177
|
+
To avoid defining a huge single Box file, you can import boxes defined elsewhere:
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
class UsersBox
|
181
|
+
letc(:users_api) { UsersApi }
|
182
|
+
end
|
183
|
+
|
184
|
+
class MainBox
|
185
|
+
import UsersBox
|
186
|
+
|
187
|
+
let(:logger) { Logger.new('/tmp/app.log') }
|
188
|
+
end
|
189
|
+
```
|
4
190
|
|
5
191
|
|
6
192
|
## Contributing
|
@@ -20,7 +206,6 @@ $ gem bump --version minor # Bump the gem version to the next minor level
|
|
20
206
|
$ gem bump --version patch # Bump the gem version to the next patch level (e.g. 0.0.1 to 0.0.2)
|
21
207
|
```
|
22
208
|
|
23
|
-
|
24
209
|
## License
|
25
210
|
|
26
211
|
Released under the MIT License.
|
data/Rakefile
CHANGED
data/lib/little_boxes.rb
CHANGED
@@ -1,7 +1,13 @@
|
|
1
1
|
module LittleBoxes
|
2
|
-
|
3
|
-
|
4
|
-
|
2
|
+
def self.root_path
|
3
|
+
Pathname.new(__FILE__) + '../..'
|
4
|
+
end
|
5
5
|
|
6
|
-
|
6
|
+
def self.lib_path
|
7
|
+
root_path + 'lib'
|
8
|
+
end
|
9
|
+
|
10
|
+
Dir.glob(lib_path + 'little_boxes/*').each{|p| require p }
|
11
|
+
|
12
|
+
DependencyNotFound = Class.new(StandardError)
|
7
13
|
end
|
data/lib/little_boxes/box.rb
CHANGED
@@ -1,19 +1,120 @@
|
|
1
1
|
module LittleBoxes
|
2
|
-
|
3
|
-
|
2
|
+
module Box
|
3
|
+
module ClassMethods
|
4
|
+
def entry_definitions
|
5
|
+
@entry_definitions ||= {}
|
6
|
+
end
|
4
7
|
|
5
|
-
|
6
|
-
|
8
|
+
def inspect
|
9
|
+
"#{name}(#{entry_definitions.keys.map(&:inspect).join(", ")})"
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def import(klass)
|
15
|
+
importable_definitions = klass.entry_definitions
|
16
|
+
entry_definitions.merge!(importable_definitions)
|
17
|
+
importable_definitions.each_key do |name|
|
18
|
+
define_method(name) do
|
19
|
+
@entries[name].value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def box(name, klass = nil, &block)
|
25
|
+
if klass
|
26
|
+
box_from_klass(name, klass)
|
27
|
+
elsif block_given?
|
28
|
+
inline_box(name, &block)
|
29
|
+
else
|
30
|
+
fail ArgumentError,
|
31
|
+
'Either class or block should be passed as argument'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def box_from_klass(name, klass)
|
36
|
+
eager(name) { |box| klass.new(parent: box) }
|
37
|
+
end
|
38
|
+
|
39
|
+
def inline_box(name, &block)
|
40
|
+
eager(name) do |box|
|
41
|
+
Class.new do
|
42
|
+
include ::LittleBoxes::Box
|
43
|
+
|
44
|
+
instance_eval(&block)
|
45
|
+
end.new(parent: box)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def get(name, options={}, &block)
|
50
|
+
entry_definitions[name] = EntryDefinition.new(name, options, &block)
|
51
|
+
.tap do |entry|
|
52
|
+
define_method(name) do
|
53
|
+
@entries[name].value
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def getc(name, options={}, &block)
|
59
|
+
get(name, options.merge(configure: true), &block)
|
60
|
+
end
|
61
|
+
|
62
|
+
def let(name, options={}, &block)
|
63
|
+
get(name, options.merge(memo: true), &block)
|
64
|
+
end
|
65
|
+
|
66
|
+
def letc(name, options={}, &block)
|
67
|
+
let(name, options.merge(configure: true), &block)
|
68
|
+
end
|
69
|
+
|
70
|
+
def eager(name, options={}, &block)
|
71
|
+
let(name, options.merge(eager: true), &block)
|
72
|
+
end
|
73
|
+
|
74
|
+
def eagerc(name, options={}, &block)
|
75
|
+
eager(name, options.merge(configure: true), &block)
|
76
|
+
end
|
7
77
|
end
|
8
78
|
|
9
|
-
|
10
|
-
|
79
|
+
attr_reader :parent, :entries
|
80
|
+
|
81
|
+
def self.included(klass)
|
82
|
+
klass.extend ClassMethods
|
83
|
+
end
|
84
|
+
|
85
|
+
def [] name
|
86
|
+
entry = @entries[name] ||= (@parent && @parent.entries[name])
|
87
|
+
entry ? entry.value : (parent && parent[name])
|
88
|
+
end
|
89
|
+
|
90
|
+
def inspect
|
91
|
+
"#<#{self.class.name} #{entries.keys.map(&:inspect).join(", ")}>"
|
92
|
+
end
|
93
|
+
|
94
|
+
def method_missing(name, *args, &block)
|
95
|
+
if respond_to?(name)
|
96
|
+
self[name.to_sym]
|
97
|
+
else
|
98
|
+
super
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def respond_to_missing?(name, include_private = false)
|
103
|
+
@parent.respond_to?(name, include_private)
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def initialize(parent: nil)
|
109
|
+
@parent = parent
|
110
|
+
@entries = entry_definitions.each_with_object({}) do |(k,v), acc|
|
111
|
+
acc[k] = v.for(self)
|
112
|
+
end
|
113
|
+
@entries.values.select(&:eager).each { |e| send(e.name) }
|
11
114
|
end
|
12
115
|
|
13
|
-
def
|
14
|
-
|
15
|
-
self[name] = s
|
16
|
-
ForwardingDsl.run s, &block
|
116
|
+
def entry_definitions
|
117
|
+
self.class.entry_definitions
|
17
118
|
end
|
18
119
|
end
|
19
120
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module LittleBoxes
|
2
|
+
module Configurable
|
3
|
+
attr_accessor :config
|
4
|
+
|
5
|
+
def initialize(options = {})
|
6
|
+
@config = {}
|
7
|
+
|
8
|
+
options.keys.each do |k|
|
9
|
+
config[k] = options[k]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def configure(&block)
|
14
|
+
yield @config
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def self.included(klass)
|
21
|
+
klass.extend ClassMethods
|
22
|
+
|
23
|
+
klass.class_eval do
|
24
|
+
class << self
|
25
|
+
attr_accessor :config
|
26
|
+
instance_variable_set :@config, {}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module ClassMethods
|
32
|
+
def dependency name, &default_block
|
33
|
+
default_block ||= Proc.new do
|
34
|
+
fail(DependencyNotFound, "Dependency #{name} not found")
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
define_method name do
|
40
|
+
@config[name] ||= default_block.call(@config[:box])
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def class_dependency name, &default_block
|
45
|
+
default_block ||= Proc.new do
|
46
|
+
fail(DependencyNotFound, "Dependency #{name} not found")
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
define_singleton_method name do
|
52
|
+
@config[name] ||= default_block.call(@config[:box])
|
53
|
+
end
|
54
|
+
|
55
|
+
define_method name do
|
56
|
+
self.class.config[name] ||= default_block.call(self.class.config[:box])
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def configure(&block)
|
61
|
+
yield @config
|
62
|
+
self
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|