jerry 1.0.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/jerry.rb CHANGED
@@ -1,53 +1,41 @@
1
1
  require 'jerry/version'
2
+ require 'jerry/errors'
2
3
  require 'jerry/config'
3
4
 
4
5
  # Inversion of Control container.
5
6
  #
6
- # This class is in charge of bootstrapping your application. This is done by defining {Jerry::Config configs}.
7
+ # This class is in charge of bootstrapping your application. This is done by
8
+ # defining {Jerry::Config configs}.
7
9
  #
8
- # @example
9
- # class MyConfig < Jerry::Config
10
- # component(:app) { MyApp.new }
10
+ # @example Basic usage
11
+ # class FooConfig < Jerry::Config
12
+ # # ...
11
13
  # end
12
- # jerry = Jerry.new MyConfig.new
13
- # jerry.rig :app #=> #<MyApp:...>
14
+ #
15
+ # class BarConfig < Jerry::Config
16
+ # # ...
17
+ # end
18
+ #
19
+ # jerry = Jerry.new FooConfig.new, BarConfig.new
20
+ # jerry[SomeClass] #=> #<Someclass:...>
14
21
  class Jerry
15
- # Indicated that an error occurred while rigging a component
16
- class RigError < StandardError; end
17
-
18
- # @param [Jerry::Config] configs Configs used to rig components. Multiple config can be given. If two configs
19
- # define the same component, the later config will have priority.
22
+ # @param configs [Array<Jerry::Config>] configurations describing how to wire
23
+ # your application
20
24
  def initialize(*configs)
21
- @index = {}
22
-
23
- configs.each { |config| self << config }
24
- end
25
-
26
- # Load a config
27
- #
28
- # @param [Jerry::Config] config Config to be loaded. If the loaded config defines a component already defined
29
- # by another config, the component from the new config will take priority.
30
- def <<(config)
31
- components = config.components
32
- components.each { |component| @index[component] = config }
33
- config.jerry = self
34
- end
35
-
36
- # Rigs a component
37
- #
38
- # @param [Symbol] component Component to rig.
39
- # @return The component requested
40
- # @raise [Jerry::RigError] when the requested component does not exist
41
- def rig(component)
42
- raise RigError, "could not find component #{component}" unless knows? component
25
+ configs.each { |conf| conf.jerry = self }
43
26
 
44
- @index[component].public_send component
27
+ @configs = configs
45
28
  end
46
29
 
47
- # Checks if a component exists
48
- #
49
- # @param [Symbol] component component to check
50
- def knows?(component)
51
- @index.has_key? component
30
+ # @param key what to provide
31
+ # @return an insance of the sepcified key provided by one of the configs
32
+ # @raise [Jerry::InstantiationError] if can't instanciate key
33
+ def [](key)
34
+ config = @configs.find { |conf| conf.knows? key }
35
+ if config
36
+ config[key]
37
+ else
38
+ fail Jerry::InstantiationError, "Can't find #{key} in any config"
39
+ end
52
40
  end
53
41
  end
