callme 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,36 @@
1
+ # Extend object with the bean injection mechanism
2
+ # Example of usage:
3
+ # class Bar
4
+ # end
5
+ #
6
+ # class Foo
7
+ # inject :bar
8
+ # or:
9
+ # inject :some_bar, ref: bar
10
+ # end
11
+ #
12
+ # ioc_container[:foo].bar == ioc_container[:bar]
13
+ class Object
14
+
15
+ class << self
16
+
17
+ def inject(dependency_name, options = {})
18
+ unless dependency_name.is_a?(Symbol)
19
+ raise ArgumentError, "dependency name should be a symbol"
20
+ end
21
+ unless options.is_a?(Hash)
22
+ raise ArgumentError, "second argument for inject method should be a Hash"
23
+ end
24
+ unless respond_to?(:_callme_injectable_attrs)
25
+ class_attribute :_callme_injectable_attrs
26
+ self._callme_injectable_attrs = { dependency_name => options.dup }
27
+ else
28
+ self._callme_injectable_attrs =
29
+ self._callme_injectable_attrs.merge(dependency_name => options.dup)
30
+ end
31
+ attr_accessor dependency_name
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,25 @@
1
+ # Prototype scope instantiates new bean instance
2
+ # on each +get_bean+ call
3
+ class Callme::Scopes::PrototypeScope
4
+
5
+ # Constructon
6
+ # @param bean_factory bean factory
7
+ def initialize(bean_factory)
8
+ @bean_factory = bean_factory
9
+ end
10
+
11
+ # Get new bean instance
12
+ # @param bean_metadata [BeanMetadata] bean metadata
13
+ # @returns bean instance
14
+ def get_bean(bean_metadata)
15
+ @bean_factory.create_bean_and_save(bean_metadata, {})
16
+ end
17
+
18
+ # Delete bean from scope,
19
+ # because Prototype scope doesn't store bean
20
+ # then do nothing here
21
+ #
22
+ # @param bean_metadata [BeanMetadata] bean metadata
23
+ def delete_bean(bean_metadata)
24
+ end
25
+ end
@@ -0,0 +1,33 @@
1
+ require 'request_store'
2
+
3
+ # Request scope instantiates new bean instance
4
+ # on each new HTTP request
5
+ class Callme::Scopes::RequestScope
6
+
7
+ # Constructon
8
+ # @param bean_factory bean factory
9
+ def initialize(bean_factory)
10
+ @bean_factory = bean_factory
11
+ end
12
+
13
+ # Returns a bean from the +RequestStore+
14
+ # RequestStore is a wrapper for Thread.current
15
+ # which clears it on each new HTTP request
16
+ #
17
+ # @param bean_metadata [BeanMetadata] bean metadata
18
+ # @returns bean instance
19
+ def get_bean(bean_metadata)
20
+ RequestStore.store[:_callme_beans] ||= {}
21
+ if bean = RequestStore.store[:_callme_beans][bean_metadata.name]
22
+ bean
23
+ else
24
+ @bean_factory.create_bean_and_save(bean_metadata, RequestStore.store[:_callme_beans])
25
+ end
26
+ end
27
+
28
+ # Delete bean from scope
29
+ # @param bean_metadata [BeanMetadata] bean metadata
30
+ def delete_bean(bean_metadata)
31
+ RequestStore.store[:_callme_beans].delete(bean_metadata.name)
32
+ end
33
+ end
@@ -0,0 +1,29 @@
1
+ # Singleton scope returns the same bean instance
2
+ # on each call
3
+ class Callme::Scopes::SingletonScope
4
+
5
+ # Constructon
6
+ # @param bean_factory bean factory
7
+ def initialize(bean_factory)
8
+ @beans = {}
9
+ @bean_factory = bean_factory
10
+ end
11
+
12
+ # Returns the same bean instance
13
+ # on each call
14
+ # @param bean_metadata [BeanMetadata] bean metadata
15
+ # @returns bean instance
16
+ def get_bean(bean_metadata)
17
+ if bean = @beans[bean_metadata.name]
18
+ bean
19
+ else
20
+ @bean_factory.create_bean_and_save(bean_metadata, @beans)
21
+ end
22
+ end
23
+
24
+ # Delete bean from scope
25
+ # @param bean_metadata [BeanMetadata] bean metadata
26
+ def delete_bean(bean_metadata)
27
+ @beans.delete(bean_metadata.name)
28
+ end
29
+ end
@@ -0,0 +1,2 @@
1
+ module Callme::Scopes
2
+ end
@@ -0,0 +1,3 @@
1
+ module Callme
2
+ VERSION = "0.5.0"
3
+ end
data/lib/callme.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'ext/vendored_activesupport'
2
+ require 'callme/version'
3
+ require 'callme/inject'
4
+ require 'callme/container'
@@ -0,0 +1,181 @@
1
+ ##### vendored code from active_support (5.0.0) for Class.class_attribute ####
2
+ # require 'active_support/core_ext/class/attribute'
3
+ # require 'active_support/core_ext/kernel/singleton_class'
4
+ # require 'active_support/core_ext/module/remove_method'
5
+ # require 'active_support/core_ext/array/extract_options'
6
+
7
+ unless defined?(ActiveSupport)
8
+ class Module
9
+ def remove_possible_method(method)
10
+ if method_defined?(method) || private_method_defined?(method)
11
+ undef_method(method)
12
+ end
13
+ end
14
+
15
+ def redefine_method(method, &block)
16
+ remove_possible_method(method)
17
+ define_method(method, &block)
18
+ end
19
+ end
20
+
21
+ module Kernel
22
+ # class_eval on an object acts like singleton_class.class_eval.
23
+ def class_eval(*args, &block)
24
+ singleton_class.class_eval(*args, &block)
25
+ end
26
+ end
27
+
28
+ class Hash
29
+ # By default, only instances of Hash itself are extractable.
30
+ # Subclasses of Hash may implement this method and return
31
+ # true to declare themselves as extractable. If a Hash
32
+ # is extractable, Array#extract_options! pops it from
33
+ # the Array when it is the last element of the Array.
34
+ def extractable_options?
35
+ instance_of?(Hash)
36
+ end
37
+ end
38
+
39
+ class Array
40
+ # Extracts options from a set of arguments. Removes and returns the last
41
+ # element in the array if it's a hash, otherwise returns a blank hash.
42
+ #
43
+ # def options(*args)
44
+ # args.extract_options!
45
+ # end
46
+ #
47
+ # options(1, 2) # => {}
48
+ # options(1, 2, a: :b) # => {:a=>:b}
49
+ def extract_options!
50
+ if last.is_a?(Hash) && last.extractable_options?
51
+ pop
52
+ else
53
+ {}
54
+ end
55
+ end
56
+ end
57
+
58
+ class Class
59
+ # Declare a class-level attribute whose value is inheritable by subclasses.
60
+ # Subclasses can change their own value and it will not impact parent class.
61
+ #
62
+ # class Base
63
+ # class_attribute :setting
64
+ # end
65
+ #
66
+ # class Subclass < Base
67
+ # end
68
+ #
69
+ # Base.setting = true
70
+ # Subclass.setting # => true
71
+ # Subclass.setting = false
72
+ # Subclass.setting # => false
73
+ # Base.setting # => true
74
+ #
75
+ # In the above case as long as Subclass does not assign a value to setting
76
+ # by performing <tt>Subclass.setting = _something_ </tt>, <tt>Subclass.setting</tt>
77
+ # would read value assigned to parent class. Once Subclass assigns a value then
78
+ # the value assigned by Subclass would be returned.
79
+ #
80
+ # This matches normal Ruby method inheritance: think of writing an attribute
81
+ # on a subclass as overriding the reader method. However, you need to be aware
82
+ # when using +class_attribute+ with mutable structures as +Array+ or +Hash+.
83
+ # In such cases, you don't want to do changes in places but use setters:
84
+ #
85
+ # Base.setting = []
86
+ # Base.setting # => []
87
+ # Subclass.setting # => []
88
+ #
89
+ # # Appending in child changes both parent and child because it is the same object:
90
+ # Subclass.setting << :foo
91
+ # Base.setting # => [:foo]
92
+ # Subclass.setting # => [:foo]
93
+ #
94
+ # # Use setters to not propagate changes:
95
+ # Base.setting = []
96
+ # Subclass.setting += [:foo]
97
+ # Base.setting # => []
98
+ # Subclass.setting # => [:foo]
99
+ #
100
+ # For convenience, an instance predicate method is defined as well.
101
+ # To skip it, pass <tt>instance_predicate: false</tt>.
102
+ #
103
+ # Subclass.setting? # => false
104
+ #
105
+ # Instances may overwrite the class value in the same way:
106
+ #
107
+ # Base.setting = true
108
+ # object = Base.new
109
+ # object.setting # => true
110
+ # object.setting = false
111
+ # object.setting # => false
112
+ # Base.setting # => true
113
+ #
114
+ # To opt out of the instance reader method, pass <tt>instance_reader: false</tt>.
115
+ #
116
+ # object.setting # => NoMethodError
117
+ # object.setting? # => NoMethodError
118
+ #
119
+ # To opt out of the instance writer method, pass <tt>instance_writer: false</tt>.
120
+ #
121
+ # object.setting = false # => NoMethodError
122
+ #
123
+ # To opt out of both instance methods, pass <tt>instance_accessor: false</tt>.
124
+ def class_attribute(*attrs)
125
+ options = attrs.extract_options!
126
+ instance_reader = options.fetch(:instance_accessor, true) && options.fetch(:instance_reader, true)
127
+ instance_writer = options.fetch(:instance_accessor, true) && options.fetch(:instance_writer, true)
128
+ instance_predicate = options.fetch(:instance_predicate, true)
129
+
130
+ attrs.each do |name|
131
+ define_singleton_method(name) { nil }
132
+ define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate
133
+
134
+ ivar = "@#{name}"
135
+
136
+ define_singleton_method("#{name}=") do |val|
137
+ singleton_class.class_eval do
138
+ remove_possible_method(name)
139
+ define_method(name) { val }
140
+ end
141
+
142
+ if singleton_class?
143
+ class_eval do
144
+ remove_possible_method(name)
145
+ define_method(name) do
146
+ if instance_variable_defined? ivar
147
+ instance_variable_get ivar
148
+ else
149
+ singleton_class.send name
150
+ end
151
+ end
152
+ end
153
+ end
154
+ val
155
+ end
156
+
157
+ if instance_reader
158
+ remove_possible_method name
159
+ define_method(name) do
160
+ if instance_variable_defined?(ivar)
161
+ instance_variable_get ivar
162
+ else
163
+ self.class.public_send name
164
+ end
165
+ end
166
+ define_method("#{name}?") { !!public_send(name) } if instance_predicate
167
+ end
168
+
169
+ attr_writer name if instance_writer
170
+ end
171
+ end
172
+
173
+ private
174
+
175
+ unless respond_to?(:singleton_class?)
176
+ def singleton_class?
177
+ ancestors.first != self
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,235 @@
1
+ require 'spec_helper'
2
+ require 'callme'
3
+
4
+ describe Callme::Container do
5
+
6
+ class Logger
7
+ attr_accessor :appender
8
+ end
9
+ class Appender
10
+ end
11
+ class Printer
12
+ end
13
+
14
+ describe "bean definitions" do
15
+ let(:container) do
16
+ container = Callme::Container.new
17
+ container.bean(:appender, class: Appender)
18
+ container.bean(:logger, class: Logger) do
19
+ attr :appender, ref: :appender
20
+ end
21
+ container.bean(:printer, class: Printer, instance: false)
22
+ container
23
+ end
24
+ it "should instanciate bean and it's dependencies" do
25
+ container[:logger].should be_a(Logger)
26
+ container[:logger].appender.should be_a(Appender)
27
+ container[:printer].should be(Printer)
28
+ end
29
+
30
+ it "container should return the same instance on each call" do
31
+ logger = container[:logger]
32
+ container[:logger].should == logger
33
+ end
34
+ end
35
+
36
+ describe "eager_load_bean_classes" do
37
+ let(:container) do
38
+ container = Callme::Container.new
39
+ container.bean(:appender, class: 'Appender')
40
+ container.bean(:logger, class: 'Logger') do
41
+ attr :appender, ref: :appender
42
+ end
43
+ container.bean(:printer, class: 'Printer', instance: false)
44
+ container
45
+ end
46
+
47
+ it "should eager load bean classes" do
48
+ container.eager_load_bean_classes
49
+ end
50
+ end
51
+
52
+
53
+ describe "#replace_bean" do
54
+ it "should replace bean definition" do
55
+ container = Callme::Container.new
56
+ container.bean(:appender, class: Appender)
57
+ container[:appender].should be_a(Appender)
58
+
59
+ container.replace_bean(:appender, class: Logger)
60
+ container[:appender].should be_a(Logger)
61
+ end
62
+ end
63
+
64
+ describe "passing bean definitions to container constructor" do
65
+ let(:resource) do
66
+ Proc.new do |c|
67
+ c.bean(:appender, class: 'Appender')
68
+ c.bean(:logger, class: Logger) do
69
+ attr :appender, ref: :appender
70
+ end
71
+ end
72
+ end
73
+
74
+ it "should instanciate given bean definitions" do
75
+ container = Callme::Container.new_with_beans([resource])
76
+ container[:logger].should be_a(Logger)
77
+ container[:appender].should be_a(Appender)
78
+ end
79
+
80
+ end
81
+
82
+ describe "inheritance" do
83
+ class Form
84
+ inject :validator
85
+ end
86
+
87
+ class Circle < Form
88
+ inject :circle_validator
89
+ end
90
+ class Rectangle < Form
91
+ inject :rectangle_validator
92
+ end
93
+
94
+ class Validator
95
+ end
96
+ class CircleValidator
97
+ end
98
+ class RectangleValidator
99
+ end
100
+
101
+ let(:container) do
102
+ Callme::Container.new do |c|
103
+ c.bean(:circle, class: Circle)
104
+ c.bean(:rectangle, class: Rectangle)
105
+ c.bean(:validator, class: Validator)
106
+ c.bean(:circle_validator, class: CircleValidator)
107
+ c.bean(:rectangle_validator, class: RectangleValidator)
108
+ end
109
+ end
110
+
111
+ it "dependencies in subclasses shouldn't affect on each other" do
112
+ container[:circle].circle_validator.should be_a(CircleValidator)
113
+ container[:rectangle].rectangle_validator.should be_a(RectangleValidator)
114
+ end
115
+ end
116
+
117
+ describe "bean scopes" do
118
+ class ContactsService
119
+ inject :contacts_repository
120
+ inject :contacts_validator
121
+ end
122
+ class ContactsRepository
123
+ end
124
+ class ContactsValidator
125
+ end
126
+
127
+ let(:container) do
128
+ container = Callme::Container.new
129
+ container.bean(:contacts_repository, class: ContactsRepository, scope: :request)
130
+ container.bean(:contacts_service, class: ContactsService, scope: :singleton)
131
+ container.bean(:contacts_validator, class: ContactsValidator, scope: :prototype)
132
+ container
133
+ end
134
+
135
+ it "should instanciate bean with :request scope on each request" do
136
+ first_repo = container[:contacts_service].contacts_repository
137
+ second_repo = container[:contacts_service].contacts_repository
138
+ first_repo.should == second_repo
139
+ RequestStore.clear! # new request
140
+ third_repo = container[:contacts_service].contacts_repository
141
+ first_repo.should_not == third_repo
142
+ end
143
+
144
+ it "should instanciate bean with :prototype scope on each call" do
145
+ first_validator = container[:contacts_service].contacts_validator
146
+ second_validator = container[:contacts_service].contacts_validator
147
+ first_validator.should_not == second_validator
148
+ end
149
+ end
150
+
151
+ describe "factory method" do
152
+ module Test
153
+ class Config
154
+ end
155
+ class ConfigsFactory
156
+ def load_config
157
+ Config.new
158
+ end
159
+ end
160
+ end
161
+
162
+ let(:container) do
163
+ Callme::Container.new do |c|
164
+ c.bean :config, class: Test::ConfigsFactory, factory_method: :load_config
165
+ end
166
+ end
167
+
168
+ it "should instantiate bean using factory method" do
169
+ container[:config].should be_instance_of(Test::Config)
170
+ end
171
+ end
172
+
173
+ describe "parent container" do
174
+ class ContactBook
175
+ inject :contacts_repository
176
+ inject :validator, ref: :contact_validator
177
+ end
178
+ class ContactBookService
179
+ inject :contacts_repository
180
+ inject :validator, ref: :contact_validator
181
+ end
182
+ class ContactsRepository
183
+ end
184
+ class ContactValidator
185
+ end
186
+ class TestContactValidator
187
+ end
188
+
189
+ class AnotherTestContactValidator
190
+ end
191
+
192
+
193
+ let(:parent){
194
+ Callme::Container.new do |c|
195
+ c.bean(:contacts_repository, class: ContactsRepository)
196
+ c.bean(:contact_validator, class: ContactValidator)
197
+ c.bean(:contact_book, class: ContactBook)
198
+ c.bean(:contact_book_service, class: "ContactBookService")
199
+ end
200
+ }
201
+
202
+ let(:container){
203
+ Callme::Container.with_parent(parent) do |c|
204
+ c.bean(:contact_validator, class: TestContactValidator)
205
+ end
206
+ }
207
+
208
+ it "works for direct beans" do
209
+ expect(container[:contact_validator]).to be_a(TestContactValidator)
210
+ expect(container[:contact_book_service].validator).to be_a(TestContactValidator)
211
+ end
212
+
213
+ it "works for in-direct dependencies" do
214
+ expect(container[:contact_book_service].validator).to be_a(TestContactValidator)
215
+ end
216
+
217
+ it "does not consider changes to parent" do
218
+ expect(parent[:contact_book_service].validator).to be_a(ContactValidator)
219
+ parent.replace_bean(:contact_validator, class: AnotherTestContactValidator)
220
+ expect(parent[:contact_validator]).to be_a(AnotherTestContactValidator)
221
+ parent.reset!
222
+ expect(parent[:contact_book_service].validator).to be_a(AnotherTestContactValidator)
223
+ expect(container[:contact_book_service].validator).to be_a(TestContactValidator)
224
+ end
225
+
226
+ it "changes in child container do not affect parent container" do
227
+ expect(parent[:contact_book_service].validator).to be_a(ContactValidator)
228
+ container.replace_bean(:contact_validator, class: AnotherTestContactValidator)
229
+ parent.reset!
230
+ container.reset!
231
+ expect(parent[:contact_validator]).to be_a(ContactValidator)
232
+ expect(container[:contact_validator]).to be_a(AnotherTestContactValidator)
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ # Ensures that :inject keyword works as it should
4
+ describe "Object.inject" do
5
+ class ContactBook
6
+ inject :contacts_repository
7
+ inject :validator, ref: :contact_validator
8
+ end
9
+ class ContactBookService
10
+ inject :contacts_repository
11
+ inject :validator, ref: :contact_validator
12
+ end
13
+ class ContactsRepository
14
+ end
15
+ class ContactValidator
16
+ end
17
+
18
+ let(:container) do
19
+ Callme::Container.new do |c|
20
+ c.bean(:contacts_repository, class: ContactsRepository)
21
+ c.bean(:contact_validator, class: ContactValidator)
22
+ c.bean(:contact_book, class: ContactBook)
23
+ c.bean(:contact_book_service, class: "ContactBookService")
24
+ end
25
+ end
26
+
27
+ it "should autowire dependencies" do
28
+ container[:contact_book].contacts_repository.should be_a(ContactsRepository)
29
+ container[:contact_book].validator.should be_a(ContactValidator)
30
+ end
31
+
32
+ it "should lazy autowire dependencies for string class names" do
33
+ container[:contact_book_service].contacts_repository.should be_a(ContactsRepository)
34
+ container[:contact_book_service].validator.should be_a(ContactValidator)
35
+ end
36
+
37
+ it "should raise ArgumentError if non-symbol passed as dependency name" do
38
+ expect do
39
+ class SomeClass
40
+ inject 'bar'
41
+ end
42
+ end.to raise_error(ArgumentError, "dependency name should be a symbol")
43
+ end
44
+
45
+ it "inject should define instance variable" do
46
+ container[:contact_book].instance_variable_get(:@contacts_repository).should be_a(ContactsRepository)
47
+ end
48
+
49
+ it "inject should not define class variable" do
50
+ expect do
51
+ container[:contact_book].class.contacts_repository
52
+ end.to raise_error(NoMethodError)
53
+ end
54
+
55
+ end
@@ -0,0 +1,7 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'callme'
4
+
5
+ RSpec.configure do |config|
6
+ config.color_enabled = true
7
+ end