payload 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +135 -92
- data/lib/payload/container.rb +7 -10
- data/lib/payload/decorator_chain.rb +16 -2
- data/lib/payload/definition.rb +39 -0
- data/lib/payload/definition_list.rb +11 -6
- data/lib/payload/dependency_already_defined_error.rb +5 -0
- data/lib/payload/exported_definition.rb +12 -2
- data/lib/payload/factory.rb +1 -1
- data/lib/payload/factory_resolver.rb +18 -0
- data/lib/payload/service_resolver.rb +17 -0
- data/lib/payload/undefined_definition.rb +37 -0
- data/lib/payload/version.rb +1 -1
- data/spec/payload/container_spec.rb +11 -3
- data/spec/payload/decorator_chain_spec.rb +34 -0
- data/spec/payload/definition_list_spec.rb +78 -27
- data/spec/payload/definition_spec.rb +72 -0
- data/spec/payload/exported_definition_spec.rb +25 -2
- data/spec/payload/factory_resolver_spec.rb +22 -0
- data/spec/payload/service_resolver_spec.rb +23 -0
- data/spec/payload/undefined_definition_spec.rb +65 -0
- metadata +26 -19
- data/lib/payload/factory_definition.rb +0 -24
- data/lib/payload/service_definition.rb +0 -24
- data/spec/payload/factory_definition_spec.rb +0 -49
- data/spec/payload/service_definition_spec.rb +0 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4b38f94a64e5378a8c51ffdccd8c4ac628328b12
|
4
|
+
data.tar.gz: 2280162582b264eb331e86d9f22807bfb28ae982
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4d5a63f30de48e69c265ad11e51567113532a5ed3f23d7548612d2afa3247c8a415e4bcc5c20667afdc86257f6cf0285312b371ca4d78942a9da398eafa17f9e
|
7
|
+
data.tar.gz: 67d2bacd4a2229145974e8cf41e88d0641514865666e2af27c24a074464349f10289a4f9b4c213a356eb90abb057e0892fabcbc03b908aff1a9899572a398b17
|
data/Gemfile.lock
CHANGED
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
|
5
|
-
plausible to use inversion of control beyond the
|
6
|
-
attempts to remove boilerplate code for common
|
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
|
-
|
19
|
-
|
20
|
-
|
24
|
+
```ruby
|
25
|
+
service :payment_client do |container|
|
26
|
+
PaymentClient.new(ENV['PAYMENT_HOST'])
|
27
|
+
end
|
28
|
+
```
|
21
29
|
|
22
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
60
|
+
post :create, payment_params
|
45
61
|
|
46
|
-
|
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
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
100
|
+
post :create, payment_params
|
79
101
|
|
80
|
-
|
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
|
-
|
116
|
+
```ruby
|
117
|
+
gem 'payload'
|
118
|
+
```
|
94
119
|
|
95
120
|
To access dependencies from controllers, include the `Controller` module:
|
96
121
|
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
134
|
+
when the application boots:
|
108
135
|
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
-
|
116
|
-
|
117
|
-
|
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
|
-
|
123
|
-
|
124
|
-
|
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
|
-
|
132
|
-
|
133
|
-
|
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
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
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
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
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
|
-
|
178
|
-
|
179
|
-
|
216
|
+
```ruby
|
217
|
+
service :payment_client do |container|
|
218
|
+
PaymentClient.new(ENV['PAYMENT_HOST'])
|
219
|
+
end
|
180
220
|
|
181
|
-
|
182
|
-
|
183
|
-
|
221
|
+
service :payment_notifier do |container|
|
222
|
+
PaymentNotifier.new(container[:mailer])
|
223
|
+
end
|
184
224
|
|
185
|
-
|
186
|
-
|
187
|
-
|
225
|
+
factory :payment do |container|
|
226
|
+
Payment.new(container[:attributes], container[:payment_client])
|
227
|
+
end
|
188
228
|
|
189
|
-
|
190
|
-
|
191
|
-
|
229
|
+
decorate :payment do |payment, container|
|
230
|
+
NotifyingPayment.new(payment, container[:payment_notifier])
|
231
|
+
end
|
192
232
|
|
193
|
-
|
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
|
-
|
247
|
+
```ruby
|
248
|
+
require 'payload/testing'
|
207
249
|
|
208
|
-
|
209
|
-
|
210
|
-
|
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
|
data/lib/payload/container.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'payload/definition_list'
|
2
|
-
require 'payload/
|
3
|
-
require 'payload/
|
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
|
-
|
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,
|
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,
|
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,
|
93
|
-
self.class.new(@definitions.add(dependency,
|
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
|
16
|
+
self.class.new decorators + [decorator]
|
15
17
|
end
|
16
18
|
|
17
19
|
def decorate(base, container)
|
18
|
-
|
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
|