payload 0.1.0 → 0.2.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: c9e68c664efe4b32e13624aa82380db246b9774e
4
- data.tar.gz: 4cf13b1014a69ec3d3faeb9d1e0fea439c33bb83
3
+ metadata.gz: 4b38f94a64e5378a8c51ffdccd8c4ac628328b12
4
+ data.tar.gz: 2280162582b264eb331e86d9f22807bfb28ae982
5
5
  SHA512:
6
- metadata.gz: 6ca9dcd639d3c26d9f9e0598343dc0f83ed702ac303206788bbee9505240e5a4b357823a9395f1cb84ada129c4125117cfb0c3f89fd66238ca9999864d2c8876
7
- data.tar.gz: 88b884efc4ebd0555b7eda0edaea9a2310d01f184ffa9a57c90a3803f9be557840926dbcb2c4b34579be973882d2853e5e3766eac3186b7f181f0d615605e0ba
6
+ metadata.gz: 4d5a63f30de48e69c265ad11e51567113532a5ed3f23d7548612d2afa3247c8a415e4bcc5c20667afdc86257f6cf0285312b371ca4d78942a9da398eafa17f9e
7
+ data.tar.gz: 67d2bacd4a2229145974e8cf41e88d0641514865666e2af27c24a074464349f10289a4f9b4c213a356eb90abb057e0892fabcbc03b908aff1a9899572a398b17
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- payload (0.1.0)
4
+ payload (0.2.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -1,10 +1,12 @@
1
1
  # Payload
2
2
 
3
3
  Payload is a lightweight framework for specifying, injecting, and using
4
- dependencies. It facilitates run-time assembly of dependencies and makes it
5
- plausible to use inversion of control beyond the controller level. It also
6
- attempts to remove boilerplate code for common patterns such as defining
7
- factories and applying decorators.
4
+ dependencies in Ruby on Rails applications. It facilitates run-time assembly of
5
+ dependencies and makes it plausible to use [inversion of control] beyond the
6
+ controller level. It also attempts to remove boilerplate code for common
7
+ patterns such as defining factories and applying decorators.
8
+
9
+ [inversion of control]: http://martinfowler.com/articles/injection.html
8
10
 
9
11
  Overview
10
12
  --------
@@ -13,74 +15,95 @@ The framework makes it easy to define dependencies from application code without
13
15
  resorting to singletons, constants, or globals. It also won't cause any
14
16
  class-reloading issues during development.
15
17
 
18
+ The central object in the framework is a "container." Dependencies are specified
19
+ using Ruby configuration files. Configured dependencies are then available
20
+ anywhere a reference to the container is available.
21
+
16
22
  Define simple dependencies in `config/dependencies.rb` using services:
17
23
 
18
- service :payment_client do |container|
19
- PaymentClient.new(ENV['PAYMENT_HOST'])
20
- end
24
+ ```ruby
25
+ service :payment_client do |container|
26
+ PaymentClient.new(ENV['PAYMENT_HOST'])
27
+ end
28
+ ```
21
29
 
22
- You can inject dependencies into controllers.
30
+ The configuration block receives a reference to the container, which will
31
+ contain all configured dependencies. This makes it possible to define
32
+ dependencies without knowing how or when their sub-dependencies will be defined.
33
+
34
+ Controllers receive a reference named `dependencies`, so you can easily inject
35
+ dependencies into controllers.
23
36
 
24
37
  For example, in `app/controllers/payments_controller.rb`:
25
38
 
26
- class PaymentsController < ApplicationController
27
- def create
28
- payment = Payment.new(params[:payment], dependencies[:payment_client])
29
- receipt = payment.process
30
- redirect_to receipt
31
- end
32
- end
39
+ ```ruby
40
+ class PaymentsController < ApplicationController
41
+ def create
42
+ payment = Payment.new(params[:payment], dependencies[:payment_client])
43
+ receipt = payment.process
44
+ redirect_to receipt
45
+ end
46
+ end
47
+ ```
33
48
 
34
49
  You can easily test this dependency in a controller spec:
35
50
 
36
- describe PaymentsController do
37
- describe '#create' do
38
- it 'processes a payment' do
39
- payment_params = :product_id => '123', amount: '25'
40
- client = stub_service(:payment_client)
41
- payment = double('payment', process: true)
42
- Payment.stub(:new).with(payment_params, client).and_return(payment)
51
+ ```ruby
52
+ describe PaymentsController do
53
+ describe '#create' do
54
+ it 'processes a payment' do
55
+ payment_params = { product_id: '123', amount: '25' }
56
+ client = stub_service(:payment_client)
57
+ payment = double('payment', process: true)
58
+ Payment.stub(:new).with(payment_params, client).and_return(payment)
43
59
 
44
- post :create, payment_params
60
+ post :create, payment_params
45
61
 
46
- expect(payment).to have_received(:process)
47
- end
48
- end
62
+ expect(payment).to have_received(:process)
49
63
  end
64
+ end
65
+ end
66
+ ```
50
67
 
51
68
  You can further invert control and use factories to hide low-level dependencies
52
69
  from controllers entirely.
53
70
 
54
71
  In `config/dependencies.rb`:
55
72
 
56
- factory :payment do |container|
57
- Payment.new(container[:attributes], container[:payment_client])
58
- end
73
+ ```ruby
74
+ factory :payment do |container|
75
+ Payment.new(container[:attributes], container[:payment_client])
76
+ end
77
+ ```
59
78
 
60
79
  In `app/controllers/payments_controller.rb`:
61
80
 
62
- class PaymentsController < ApplicationController
63
- def create
64
- payment = dependencies[:payment].new(attributes: params[:payment])
65
- payment.process
66
- redirect_to payment
67
- end
68
- end
81
+ ```ruby
82
+ class PaymentsController < ApplicationController
83
+ def create
84
+ payment = dependencies[:payment].new(attributes: params[:payment])
85
+ payment.process
86
+ redirect_to payment
87
+ end
88
+ end
89
+ ```
69
90
 
70
91
  You can also stub factories in tests:
71
92
 
72
- describe PaymentsController do
73
- describe '#create' do
74
- it 'processes a payment' do
75
- payment_params = :product_id => '123', amount: '25'
76
- payment = stub_factory_instance(:payment, attributes: payment_params)
93
+ ```ruby
94
+ describe PaymentsController do
95
+ describe '#create' do
96
+ it 'processes a payment' do
97
+ payment_params = { product_id: '123', amount: '25' }
98
+ payment = stub_factory_instance(:payment, attributes: payment_params)
77
99
 
78
- post :create, payment_params
100
+ post :create, payment_params
79
101
 
80
- expect(payment).to have_received(:process)
81
- end
82
- end
102
+ expect(payment).to have_received(:process)
83
103
  end
104
+ end
105
+ end
106
+ ```
84
107
 
85
108
  The controller and its tests are now completely ignorant of `payment_client`,
86
109
  and deal only with the collaborator they need: `payment`.
@@ -90,13 +113,17 @@ Setup
90
113
 
91
114
  Add payload to your Gemfile:
92
115
 
93
- gem 'payload'
116
+ ```ruby
117
+ gem 'payload'
118
+ ```
94
119
 
95
120
  To access dependencies from controllers, include the `Controller` module:
96
121
 
97
- class ApplicationController < ActionController::Base
98
- include Payload::Controller
99
- end
122
+ ```ruby
123
+ class ApplicationController < ActionController::Base
124
+ include Payload::Controller
125
+ end
126
+ ```
100
127
 
101
128
  Specifying Dependencies
102
129
  -----------------------
@@ -104,33 +131,41 @@ Specifying Dependencies
104
131
  Edit `config/dependencies.rb` to specify dependencies.
105
132
 
106
133
  Use the `service` method to define dependencies which can be fully instantiated
107
- during application bootup:
134
+ when the application boots:
108
135
 
109
- service :payment_client do |container|
110
- PaymentClient.new(ENV['PAYMENT_HOST'])
111
- end
136
+ ```ruby
137
+ service :payment_client do |container|
138
+ PaymentClient.new(ENV['PAYMENT_HOST'])
139
+ end
140
+ ```
112
141
 
113
142
  Other dependencies are accessible from the container:
114
143
 
115
- service :payment_notifier do |container|
116
- PaymentNotifier.new(container[:mailer])
117
- end
144
+ ```ruby
145
+ service :payment_notifier do |container|
146
+ PaymentNotifier.new(container[:mailer])
147
+ end
148
+ ```
118
149
 
119
150
  Use the `factory` method to define dependencies which require dependencies from
120
151
  the container as well as runtime state which varies per-request:
121
152
 
122
- factory :payment do |container|
123
- Payment.new(container[:attributes], container[:payment_client])
124
- end
153
+ ```ruby
154
+ factory :payment do |container|
155
+ Payment.new(container[:attributes], container[:payment_client])
156
+ end
157
+ ```
125
158
 
126
159
  The container for factory definitions contains all dependencies defined on the
127
160
  container as well as dependencies provided when instantiating the factory.
128
161
 
129
162
  Use the `decorate` method to extend or replace a previously defined dependency:
130
163
 
131
- decorate :payment do |payment, container|
132
- NotifyingPayment.new(payment, container[:payment_notifier])
133
- end
164
+ ```ruby
165
+ decorate :payment do |payment, container|
166
+ NotifyingPayment.new(payment, container[:payment_notifier])
167
+ end
168
+ ```
134
169
 
135
170
  Decorated dependencies have access to other dependencies through the container,
136
171
  as well as the current definition for that dependency.
@@ -144,22 +179,26 @@ controllers and `env[:dependencies]` in the Rack stack.
144
179
 
145
180
  Use `[]` to access services:
146
181
 
147
- class PaymentsController < ApplicationController
148
- def create
149
- dependencies[:payment_client].charge(params[:amount])
150
- redirect_to payments_path
151
- end
152
- end
182
+ ```ruby
183
+ class PaymentsController < ApplicationController
184
+ def create
185
+ dependencies[:payment_client].charge(params[:amount])
186
+ redirect_to payments_path
187
+ end
188
+ end
189
+ ```
153
190
 
154
191
  Use `new` to instantiate dependencies from factories:
155
192
 
156
- class PaymentsController < ApplicationController
157
- def create
158
- payment = dependencies[:payment].new(attributes: params[:payment])
159
- payment.process
160
- redirect_to payment
161
- end
162
- end
193
+ ```ruby
194
+ class PaymentsController < ApplicationController
195
+ def create
196
+ payment = dependencies[:payment].new(attributes: params[:payment])
197
+ payment.process
198
+ redirect_to payment
199
+ end
200
+ end
201
+ ```
163
202
 
164
203
  The `new` method accepts a `Hash`. Each element of the `Hash` will be accessible
165
204
  from the container in `factory` definitions.
@@ -174,23 +213,25 @@ layer.
174
213
  For example, you can specify payment dependencies in
175
214
  `config/dependencies/payments.rb`:
176
215
 
177
- service :payment_client do |container|
178
- PaymentClient.new(ENV['PAYMENT_HOST'])
179
- end
216
+ ```ruby
217
+ service :payment_client do |container|
218
+ PaymentClient.new(ENV['PAYMENT_HOST'])
219
+ end
180
220
 
181
- service :payment_notifier do |container|
182
- PaymentNotifier.new(container[:mailer])
183
- end
221
+ service :payment_notifier do |container|
222
+ PaymentNotifier.new(container[:mailer])
223
+ end
184
224
 
185
- factory :payment do |container|
186
- Payment.new(container[:attributes], container[:payment_client])
187
- end
225
+ factory :payment do |container|
226
+ Payment.new(container[:attributes], container[:payment_client])
227
+ end
188
228
 
189
- decorate :payment do |payment, container|
190
- NotifyingPayment.new(payment, container[:payment_notifier])
191
- end
229
+ decorate :payment do |payment, container|
230
+ NotifyingPayment.new(payment, container[:payment_notifier])
231
+ end
192
232
 
193
- export :payment
233
+ export :payment
234
+ ```
194
235
 
195
236
  In this example, the final, decorated `:payment` dependency will be available in
196
237
  controllers, but `:payment_client` and `:payment_notifier` will not.
@@ -203,11 +244,13 @@ Testing
203
244
 
204
245
  To activate testing support, require and mix in the `Testing` module:
205
246
 
206
- require 'dependencies/testing'
247
+ ```ruby
248
+ require 'payload/testing'
207
249
 
208
- RSpec.configure do |config|
209
- config.include Payload::Testing
210
- end
250
+ RSpec.configure do |config|
251
+ config.include Payload::Testing
252
+ end
253
+ ```
211
254
 
212
255
  During integration tests, the fully configured container will be used. During
213
256
  controller tests, an empty container will be initialized for each test. Tests
@@ -1,6 +1,6 @@
1
1
  require 'payload/definition_list'
2
- require 'payload/factory_definition'
3
- require 'payload/service_definition'
2
+ require 'payload/factory_resolver'
3
+ require 'payload/service_resolver'
4
4
 
5
5
  module Payload
6
6
  # Used for configuring and resolving dependencies.
@@ -25,8 +25,7 @@ module Payload
25
25
  # @yield [Container] the resolved container.
26
26
  # @yieldreturn the decorated instance.
27
27
  def decorate(dependency, &block)
28
- decorated = @definitions.find(dependency).decorate(block)
29
- define dependency, decorated
28
+ self.class.new(@definitions.decorate(dependency, block))
30
29
  end
31
30
 
32
31
  # Defines a factory which can be used to instantiate the dependency. Useful
@@ -42,7 +41,7 @@ module Payload
42
41
  # added to the container.
43
42
  # @yieldreturn an instance of your dependency.
44
43
  def factory(dependency, &block)
45
- define dependency, FactoryDefinition.new(block)
44
+ define dependency, FactoryResolver.new(block)
46
45
  end
47
46
 
48
47
  # Defines a service which can be fully resolved from the container.
@@ -51,7 +50,7 @@ module Payload
51
50
  # @yield [Container] the resolved container.
52
51
  # @yieldreturn the instantiated service.
53
52
  def service(dependency, &block)
54
- define dependency, ServiceDefinition.new(block)
53
+ define dependency, ServiceResolver.new(block)
55
54
  end
56
55
 
57
56
  # Resolves and returns dependency.
@@ -86,11 +85,9 @@ module Payload
86
85
 
87
86
  private
88
87
 
89
- # Duplicates this object with the definition at the end of the list.
90
- #
91
88
  # @api private
92
- def define(dependency, definition)
93
- self.class.new(@definitions.add(dependency, definition))
89
+ def define(dependency, resolver)
90
+ self.class.new(@definitions.add(dependency, resolver))
94
91
  end
95
92
  end
96
93
  end
@@ -6,18 +6,32 @@ module Payload
6
6
  #
7
7
  # @api private
8
8
  class DecoratorChain
9
+ include Enumerable
10
+
9
11
  def initialize(decorators = [])
10
12
  @decorators = decorators
11
13
  end
12
14
 
13
15
  def add(decorator)
14
- self.class.new @decorators + [decorator]
16
+ self.class.new decorators + [decorator]
15
17
  end
16
18
 
17
19
  def decorate(base, container)
18
- @decorators.inject(base) do |component, decorator|
20
+ decorators.inject(base) do |component, decorator|
19
21
  decorator.call(component, container)
20
22
  end
21
23
  end
24
+
25
+ def each(&block)
26
+ decorators.each(&block)
27
+ end
28
+
29
+ def ==(other)
30
+ other.is_a?(DecoratorChain) && decorators == other.decorators
31
+ end
32
+
33
+ protected
34
+
35
+ attr_reader :decorators
22
36
  end
23
37
  end
@@ -0,0 +1,39 @@
1
+ require 'payload/decorator_chain'
2
+ require 'payload/dependency_already_defined_error'
3
+
4
+ module Payload
5
+ # Definition for a dependency which can be resolved and decorated.
6
+ #
7
+ # Used internally by {DefinitionList}. Use {Container#service} or
8
+ # {Container#fatory}.
9
+ #
10
+ # @api private
11
+ class Definition
12
+ def initialize(resolver, decorators = DecoratorChain.new)
13
+ @resolver = resolver
14
+ @decorators = decorators
15
+ end
16
+
17
+ def resolve(container)
18
+ resolver.resolve(container, decorators)
19
+ end
20
+
21
+ def decorate(decorator)
22
+ self.class.new resolver, decorators.add(decorator)
23
+ end
24
+
25
+ def set(_)
26
+ raise DependencyAlreadyDefinedError
27
+ end
28
+
29
+ def ==(other)
30
+ other.is_a?(Definition) &&
31
+ resolver == other.resolver &&
32
+ decorators == other.decorators
33
+ end
34
+
35
+ protected
36
+
37
+ attr_reader :resolver, :decorators
38
+ end
39
+ end