@@ -0,0 +1,109 @@
1
+ require 'jerry/class_provider'
2
+
3
+ describe Jerry::ClassProvider do
4
+ let(:jerry) { double 'jerry' }
5
+ let(:config) { double 'config' }
6
+ let(:klass) { dummy_class }
7
+
8
+ def dummy_class
9
+ Class.new do
10
+ define_method(:initialize) { |*| }
11
+ end
12
+ end
13
+
14
+ describe '#call' do
15
+ it 'should return an instance of the class' do
16
+ provider = Jerry::ClassProvider.new klass, []
17
+ instance = double 'instance'
18
+ allow(klass).to receive(:new).and_return(instance)
19
+
20
+ expect(provider.call jerry, config).to eq instance
21
+ end
22
+
23
+ it 'should pass constructor arguments in the right order' do
24
+ provider = Jerry::ClassProvider.new klass, [
25
+ proc { 'fi' }, proc { 'fo' }, proc { 'fum' }]
26
+
27
+ expect(klass).to receive(:new).with('fi', 'fo', 'fum')
28
+
29
+ provider.call jerry, config
30
+ end
31
+ end
32
+
33
+ describe 'with a class argument' do
34
+ it 'should get the instance from jerry' do
35
+ arg_klass = dummy_class
36
+ provider = Jerry::ClassProvider.new klass, [arg_klass]
37
+
38
+ expect(jerry).to receive(:[]).with(arg_klass)
39
+
40
+ provider.call jerry, config
41
+ end
42
+
43
+ it 'should pass the instance from jerry to the constructor' do
44
+ provider = Jerry::ClassProvider.new klass, [dummy_class]
45
+ instance = double 'instance'
46
+ allow(jerry).to receive(:[]).and_return(instance)
47
+
48
+ expect(klass).to receive(:new).with(instance)
49
+
50
+ provider.call jerry, config
51
+ end
52
+ end
53
+
54
+ describe 'with a symbol argument' do
55
+ it 'should get the instance from jerry' do
56
+ provider = Jerry::ClassProvider.new klass, [:foobar]
57
+
58
+ expect(jerry).to receive(:[]).with(:foobar)
59
+
60
+ provider.call jerry, config
61
+ end
62
+
63
+ it 'should pass the instance from jerry to the constructor' do
64
+ provider = Jerry::ClassProvider.new klass, [:foobar]
65
+ instance = double 'instance'
66
+ allow(jerry).to receive(:[]).with(:foobar).and_return(instance)
67
+
68
+ expect(klass).to receive(:new).with(instance)
69
+
70
+ provider.call jerry, config
71
+ end
72
+ end
73
+
74
+ describe 'with callable argument' do
75
+ it 'should call the callable' do
76
+ expect do |callable|
77
+ provider = Jerry::ClassProvider.new klass, [callable.to_proc]
78
+ provider.call jerry, config
79
+ end.to yield_control
80
+ end
81
+
82
+ it 'should pass the result of the callable to the constructor' do
83
+ instance = double 'instance'
84
+ provider = Jerry::ClassProvider.new klass, [proc { instance }]
85
+
86
+ expect(klass).to receive(:new).with(instance)
87
+
88
+ provider.call jerry, config
89
+ end
90
+
91
+ it 'should pass the jerry and config instance to the proc' do
92
+ expect do |callable|
93
+ provider = Jerry::ClassProvider.new klass, [callable.to_proc]
94
+ provider.call jerry, config
95
+ end.to yield_with_args(jerry, config)
96
+ end
97
+
98
+ it 'should call the proc in the context of the config' do
99
+ config = double 'config'
100
+ config.instance_eval { @stuff = 'something private' }
101
+ callable = proc { @stuff }
102
+ provider = Jerry::ClassProvider.new klass, [callable]
103
+
104
+ expect(klass).to receive(:new).with('something private')
105
+
106
+ provider.call jerry, config
107
+ end
108
+ end
109
+ end
data/spec/config_spec.rb CHANGED
@@ -1,172 +1,192 @@
1
- require 'rspec'
2
1
  require 'jerry/config'
2
+ require 'fixtures/house'
3
3
 
4
4
  describe Jerry::Config do
5
- let(:klass) {Class.new(Jerry::Config)}
5
+ let(:config_klass) { Class.new Jerry::Config }
6
+ let(:config) { config_klass.new }
7
+ let(:jerry) { double 'jerry' }
6
8
 
7
- describe 'subclass' do
8
- it 'should respond to all inherited class methods' do
9
- expect(klass).to respond_to :components
10
- expect(klass).to respond_to :component
11
- end
9
+ before do
10
+ config.jerry = jerry
12
11
  end
13
12
 
14
- describe '#components' do
15
- it 'should call the class contents method' do
16
- instance = klass.new
17
-
18
- expect(klass).to receive(:components).and_return([:something])
13
+ describe '::bind' do
14
+ it 'should create a class provider' do
15
+ klass = double 'class'
16
+ args = [Class.new, :foobar, -> { 'stuff' }]
17
+ expect(Jerry::ClassProvider).to receive(:new).with(klass, args)
19
18
 
