payload 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
![thoughtbot](http://thoughtbot.com/assets/tm/logo.png)
|
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
|