little_boxes 0.1.0 → 0.3.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 86e6644930011e5254977f6ab0809cafa6a22696
4
- data.tar.gz: bc84eb541accd46a4f35a3eb3e13d51913f489ca
3
+ metadata.gz: 8d707db9ba1fe89421ed9075a15f9c0de9da1d45
4
+ data.tar.gz: f70cd242054f28c5aa232d2c0303efb04d7efd40
5
5
  SHA512:
6
- metadata.gz: efe43b09428af99ed51374d404f2d590ab135cba431fc7d53317d93d304befdb00db1ddb38f65aace6460d0bd684932d88d8cd26e0950e72ee551543eca5d531
7
- data.tar.gz: 3fe8e90d9769f77b80b795eb939b5fc4b4779e4c61db4bc7ac6d84861c32824e1cfbc6650b3061f5dc009e415ae2eac4fbcbd8d266b1b521afd83dc243967379
6
+ metadata.gz: 3eae4e0071fbaa498d6be5b03b08712b78cc99b5e301177034d32166c6f46b6e262f32bd24b706f04f3fe40c43944cdef10aa7228820f0afe5e4b43242496574
7
+ data.tar.gz: a5f83ed8ee3ded14d79180e49a78d04fcb9b373620860831fc8ab7d807c960b35ec565cdc4c8aaaa446671174b7208a93d2bf2060091c875ed095347dda471cc
data/.gitignore CHANGED
@@ -1 +1,3 @@
1
1
  Gemfile.lock
2
+ *.gem
3
+ tmp/*
data/Gemfile CHANGED
@@ -3,6 +3,7 @@ source 'https://rubygems.org'
3
3
  gemspec
4
4
 
5
5
  group :test, :development do
6
+ gem 'ruby-prof'
6
7
  gem 'rspec'
7
8
  gem 'pry'
8
9
  gem 'rerun'
data/README.md CHANGED
@@ -1,6 +1,192 @@
1
1
  # LittleBoxes
2
2
 
3
- Dependancy injection framework in Ruby.
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
@@ -8,3 +8,7 @@ desc 'Run all specs'
8
8
  RSpec::Core::RakeTask.new('spec') do |spec|
9
9
  spec.rspec_opts = %w{}
10
10
  end
11
+
12
+
13
+ require 'little_boxes'
14
+ Dir.glob(LittleBoxes.root_path + 'tasks/*').each { |p| load p }
@@ -1,7 +1,13 @@
1
1
  module LittleBoxes
2
- require 'little_boxes/registry'
3
- require 'little_boxes/dependant_registry'
4
- Dir.glob(File.dirname(__FILE__) + '/little_boxes/*').each{|p| require p }
2
+ def self.root_path
3
+ Pathname.new(__FILE__) + '../..'
4
+ end
5
5
 
6
- class MissingDependency < RuntimeError; end
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
@@ -1,19 +1,120 @@
1
1
  module LittleBoxes
2
- class Box
3
- include Registry
2
+ module Box
3
+ module ClassMethods
4
+ def entry_definitions
5
+ @entry_definitions ||= {}
6
+ end
4
7
 
5
- def get
6
- self
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
- def customize name, &block
10
- ForwardingDsl.run self[name], &block
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 box name, &block
14
- s = self.class.new parent: self, name: name
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