20
- expect(instance.components).to eq([:something])
19
+ Class.new Jerry::Config do
20
+ bind klass, args
21
+ end
21
22
  end
22
- end
23
23
 
24
- describe '#rig' do
25
- it 'should call rig on the previously set jerry' do
26
- jerry = double('jerry')
27
- config = klass.new
28
- config.jerry = jerry
24
+ it 'should default ctor_args to empty array' do
25
+ expect(Jerry::ClassProvider).to receive(:new).with(anything, [])
26
+
27
+ Class.new Jerry::Config do
28
+ bind Class.new
29
+ end
30
+ end
29
31
 
30
- expect(jerry).to receive(:rig).with(:target)
32
+ it 'should use the class to identify the provider' do
33
+ klass = Class.new
34
+ config = Class.new Jerry::Config do
35
+ bind klass
36
+ end
31
37
 
32
- config.instance_eval { rig :target }
38
+ expect(config.providers).to have_key klass
33
39
  end
34
40
 
35
- it 'should return the result from the jerry rig method' do
36
- jerry = double('jerry')
37
- allow(jerry).to receive(:rig).with(anything).and_return 42
38
- config = klass.new
39
- config.jerry = jerry
41
+ it 'should return the class' do
42
+ klass = Class.new
43
+ thing = nil
40
44
 
41
- expect(config.instance_eval { rig :something }).to eq(42)
45
+ Class.new Jerry::Config do
46
+ thing = bind klass
47
+ end
48
+
49
+ expect(thing).to eq klass
42
50
  end
43
51
  end
44
52
 
45
- describe '#knows?' do
46
- it 'should call the knows? method on the previously set jerry' do
47
- jerry = double('jerry')
48
- config = klass.new
49
- config.jerry = jerry
53
+ describe '::named_bind' do
54
+ it 'should create a class provider' do
55
+ klass = double 'class'
56
+ args = [Class.new, :foobar, proc { 'stuff' }]
57
+ expect(Jerry::ClassProvider).to receive(:new).with(klass, args)
50
58
 
51
- expect(jerry).to receive(:knows?).with(:target)
59
+ Class.new Jerry::Config do
60
+ named_bind :stuff, klass, args
61
+ end
62
+ end
52
63
 
53
- config.instance_eval { knows? :target }
64
+ it 'should default ctor args to emtpy array' do
65
+ expect(Jerry::ClassProvider).to receive(:new).with(anything, [])
66
+
67
+ Class.new Jerry::Config do
68
+ named_bind :thing, Class.new
69
+ end
54
70
  end
55
71
 
56
- it 'should return the results from the jerry knows? method' do
57
- jerry = double('jerry')
58
- allow(jerry).to receive(:knows?).with(anything).and_return true
59
- config = klass.new
60
- config.jerry = jerry
72
+ it 'should use the given key to store the provider' do
73
+ config = Class.new Jerry::Config do
74
+ named_bind :the_name, Class.new
75
+ end
61
76
 
62
- expect(config.instance_eval { knows? :something }).to be_truthy
77
+ expect(config.providers).to have_key :the_name
63
78
  end
64
- end
65
79
 
66
- describe 'class methods' do
67
- describe '#component' do
68
- it 'should create a method with the given name' do
69
- klass.component(:my_component) {}
80
+ it 'should return the key' do
81
+ thing = nil
70
82
 
71
- expect(klass.new).to respond_to(:my_component)
83
+ Class.new Jerry::Config do
84
+ thing = named_bind :foobar, Class.new
72
85
  end
73
86
 
74
- it 'should add the name to the list of components' do
75
- klass.component(:target) {}
87
+ expect(thing).to eq :foobar
88
+ end
89
+ end
76
90
 
77
- expect(klass.components).to include(:target)
91
+ describe '::singleton' do
92
+ it 'should always use the same instance' do
93
+ config = Class.new Jerry::Config do
94
+ singleton named_bind :thing, Class.new
78
95
  end
79
96
 
