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 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
@@ -0,0 +1,4 @@
1
+ .bundle
2
+ .yardoc
3
+ doc
4
+ pkg
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
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
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,10 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'bundler/gem_tasks'
4
+ require 'rake'
5
+ require 'rspec/core/rake_task'
6
+
7
+ desc 'Default: run specs'
8
+ task :default => [:spec]
9
+
10
+ RSpec::Core::RakeTask.new(:spec)
@@ -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