payload 0.1.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 +7 -0
- data/.gitignore +4 -0
- data/CONTRIBUTING.md +10 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +28 -0
- data/LICENSE +19 -0
- data/README.md +242 -0
- data/Rakefile +10 -0
- data/lib/payload/container.rb +96 -0
- data/lib/payload/controller.rb +12 -0
- data/lib/payload/decorator_chain.rb +23 -0
- data/lib/payload/definition_list.rb +41 -0
- data/lib/payload/exported_definition.rb +19 -0
- data/lib/payload/factory.rb +33 -0
- data/lib/payload/factory_definition.rb +24 -0
- data/lib/payload/mutable_container.rb +57 -0
- data/lib/payload/rack_container.rb +23 -0
- data/lib/payload/rails_loader.rb +58 -0
- data/lib/payload/railtie.rb +20 -0
- data/lib/payload/service_definition.rb +24 -0
- data/lib/payload/testing.rb +84 -0
- data/lib/payload/undefined_dependency_error.rb +5 -0
- data/lib/payload/version.rb +3 -0
- data/payload.gemspec +24 -0
- data/spec/payload/container_spec.rb +154 -0
- data/spec/payload/controller_spec.rb +32 -0
- data/spec/payload/decorator_chain_spec.rb +29 -0
- data/spec/payload/definition_list_spec.rb +80 -0
- data/spec/payload/exported_definition_spec.rb +30 -0
- data/spec/payload/factory_definition_spec.rb +49 -0
- data/spec/payload/factory_spec.rb +46 -0
- data/spec/payload/mutable_container_spec.rb +68 -0
- data/spec/payload/rack_container_spec.rb +33 -0
- data/spec/payload/rails_loader_spec.rb +62 -0
- data/spec/payload/service_definition_spec.rb +39 -0
- data/spec/spec_helper.rb +0 -0
- metadata +137 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c9e68c664efe4b32e13624aa82380db246b9774e
|
4
|
+
data.tar.gz: 4cf13b1014a69ec3d3faeb9d1e0fea439c33bb83
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6ca9dcd639d3c26d9f9e0598343dc0f83ed702ac303206788bbee9505240e5a4b357823a9395f1cb84ada129c4125117cfb0c3f89fd66238ca9999864d2c8876
|
7
|
+
data.tar.gz: 88b884efc4ebd0555b7eda0edaea9a2310d01f184ffa9a57c90a3803f9be557840926dbcb2c4b34579be973882d2853e5e3766eac3186b7f181f0d615605e0ba
|
data/.gitignore
ADDED
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
If you'd like to contribute:
|
2
|
+
|
3
|
+
1. Fork the [official repository](https://github.com/thoughtbot/payload).
|
4
|
+
2. Make your changes in a topic branch.
|
5
|
+
3. Submit a pull request.
|
6
|
+
|
7
|
+
Notes:
|
8
|
+
|
9
|
+
* Contributions without tests won't be accepted.
|
10
|
+
* Please don't update the Gem version.
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
payload (0.1.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
diff-lcs (1.2.5)
|
10
|
+
rack (1.5.2)
|
11
|
+
rake (10.3.2)
|
12
|
+
rspec (2.14.1)
|
13
|
+
rspec-core (~> 2.14.0)
|
14
|
+
rspec-expectations (~> 2.14.0)
|
15
|
+
rspec-mocks (~> 2.14.0)
|
16
|
+
rspec-core (2.14.8)
|
17
|
+
rspec-expectations (2.14.5)
|
18
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
19
|
+
rspec-mocks (2.14.6)
|
20
|
+
|
21
|
+
PLATFORMS
|
22
|
+
ruby
|
23
|
+
|
24
|
+
DEPENDENCIES
|
25
|
+
payload!
|
26
|
+
rack
|
27
|
+
rake
|
28
|
+
rspec (~> 2.14.1)
|
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2014 Joe Ferris and thoughtbot, inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,242 @@
|
|
1
|
+
# Payload
|
2
|
+
|
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.
|
8
|
+
|
9
|
+
Overview
|
10
|
+
--------
|
11
|
+
|
12
|
+
The framework makes it easy to define dependencies from application code without
|
13
|
+
resorting to singletons, constants, or globals. It also won't cause any
|
14
|
+
class-reloading issues during development.
|
15
|
+
|
16
|
+
Define simple dependencies in `config/dependencies.rb` using services:
|
17
|
+
|
18
|
+
service :payment_client do |container|
|
19
|
+
PaymentClient.new(ENV['PAYMENT_HOST'])
|
20
|
+
end
|
21
|
+
|
22
|
+
You can inject dependencies into controllers.
|
23
|
+
|
24
|
+
For example, in `app/controllers/payments_controller.rb`:
|
25
|
+
|
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
|
33
|
+
|
34
|
+
You can easily test this dependency in a controller spec:
|
35
|
+
|
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)
|
43
|
+
|
44
|
+
post :create, payment_params
|
45
|
+
|
46
|
+
expect(payment).to have_received(:process)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
You can further invert control and use factories to hide low-level dependencies
|
52
|
+
from controllers entirely.
|
53
|
+
|
54
|
+
In `config/dependencies.rb`:
|
55
|
+
|
56
|
+
factory :payment do |container|
|
57
|
+
Payment.new(container[:attributes], container[:payment_client])
|
58
|
+
end
|
59
|
+
|
60
|
+
In `app/controllers/payments_controller.rb`:
|
61
|
+
|
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
|
69
|
+
|
70
|
+
You can also stub factories in tests:
|
71
|
+
|
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)
|
77
|
+
|
78
|
+
post :create, payment_params
|
79
|
+
|
80
|
+
expect(payment).to have_received(:process)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
The controller and its tests are now completely ignorant of `payment_client`,
|
86
|
+
and deal only with the collaborator they need: `payment`.
|
87
|
+
|
88
|
+
Setup
|
89
|
+
-----
|
90
|
+
|
91
|
+
Add payload to your Gemfile:
|
92
|
+
|
93
|
+
gem 'payload'
|
94
|
+
|
95
|
+
To access dependencies from controllers, include the `Controller` module:
|
96
|
+
|
97
|
+
class ApplicationController < ActionController::Base
|
98
|
+
include Payload::Controller
|
99
|
+
end
|
100
|
+
|
101
|
+
Specifying Dependencies
|
102
|
+
-----------------------
|
103
|
+
|
104
|
+
Edit `config/dependencies.rb` to specify dependencies.
|
105
|
+
|
106
|
+
Use the `service` method to define dependencies which can be fully instantiated
|
107
|
+
during application bootup:
|
108
|
+
|
109
|
+
service :payment_client do |container|
|
110
|
+
PaymentClient.new(ENV['PAYMENT_HOST'])
|
111
|
+
end
|
112
|
+
|
113
|
+
Other dependencies are accessible from the container:
|
114
|
+
|
115
|
+
service :payment_notifier do |container|
|
116
|
+
PaymentNotifier.new(container[:mailer])
|
117
|
+
end
|
118
|
+
|
119
|
+
Use the `factory` method to define dependencies which require dependencies from
|
120
|
+
the container as well as runtime state which varies per-request:
|
121
|
+
|
122
|
+
factory :payment do |container|
|
123
|
+
Payment.new(container[:attributes], container[:payment_client])
|
124
|
+
end
|
125
|
+
|
126
|
+
The container for factory definitions contains all dependencies defined on the
|
127
|
+
container as well as dependencies provided when instantiating the factory.
|
128
|
+
|
129
|
+
Use the `decorate` method to extend or replace a previously defined dependency:
|
130
|
+
|
131
|
+
decorate :payment do |payment, container|
|
132
|
+
NotifyingPayment.new(payment, container[:payment_notifier])
|
133
|
+
end
|
134
|
+
|
135
|
+
Decorated dependencies have access to other dependencies through the container,
|
136
|
+
as well as the current definition for that dependency.
|
137
|
+
|
138
|
+
Using Dependencies
|
139
|
+
------------------
|
140
|
+
|
141
|
+
The Railtie inserts middleware into the stack which will inject a container into
|
142
|
+
the Rack environment for each request. This is available as `dependencies` in
|
143
|
+
controllers and `env[:dependencies]` in the Rack stack.
|
144
|
+
|
145
|
+
Use `[]` to access services:
|
146
|
+
|
147
|
+
class PaymentsController < ApplicationController
|
148
|
+
def create
|
149
|
+
dependencies[:payment_client].charge(params[:amount])
|
150
|
+
redirect_to payments_path
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
Use `new` to instantiate dependencies from factories:
|
155
|
+
|
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
|
163
|
+
|
164
|
+
The `new` method accepts a `Hash`. Each element of the `Hash` will be accessible
|
165
|
+
from the container in `factory` definitions.
|
166
|
+
|
167
|
+
Grouping Dependencies
|
168
|
+
---------------------
|
169
|
+
|
170
|
+
You can enforce simplicity in your dependency graph by grouping dependencies and
|
171
|
+
explicitly exporting only the dependencies you need to expose to the application
|
172
|
+
layer.
|
173
|
+
|
174
|
+
For example, you can specify payment dependencies in
|
175
|
+
`config/dependencies/payments.rb`:
|
176
|
+
|
177
|
+
service :payment_client do |container|
|
178
|
+
PaymentClient.new(ENV['PAYMENT_HOST'])
|
179
|
+
end
|
180
|
+
|
181
|
+
service :payment_notifier do |container|
|
182
|
+
PaymentNotifier.new(container[:mailer])
|
183
|
+
end
|
184
|
+
|
185
|
+
factory :payment do |container|
|
186
|
+
Payment.new(container[:attributes], container[:payment_client])
|
187
|
+
end
|
188
|
+
|
189
|
+
decorate :payment do |payment, container|
|
190
|
+
NotifyingPayment.new(payment, container[:payment_notifier])
|
191
|
+
end
|
192
|
+
|
193
|
+
export :payment
|
194
|
+
|
195
|
+
In this example, the final, decorated `:payment` dependency will be available in
|
196
|
+
controllers, but `:payment_client` and `:payment_notifier` will not.
|
197
|
+
|
198
|
+
You can use this approach to hide low-level dependencies behind a facade and
|
199
|
+
only expose the facade to the application layer.
|
200
|
+
|
201
|
+
Testing
|
202
|
+
-------
|
203
|
+
|
204
|
+
To activate testing support, require and mix in the `Testing` module:
|
205
|
+
|
206
|
+
require 'dependencies/testing'
|
207
|
+
|
208
|
+
RSpec.configure do |config|
|
209
|
+
config.include Payload::Testing
|
210
|
+
end
|
211
|
+
|
212
|
+
During integration tests, the fully configured container will be used. During
|
213
|
+
controller tests, an empty container will be initialized for each test. Tests
|
214
|
+
can inject the dependencies they need for each interaction.
|
215
|
+
|
216
|
+
This module provides two useful methods:
|
217
|
+
|
218
|
+
* `stub_service`: Injects a stubbed service into the test container and returns
|
219
|
+
it.
|
220
|
+
* `stub_factory_instance`: Finds or injects a stubbed factory into the test
|
221
|
+
container and expects an instance to be created with the given attributes.
|
222
|
+
|
223
|
+
Contributing
|
224
|
+
------------
|
225
|
+
|
226
|
+
Please see the [contribution guidelines].
|
227
|
+
|
228
|
+
[contribution guidelines]: https://github.com/thoughtbot/payload/blob/master/CONTRIBUTING.md
|
229
|
+
|
230
|
+
License
|
231
|
+
-------
|
232
|
+
|
233
|
+