80
- it 'should raise an error when scope is unknown' do
81
- expect{klass.component(:target, scope: :not_a_thing) {}}.to raise_error(Jerry::ComponentError)
97
+ jerry = Jerry.new config.new
98
+ thing1 = jerry[:thing]
99
+ thing2 = jerry[:thing]
100
+
101
+ expect(thing1).to equal thing2
102
+ end
103
+
104
+ it 'should only instantiate the class once' do
105
+ klass = double 'class', new: double('instance')
106
+ config = Class.new Jerry::Config do
107
+ singleton bind klass
82
108
  end
83
109
 
84
- it 'should raise an error when block is missing' do
85
- expect{klass.component :target}.to raise_error(Jerry::ComponentError)
110
+ expect(klass).to receive(:new).once
111
+
112
+ jerry = Jerry.new config.new
113
+ 2.times { jerry[klass] }
114
+ end
115
+ end
116
+
117
+ describe '#[]' do
118
+ it 'should get the provider instance from ::bind' do
119
+ instance = double 'instance'
120
+ klass = double 'class', new: instance
121
+
122
+ config_klass.class_eval do
123
+ bind klass
86
124
  end
87
125
 
88
- describe 'defined method' do
89
- it 'should have the right self set' do
90
- klass.component(:_self) { self }
91
- instance = klass.new
92
-
93
- expect(instance._self).not_to be_a Class
94
- expect(instance._self).to be_a klass
95
- expect(instance._self).to eq(instance)
96
- end
97
-
98
- context 'with scope set to :single' do
99
- it 'should only call the block once' do
100
- call_count = 0
101
- klass.component(:target, scope: :single) {call_count += 1}
102
- instance = klass.new
103
-
104
- 3.times {instance.target}
105
-
106
- expect(call_count).to eq(1)
107
- end
108
-
109
- it 'should keep returning the first created component' do
110
- call_count = 0
111
- klass.component(:target, scope: :single) {call_count += 1}
112
- instance = klass.new
113
-
114
- expect(instance.target).to eq(1)
115
- expect(instance.target).to eq(1)
116
- expect(instance.target).to eq(1)
117
- end
118
- end
119
-
120
- context 'with scope set to :instance' do
121
- it 'should call the block every time' do
122
- call_count = 0
123
- klass.component(:target, scope: :instance) {call_count += 1}
124
- instance = klass.new
125
-
126
- 3.times {instance.target}
127
-
128
- expect(call_count).to eq(3)
129
- end
130
-
131
- it 'should return a new instance every time' do
132
- call_count = 0
133
- klass.component(:target, scope: :instance) {call_count += 1}
134
- instance = klass.new
135
-
136
- expect(instance.target).to eq(1)
137
- expect(instance.target).to eq(2)
138
- expect(instance.target).to eq(3)
139
- end
140
- end
141
-
142
- context 'with no scope' do
143
- it 'should only call the block once' do
144
- call_count = 0
145
- klass.component(:target) {call_count += 1}
146
- instance = klass.new
147
-
148
- 3.times {instance.target}
149
-
150
- expect(call_count).to eq(1)
151
- end
152
-
153
- it 'should keep returning the first created component' do
154
- call_count = 0
155
- klass.component(:target) {call_count += 1}
156
- instance = klass.new
157
-
158
- expect(instance.target).to eq(1)
159
- expect(instance.target).to eq(1)
160
- expect(instance.target).to eq(1)
161
- end
162
- end
126
+ expect(config[klass]).to eq instance
127
+ end
128
+
129
+ it 'should pass jerry to the provider' do
130
+ provider = double 'provider'
131
+ config_klass.send(:providers)[:foobar] = provider
132
+
133
+ expect(provider).to receive(:call).with(jerry, anything)
134
+
135
+ config[:foobar]
136
+ end
137
+
138
+ it 'should pass config instance to the provider' do
139
+ provider = double 'provider'
140
+ config_klass.providers[:something] = provider
141
+
142
+ expect(provider).to receive(:call).with(anything, config)
143
+
144
+ config[:something]
145
+ end
146
+
147
+ it 'should fail when provider is missing' do
148
+ expect { config[:not_there] }.to raise_error(Jerry::InstantiationError)
149
+ end
150
+
151
+ it 'should wrap errors from the provider' do
152
+ provider = double 'provider'
153
+ allow(provider).to receive(:call)
154
+ .and_raise(RuntimeError, 'something blew up')
155
+ config_klass.providers[:failing] = provider
156
+
157
+ expect { config[:failing] }
158
+ .to raise_error(Jerry::InstantiationError) do |e|
159
+ expect(e.cause.message).to eq 'something blew up'
163
160
  end
