dependency_injection 0.2.0 → 0.3.0

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: 2116ad6a0f9eb9fef58b268cd9413aa4fd8177c3
4
- data.tar.gz: ed1755da4b8037d366fc95c3693c86ac9b346b28
3
+ metadata.gz: e40f369e9fe6ece4538ced539176e74e00d0154e
4
+ data.tar.gz: 313501c69d9d7bc6dfa3e8e824c41ba22671a1fa
5
5
  SHA512:
6
- metadata.gz: ab60bdcc326a550b133d8406c459d072670af4f259a50eb5cc5c1c9edf33b671d6e544f1871e27b88e90027befeb0477c1c681e43a3f6de7a0ec52f2a913110a
7
- data.tar.gz: 770dcddd3fe38639736abae27599ef504934050f29b3766ca79efba18b5fbcdb0c5c56ffda141485f2d52fb6990060073ac657f61be6fbd7a440af6ff3c97223
6
+ metadata.gz: 200c2f71aa5c1e9c3cebfe50723f45d28a5a5e7a11c9ca3546c8a1b0fdc273ecee599e69f12b01e3c1e85e9b6c499e8624b065e42a5cfe7224c7bc2818f72de7
7
+ data.tar.gz: e84f7d457b800cad35a56ea24645b6fe7797c8265b5bec545f24ccd9a44e292540c3b18657e3626a5b049b8992deef7d81233b57f6a710deb0a1e7b6b6586b67
data/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # 0.3.0 (2013-09-16)
2
+
3
+ * Add prototype and container scope notion in service
4
+
5
+ # 0.2.0
6
+
7
+ * Add lazy loading option
8
+ * Add service aliasing option
9
+ * Add service configurator option
10
+
11
+ # 0.1.0
12
+
13
+ * Add YAML configuration
14
+ * Add global container parameters
15
+ * Add service as reference to another service
data/README.md CHANGED
@@ -1,6 +1,216 @@
1
1
  # Dependency Injection for Ruby
