callme 0.5.0

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