164
161
  end
165
162
 
166
- describe '#components' do
167
- it 'should default to an empty array' do
168
- expect(klass.components).to eq([])
163
+ it 'should set self to config instance for all procs in spec' do
164
+ klass = Class.new do
165
+ attr_reader :thing
166
+ define_method(:initialize) { |thing| @thing = thing }
167
+ end
168
+ config_klass = Class.new(Jerry::Config) do
169
+ define_method(:initialize) { @foobar = 'something private' }
170
+ bind klass, [proc { @foobar }]
169
171
  end
172
+ config = config_klass.new
173
+ config.jerry = jerry
174
+
175
+ instance = config[klass]
176
+ expect(instance.thing).to eq 'something private'
177
+ end
178
+ end
179
+
180
+ describe '#knows?' do
181
+ it 'should be true if key is in providers' do
182
+ key = double 'some key'
183
+ config_klass.providers[key] = double 'some provider'
184
+
185
+ expect(config.knows? key).to be true
186
+ end
187
+
188
+ it 'should be false if key is not known' do
189
+ expect(config.knows? double('some unknown key')).to be false
170
190
  end
171
191
  end
172
- end
192
+ end
@@ -0,0 +1,23 @@
1
+ describe Jerry::Error do
2
+ def raise_error_with_cause
3
+ fail 'Something went wrong'
4
+ rescue RuntimeError
5
+ raise Jerry::Error, 'wrapping error'
6
+ end
7
+
8
+ it 'should record the causing exception when there is one' do
9
+ expect { raise_error_with_cause }.to raise_error(Jerry::Error) do |ex|
10
+ expect(ex.cause.message).to eq 'Something went wrong'
11
+ end
12
+ end
13
+
14
+ it 'should not record the causing exception when there is none' do
15
+ expect { fail Jerry::Error }.to raise_error(Jerry::Error) do |ex|
16
+ expect(ex.cause).to be_nil
17
+ end
18
+ end
19
+
20
+ it 'should have a message' do
21
+ expect { fail Jerry::Error, ':(' }.to raise_error(Jerry::Error, ':(')
22
+ end
23
+ end
@@ -0,0 +1,120 @@
1
+ require 'jerry'
2
+ require 'fixtures/house'
3
+ require 'fixtures/db_app'
4
+ require 'fixtures/shopping_cart'
5
+ require 'fixtures/shopping_cart_config'
6
+ require 'fixtures/multi_db_app'
7
+
8
+ describe Jerry do
9
+ it 'should wire dependencies' do
10
+ HousingModule = Class.new(Jerry::Config) do
11
+ bind House, [Door, Window]
12
+ bind Window, []
13
+ bind Door, []
14
+ end
15
+
16
+ jerry = Jerry.new HousingModule.new
17
+
18
+ house = jerry[House]
19
+
20
+ expect(house).to be_a House
21
+ expect(house.window).to be_a Window
22
+ expect(house.door).to be_a Door
23
+ end
24
+
25
+ it 'should allow the use of procs to wire settings' do
26
+ AppConfig = Class.new Jerry::Config do
27
+ def initialize(database_uri)
28
+ @database_uri = database_uri
29
+ end
30
+
31
+ bind DbApplication, [Database]
32
+ bind Database, [proc { @database_uri }]
33
+ end
34
+
35
+ jerry = Jerry.new AppConfig.new('mongodb://localhost:27017')
36
+ app = jerry[DbApplication]
37
+
38
+ expect(app).to be_a DbApplication
39
+ expect(app.db).to be_a Database
40
+ expect(app.db.uri).to eq 'mongodb://localhost:27017'
41
+ end
42
+
43
+ it 'should support multiple configs' do
44
+ jerry = Jerry.new(
45
+ ShoppingCart::DatabaseConfig.new,
46
+ ShoppingCart::ApplicationConfig.new,
47
+ ShoppingCart::UserConfig.new,
48
+ ShoppingCart::ProductConfig.new,
49
+ ShoppingCart::ShoppingCartConfig.new
50
+ )
51
+
52
+ app = jerry[ShoppingCart::Application]
53
+
54
+ expect(app).to be_a ShoppingCart::Application
55
+
56
+ expect(app.user_controller).to be_a ShoppingCart::UserController
57
+ expect(app.product_controller).to be_a ShoppingCart::ProductController
58
+ expect(app.shopping_cart_controller).to \
59
+ be_a ShoppingCart::ShoppingCartController
60
+
61
+ expect(app.user_controller.user_service).to be_a ShoppingCart::UserService
62
+ expect(app.product_controller.product_service).to \
63
+ be_a ShoppingCart::ProductService
64
+ expect(app.shopping_cart_controller.shopping_cart_service).to \
65
+ be_a ShoppingCart::ShoppingCartService
66
+
67
+ expect(app.user_controller.user_service.db).to be_a ShoppingCart::Database
68
+ expect(app.product_controller.product_service.db).to \
69
+ be_a ShoppingCart::Database
70
+ expect(app.shopping_cart_controller.shopping_cart_service.db).to \
71
+ be_a ShoppingCart::Database
72
+
73
+ shopping_cart_service = app.shopping_cart_controller.shopping_cart_service
74
+ expect(shopping_cart_service.user_service).to be_a ShoppingCart::UserService
75
+ expect(shopping_cart_service.product_service).to \
76
+ be_a ShoppingCart::ProductService
77
+ end
78
+
79
+ it 'should support wiring one class in multiple ways through naming' do
80
+ jerry = Jerry.new MultiDbApp::Config.new('db://foo', 'db://bar')
81
+
82
+ app = jerry[MultiDbApp::Application]
83
+
84
+ expect(app.foo_db.uri).to eq 'db://foo'
85
+ expect(app.bar_db.uri).to eq 'db://bar'
86
+ end
87
+
88
+ it 'should wire the same class multiple times with multiple names' do
89
+ klass = Class.new do
90
+ attr_reader :str
91
+ define_method(:initialize) { |str| @str = str }
92
+ end
93
+ config = Class.new Jerry::Config do
94
+ named_bind :foo, klass, [proc { 'foo' }]
95
+ named_bind :bar, klass, [proc { 'bar' }]
96
+ end
97
+ jerry = Jerry.new config.new
98
+
99
+ foo = jerry[:foo]
100
+ bar = jerry[:bar]
101
+
102
+ expect(foo).to be_a klass
103
+ expect(foo.str).to eq 'foo'
104
+ expect(bar).to be_a klass
105
+ expect(bar.str).to eq 'bar'
106
+ end
107
+
108
+ it 'should always wire the same instance when using singleton' do
109
+ klass = Class.new
110
+ config = Class.new Jerry::Config do
111
+ singleton bind klass
112
+ end
113
+ jerry = Jerry.new config.new
114
+
115
+ alfa = jerry[klass]
116
+ bravo = jerry[klass]
117
+
118
+ expect(alfa).to equal bravo
119
+ end
120
+ end
@@ -0,0 +1,17 @@
1
+ # A database connector that connects to a given uri
2
+ class Database
3
+ attr_reader :uri
4
+
5
+ def initialize(uri)
6
+ @uri = uri
7
+ end
8
+ end
9
+
10
+ # An application that uses a database
11
+ class DbApplication
12
+ attr_reader :db
13
+
14
+ def initialize(db)
15
+ @db = db
16
+ end
17
+ end