2
2
  [![Build Status](https://travis-ci.org/kdisneur/dependency_injection-ruby.png?branch=master)](https://travis-ci.org/kdisneur/dependency_injection-ruby) [![Coverage Status](https://coveralls.io/repos/kdisneur/dependency_injection-ruby/badge.png?branch=master)](https://coveralls.io/r/kdisneur/dependency_injection-ruby?branch=master) [![Code Climate](https://codeclimate.com/github/kdisneur/dependency_injection-ruby.png)](https://codeclimate.com/github/kdisneur/dependency_injection-ruby)
3
3
 
4
- It's a WIP for now.
4
+ ## Foreword
5
5
 
6
- You can find some examples in the [examples folder](https://github.com/kdisneur/dependency_injection-ruby/tree/master/examples)
6
+ This gem is heavily inspired from The Symfony Framework [Container Service](http://symfony.com/doc/current/book/service_container.html). Here's the description they give to explain the concept:
7
+
8
+ > It helps you instantiate, organize and retrieve the many objects of your application. This object, called a service container, will allow you to standardize and centralize the way objects are constructed in your application. The container makes your life easier, is super fast, and emphasizes an architecture that promotes reusable and decoupled code.
9
+
10
+ You can learn more about everything this gem does by looking at the [examples](https://github.com/kdisneur/dependency_injection-ruby/tree/master/examples) directory. See [Usage](#usage) for a detailed explanation.
11
+
12
+ ## Description
13
+
14
+ ### Installation
15
+
16
+ Just add the gem to your Gemfile:
17
+
18
+ ```ruby
19
+ gem 'dependency_injection'
20
+ ```
21
+
22
+ Or simply install it using rubygems:
23
+
24
+ ```shell
25
+ gem install dependency_injection
26
+ ```
27
+
28
+ ### Example
29
+
30
+ #### Without using Dependency Injection
31
+
32
+ In this example, we'll consider a simple application that needs to send emails for a newsletter.
33
+
34
+ We have the two following classes:
35
+
36
+ ```ruby
37
+ # mailer.rb
38
+ class Mailer
39
+ attr_accessor :transporter
40
+
41
+ def initialize
42
+ puts 'mailer initialized'
43
+ end
44
+
45
+ def send_mail(message, recipient)
46
+ puts "mail sent via #{self.transporter}: #{message}"
47
+ end
48
+ end
49
+ ```
50
+
51
+ ```ruby
52
+ # newsletter_manager.rb
53
+ class NewsletterManager
54
+ def initialize(mailer)
55
+ @mailer = mailer
56
+ end
57
+
58
+ def send_newsletter(message, recipients)
59
+ puts 'newsletter #{message} send to #{recipients}'
60
+ recipients.each { |recipient| @mailer.send_mail(message, recipient) }
61
+ end
62
+ end
63
+ ```
64
+
65
+ A `Mailer` class that handles email sending, through a given transporter, for a recipient.
66
+
67
+ A `NewsletterManager` class that sends a newsletter (message) to a list of recipients.
68
+
69
+ Without __DependencyInjection__, we would need to do the following to achieve our goal:
70
+
71
+ ```ruby
72
+ # send_newsletter.rb
73
+ mailer = Mailer.new
74
+ mailer.transporter = :smtp
75
+
76
+ recipients = %w(john@doe.com david@heinemeier-hansson.com)
77
+ message = 'I love dependency injection and think this is the future!'
78
+
79
+ newsletter_manager = NewsletterManager.new(mailer)
80
+ newsletter_manager.send_newsletter(message, recipients)
81
+ ```
82
+
83
+ You have a working application but this code is thightly coupled and might be, in a real life, hard to refactor.
84
+
85
+ Another big drawback is that you have to instantiate as many objects as you have emails and newsletters to send.
86
+
87
+ #### Now with Dependency Injection
88
+
89
+ Our two classes stay untouched, the only thing you have to do is to add a configuration file.
90
+
91
+ ```yaml
92
+ # services.yml
93
+ parameters:
94
+ mailer.transporter: 'smtp'
95
+ services:
96
+ mailer:
97
+ class: 'Mailer'
98
+ calls:
99
+ - ['transporter=', '%mailer.transporter%']
100
+ newsletter_manager:
101
+ class: 'NewsletterManager'
102
+ arguments:
103
+ - '@mailer'
104
+ ```
105
+
106
+ We now need to require __DependencyInjection__ and declare our __Container__.
107
+
108
+ Please note that the following code only needs to be declared once as long as the `container` value is accessible throughout your whole application.
109
+
110
+ ```ruby
111
+ # initialize.rb
112
+ require 'dependency_injection/container'
113
+ require 'dependency_injection/loaders/yaml'
114
+
115
+ container = DependencyInjection::Container.new
116
+ loader = DependencyInjection::Loaders::Yaml.new(container)
117
+ loader.load(File.join(File.dirname(File.expand_path(__FILE__)), 'services.yml'))
118
+ ```
119
+
120
+ We can now do the same as the previous example with the following.
121
+
122
+ ```ruby
123
+ # send_newsletter.rb
124
+ recipients = %w(john@doe.com david@heinemeier-hansson.com)
125
+ message = 'I love dependency injection and think this is the future!'
126
+
127
+ container.get('newsletter_manager').send_newsletter(message, recipients)
128
+ ```
129
+
130
+ Now your code is no longer tightly coupled and can be a lot more easily refactored. Moreover, the `Mailer` and `NewsletterManager` classes are only instantiated once during your application's lifecycle.
131
+
132
+ ### Usage
133
+
134
+ Before diving into the details of __DependencyInjection__, here are some keywords that you need to be acquainted with:
135
+
136
+ * __Container__ object must be declared to be used by your application during it's whole lifecycle. The Container job is to register and retrieve Services.
137
+
138
+ * __Service__ is a Plain Old Ruby Object (Poro o/) that contains your own logic. The __DependencyInjection__ gem doesn't need to know anything about it and won't force your to add/inherit any specific method.
139
+
140
+ * __Configurator__ is a standard Ruby Class that shares a callable to be used by different objects (like Services) to configure them after their instantiation.
141
+
142
+ #### Configuration
143
+
144
+ __DependencyInjection__ needs to be configured, using a yaml file, in order to map your services with your existing classes and their dependencies. There's also some other options that we'll list below.
145
+
146
+ Here's a configuration file example using almost everything __DependencyInjection__ has to offer:
147
+
148
+ ```yaml
149
+ parameters:
150
+ mailer.transport: smtp
151
+ services:
152
+ mailer:
153
+ class: Mailer
154
+ calls:
155
+ - ['transport=', '%mailer.transport%']
156
+ lazy: true
157
+ newsletter:
158
+ class: NewsletterManager
159
+ arguments:
160
+ - '@mailer'
161
+ ```
162
+
163
+ And here's some more details about each keyword:
164
+
165
+ * `parameters`: Based on a basic key/value scheme. This can later be used throughout your services by calling `%parameter_name%`.
166
+
167
+ * `services`: The services name must be used as the first indentation tier.
168
+
169
+ * `class`: A string containing the class name of your service.
170
+
171
+ * `arguments`: An array containing the parameters used by your class `intialize` method.
172
+
173
+ * `calls`: An array containing an array of each instance method and its parameters. Note that you only need to define the methods during your class instantiation.
174
+
175
+ * `lazy`: Returns a Proxy Object if true. The _real_ object will only be instantiated at the first method call.
176
+
177
+ * `alias`: A string containing the target service name.
178
+
179
+ * `scope`: A string containing one of two possibles values to handle the service initialization scope:
180
+ * `container`: a service is initialized only once throughout the container life (default)
181
+ * `prototype`: a new service is initialized each time you call the container
182
+
183
+ Note that the usage of a `prototype` service inside a `container` service raises a `ScopeWideningInjectionError`
184
+
185
+ __Please note:__
186
+ * You can reference a variable in the configuration with the following syntax: `%variable%`.
187
+ * You can reference declared services by prefixing it with an `@` sign.
188
+ * If you declare a service as an alias, the target service configuration will be used. Your own service configuration will be ignored.
189
+
190
+ ### Tests
191
+
192
+ __DependencyInjection__ is covered by tests at 100%, see [coveralls.io](https://coveralls.io/r/kdisneur/dependency_injection-ruby) service.
193
+
194
+ If you want to launch the tests by yourself:
195
+ * Clone this repository by running `git clone git@github.com:kdisneur/dependency_injection-ruby`
196
+ * Run `bundle install`
197
+ * Run `rake test`
198
+
199
+ ## Contribute
200
+
201
+ This is Github folks!
202
+
203
+ If you find a *bug*, open an [Issue](https://github.com/kdisneur/dependency_injection-ruby/issues).
204
+
205
+ It's OK to open an issue to ask us what we think about a change you'd like to make, so you don't work for nothing :)
206
+
207
+ If you want to add/change/hack/fix/improve/whatever something, make a [Pull Request](https://github.com/kdisneur/dependency_injection-ruby/pulls):
208
+
209
+ * Fork this repository
210
+ * Create a feature branch on your fork, we just love [git-flow](http://nvie.com/posts/a-successful-git-branching-model/)
211
+ * Do your stuff and pay attention to the following:
212
+ * Your code should be documented using [Tomdoc](http://tomdoc.org)
213
+ * You should follow [Github's Ruby Styleguide](https://github.com/styleguide/ruby)
214
+ * If needed, squash your commits to group them logically
215
+ * Update the CHANGELOG accordingly
216
+ * Make a Pull Request, we will *always* respond, and try to do it fast.
@@ -0,0 +1,28 @@
1
+ require 'dependency_injection/container'
2
+ require 'dependency_injection/loaders/yaml'
3
+
4
+ c = DependencyInjection::Container.new
5
+ loader = DependencyInjection::Loaders::Yaml.new(c)
6
+ loader.load(File.join(File.dirname(File.expand_path(__FILE__)), 'scoped_services.yml'))
7
+
8
+ class ContainerScopedService
9
+ def initialize
10
+ puts 'Container scoped initialization'
11
+ end
12
+ end
13
+
14
+ class PrototypeScopedService
15
+ def initialize
16
+ puts 'Prorotype scoped initialization'
17
+ end
18
+ end
19
+
20
+ c.get('my.container.scoped.service')
21
+ # => Container scoped initialization
22
+ c.get('my.container.scoped.service')
23
+ # =>
24
+
25
+ c.get('my.prototype.scoped.service')
26
+ # => Prorotype scoped initialization
27
+ c.get('my.prototype.scoped.service')
28
+ # => Prorotype scoped initialization
@@ -0,0 +1,6 @@
1
+ services:
2
+ my.container.scoped.service:
3
+ class: 'ContainerScopedService'
4
+ my.prototype.scoped.service:
5
+ class: 'PrototypeScopedService'
6
+ scope: 'prototype'
@@ -23,7 +23,7 @@ module DependencyInjection
23
23
 
24
24
  def register(name, klass_name, lazy=false)
25
25
  definition = lazy ? LazyDefinition.new(klass_name, self) : Definition.new(klass_name, self)
26
- @definitions[name] =definition
26
+ @definitions[name] = definition
27
27
  end
28
28
 
29
29
  def register_alias(name, alias_definition_name)
@@ -1,14 +1,16 @@
1
1
  require 'active_support/core_ext/string/inflections'
2
+ require 'dependency_injection/scope_widening_injection_error'
2
3
 
3
4
  module DependencyInjection
4
5
  class Definition
5
- attr_accessor :arguments, :configurator, :klass_name, :method_calls
6
+ attr_accessor :arguments, :configurator, :klass_name, :method_calls, :scope
6
7
 
7
8
  def initialize(klass_name, container)
8
9
  @container = container
9
10
  self.arguments = []
10
11
  self.klass_name = klass_name
11
12
  self.method_calls = {}
13
+ self.scope = :container
12
14
  end
13
15
 
14
16
  def add_argument(argument)
@@ -38,20 +40,30 @@ module DependencyInjection
38
40
  end
39
41
 
40
42
  def object
41
- return @object if @object
43
+ self.send("#{self.scope}_scoped_object")
44
+ end
45
+
46
+ private
47
+
48
+ def container_scoped_object
49
+ @object ||= initialize_object
50
+ end
42
51
 
43
- @object = self.klass.new(*resolve(self.arguments))
44
- self.method_calls.each { |method_name, arguments| @object.send(method_name, *resolve(arguments)) }
52
+ def initialize_object
53
+ object = self.klass.new(*resolve(self.arguments))
54
+ self.method_calls.each { |method_name, arguments| object.send(method_name, *resolve(arguments)) }
45
55
  if self.configurator
46
56
  name, method_name = self.configurator
47
57
  configurator_object = resolve([name]).first
48
- configurator_object.send(method_name, @object)
58
+ configurator_object.send(method_name, object)
49
59
  end
50
60
 
51
- @object
61
+ object
52
62
  end
53
63
 
54
- private
64
+ def prototype_scoped_object
65
+ initialize_object
66
+ end
55
67
 
56
68
  def resolve(arguments)
57
69
  resolve_references(resolve_container_parameters(arguments))
@@ -70,7 +82,10 @@ module DependencyInjection
70
82
  def resolve_references(arguments)
71
83
  arguments.map do |argument|
72
84
  if /^@(?<reference_name>.*)/ =~ argument
73
- @container.get(reference_name)
85
+ reference = @container.get(reference_name)
86
+ raise ScopeWideningInjectionError if reference.scope == :prototype && scope == :container
87
+
88
+ reference
74
89
  else
75
90
  argument
76
91
  end
@@ -38,6 +38,7 @@ module DependencyInjection
38
38
  def add_standard_service(name, parameters)
39
39
  lazy_load = parameters['lazy'] || false
40
40
  definition = @container.register(name, parameters['class'], lazy_load)
41
+ definition.scope = parameters['scope'] if parameters['scope']
41
42
  definition.add_arguments(*parameters['arguments']) if parameters['arguments']
42
43
  if (configurator = parameters['configurator'])
43
44
  definition.add_configurator(configurator[0], configurator[1])
@@ -0,0 +1,2 @@
1
+ class ScopeWideningInjectionError < Exception
2
+ end
@@ -1,3 +1,3 @@
1
1
  module DependencyInjection
2
- VERSION = '0.2.0'
2
+ VERSION = '0.3.0'
3
3
  end
@@ -70,6 +70,22 @@ class TestYaml < Minitest::Test
70
70
  @yaml_loader.send(:add_service, 'my_alias', { 'alias' => 'my_definition' })
71
71
  end
72
72
 
73
+ def test_adding_service_without_defined_scope
74
+ definition = mock
75
+ @container.stubs(:register).with('key_1', 'MyKlass', false).returns(definition)
76
+
77
+ definition.expects(:scope=).never
78
+ @yaml_loader.send(:add_service, 'key_1', { 'class' => 'MyKlass' })
79
+ end
80
+
81
+ def test_adding_service_with_defined_scope
82
+ definition = mock
83
+ @container.stubs(:register).with('key_1', 'MyKlass', false).returns(definition)
84
+
85
+ definition.expects(:scope=).with('awesome_scope')
86
+ @yaml_loader.send(:add_service, 'key_1', { 'class' => 'MyKlass', 'scope' => 'awesome_scope' })
87
+ end
88
+
73
89
  def test_adding_standard_service_as_lazy
74
90
  @container.expects(:register).with('my_lazy_definition', 'MyLazyDefinition', true)
75
91
  @yaml_loader.send(:add_standard_service, 'my_lazy_definition', { 'class' => 'MyLazyDefinition', 'lazy' => true })
@@ -130,6 +130,32 @@ class TestDefinition < Minitest::Test
130
130
  @definition.object
131
131
  end
132
132
 
133
+ def test_getting_container_scoped_object
134
+ @definition.scope = :container
135
+ @definition.expects(:send).with('container_scoped_object')
136
+ @definition.object
137
+ end
138
+
139
+ def test_getting_container_scoped_object_multiple_times
140
+ @definition.stubs(:initialize_object).returns(:object_1, :object_2)
141
+ @definition.scope = :container
142
+ assert_equal(:object_1, @definition.object)
143
+ assert_equal(:object_1, @definition.object)
144
+ end
145
+
146
+ def test_getting_prototype_scoped_object
147
+ @definition.scope = :prototype
148
+ @definition.expects(:send).with('prototype_scoped_object')
149
+ @definition.object
150
+ end
151
+
152
+ def test_getting_prototype_scoped_object_multiple_times
153
+ @definition.stubs(:initialize_object).returns(:object_1, :object_2)
154
+ @definition.scope = :prototype
155
+ assert_equal(:object_1, @definition.object)
156
+ assert_equal(:object_2, @definition.object)
157
+ end
158
+
133
159
  def test_resolving_first_container_parameters
134
160
  changed_arguments = mock
135
161
  arguments = mock
@@ -162,10 +188,39 @@ class TestDefinition < Minitest::Test
162
188
  assert_equal(%w(first second), @definition.send(:resolve_references, %w(first second)))
163
189
  end
164
190
 
165
- def test_resolving_references_with_references
191
+ def test_resolving_references_with_defintion_and_referenced_object_in_container_scope
166
192
  referenced_object = mock
193
+ referenced_object.stubs(:scope).returns(:container)
194
+ @definition.scope= :container
167
195
  @container.stubs(:get).with('reference.name').returns(referenced_object)
168
196
 
169
197
  assert_equal(['first', referenced_object], @definition.send(:resolve_references, %w(first @reference.name)))
170
198
  end
199
+
200
+ def test_resolving_references_with_defintion_and_referenced_object_in_prototype_scope
201
+ referenced_object = mock
202
+ referenced_object.stubs(:scope).returns(:prototype)
203
+ @definition.scope= :prototype
204
+ @container.stubs(:get).with('reference.name').returns(referenced_object)
205
+
206
+ assert_equal(['first', referenced_object], @definition.send(:resolve_references, %w(first @reference.name)))
207
+ end
208
+
209
+ def test_resolving_references_with_defintion_in_prototype_scope_and_referenced_object_in_container_scope
210
+ referenced_object = mock
211
+ referenced_object.stubs(:scope).returns(:container)
212
+ @definition.scope= :prototype
213
+ @container.stubs(:get).with('reference.name').returns(referenced_object)
214
+
215
+ assert_equal(['first', referenced_object], @definition.send(:resolve_references, %w(first @reference.name)))
216
+ end
217
+
218
+ def test_resolving_references_with_defintion_in_container_scope_and_referenced_object_in_prototype_scope
219
+ referenced_object = mock
220
+ referenced_object.stubs(:scope).returns(:prototype)
221
+ @definition.scope= :container
222
+ @container.stubs(:get).with('reference.name').returns(referenced_object)
223
+
224
+ assert_raises(ScopeWideningInjectionError) { @definition.send(:resolve_references, %w(first @reference.name)) }
225
+ end
171
226
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependency_injection
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Disneur
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-08-05 00:00:00.000000000 Z
11
+ date: 2013-09-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -34,6 +34,7 @@ files:
34
34
  - .ruby-gemset
35
35
  - .ruby-version
36
36
  - .travis.yml
37
+ - CHANGELOG.md
37
38
  - Gemfile
38
39
  - Gemfile.lock
39
40
  - README.md
@@ -48,6 +49,8 @@ files:
48
49
  - examples/inject_a_reference_to_another_dependency.rb
49
50
  - examples/lazy_load_object.rb
50
51
  - examples/lazy_load_object.yml
52
+ - examples/scoped_services.rb
53
+ - examples/scoped_services.yml
51
54
  - examples/yaml_configuration_file.rb
52
55
  - examples/yaml_configuration_file.yml
53
56
  - lib/dependency_injection/alias_definition.rb
@@ -56,6 +59,7 @@ files:
56
59
  - lib/dependency_injection/lazy_definition.rb
57
60
  - lib/dependency_injection/loaders/yaml.rb
58
61
  - lib/dependency_injection/proxy_object.rb
62
+ - lib/dependency_injection/scope_widening_injection_error.rb
59
63
  - lib/dependency_injection/version.rb
60
64
  - test/dependency_injection/loaders/test_yaml.rb
61
65
  - test/dependency_injection/test_alias_definition.rb