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 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