|
234
|
+
|
235
|
+
Payload is Copyright © 2014 Joe Ferris and thoughtbot. It is free software, and
|
236
|
+
may be redistributed under the terms specified in the [LICENSE] file.
|
237
|
+
|
238
|
+
[LICENSE]: https://github.com/thoughtbot/payload/blob/master/LICENSE
|
239
|
+
|
240
|
+
Payload is maintained and funded by [thoughtbot, inc](http://thoughtbot.com/).
|
241
|
+
|
242
|
+
The names and logos for thoughtbot are trademarks of thoughtbot, inc.
|
data/Rakefile
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'payload/definition_list'
|
2
|
+
require 'payload/factory_definition'
|
3
|
+
require 'payload/service_definition'
|
4
|
+
|
5
|
+
module Payload
|
6
|
+
# Used for configuring and resolving dependencies.
|
7
|
+
#
|
8
|
+
# @see Railtie Railtie to configure and use dependencies in Rails
|
9
|
+
# applications.
|
10
|
+
# @see RackContainer RackContainer to inject a container into Rack requests.
|
11
|
+
# @see MutableContainer MutableContainer to define dependencies in
|
12
|
+
# configuration files.
|
13
|
+
class Container
|
14
|
+
# Used internally by {RailsLoader}.
|
15
|
+
#
|
16
|
+
# @api private
|
17
|
+
# @param [DefinitionList] definitions previously defined definitions.
|
18
|
+
def initialize(definitions = DefinitionList.new)
|
19
|
+
@definitions = definitions
|
20
|
+
end
|
21
|
+
|
22
|
+
# Extends or replaces an existing dependency definition.
|
23
|
+
#
|
24
|
+
# @param dependency [Symbol] the name of the dependency to decorate.
|
25
|
+
# @yield [Container] the resolved container.
|
26
|
+
# @yieldreturn the decorated instance.
|
27
|
+
def decorate(dependency, &block)
|
28
|
+
decorated = @definitions.find(dependency).decorate(block)
|
29
|
+
define dependency, decorated
|
30
|
+
end
|
31
|
+
|
32
|
+
# Defines a factory which can be used to instantiate the dependency. Useful
|
33
|
+
# if some dependencies come from the container but others come from runtime
|
34
|
+
# state.
|
35
|
+
#
|
36
|
+
# Resolving the dependency will return an object which responds to `new`.
|
37
|
+
# The `new` method will accept remaining dependencies and return the fully
|
38
|
+
# resolved dependency from the given block.
|
39
|
+
#
|
40
|
+
# @param dependency [Symbol] the name of the dependency to define.
|
41
|
+
# @yield [Container] the resolved container; any arguments to `new` will be
|
42
|
+
# added to the container.
|
43
|
+
# @yieldreturn an instance of your dependency.
|
44
|
+
def factory(dependency, &block)
|
45
|
+
define dependency, FactoryDefinition.new(block)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Defines a service which can be fully resolved from the container.
|
49
|
+
#
|
50
|
+
# @param dependency [Symbol] the name of the dependency to define.
|
51
|
+
# @yield [Container] the resolved container.
|
52
|
+
# @yieldreturn the instantiated service.
|
53
|
+
def service(dependency, &block)
|
54
|
+
define dependency, ServiceDefinition.new(block)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Resolves and returns dependency.
|
58
|
+
#
|
59
|
+
# @param dependency [Symbol] the name of the dependency to resolve.
|
60
|
+
# @raise [UndefinedDependencyError] for undefined dependencies.
|
61
|
+
def [](dependency)
|
62
|
+
@definitions.find(dependency).resolve(self)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Exports dependencies which can be imported into another container.
|
66
|
+
#
|
67
|
+
# Used internally by {MutableContainer}. Use {MutableContainer#export}.
|
68
|
+
#
|
69
|
+
# @api private
|
70
|
+
# @param names [Array<Symbol>] dependencies to export.
|
71
|
+
# @return [DependencyList] exported dependencies.
|
72
|
+
def export(*names)
|
73
|
+
@definitions.export(names)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Import dependencies which were exported from another container.
|
77
|
+
#
|
78
|
+
# Used internally by {RailsLoader}.
|
79
|
+
#
|
80
|
+
# @api private
|
81
|
+
# @param definitions [DependencyList] definitions to import.
|
82
|
+
# @return [Container] a new container with the imported definitions.
|
83
|
+
def import(definitions)
|
84
|
+
self.class.new @definitions.import(definitions)
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
# Duplicates this object with the definition at the end of the list.
|
90
|
+
#
|
91
|
+
# @api private
|
92
|
+
def define(dependency, definition)
|
93
|
+
self.class.new(@definitions.add(dependency, definition))
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Payload
|
2
|
+
# Mixin to provide access to a {Container}.
|
3
|
+
#
|
4
|
+
# Include this mixin to access dependencies in controllers. The {Container}
|
5
|
+
# will be injected using {RackContainer}.
|
6
|
+
module Controller
|
7
|
+
# @return [Container] dependencies injected from {RackContainer}.
|
8
|
+
def dependencies
|
9
|
+
request.env[:dependencies]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|