poniard 0.0.3 → 1.0.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/CHANGELOG.md +23 -0
- data/README.md +41 -61
- data/lib/poniard/controller.rb +18 -1
- data/lib/poniard/controller_source.rb +101 -5
- data/lib/poniard/injector.rb +74 -13
- data/lib/poniard/version.rb +3 -1
- data/poniard.gemspec +1 -2
- data/spec/controller_source_spec.rb +294 -0
- data/spec/injector_spec.rb +18 -0
- data/spec/integration_helper.rb +44 -0
- data/spec/spec_helper.rb +3 -0
- metadata +18 -26
- data/HISTORY.md +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b20dd7bc5a80523165730ffd2352ba33036988b2
|
4
|
+
data.tar.gz: 9eb61c3e74958ace02b6865bb2d6ae0ed443b3a4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b5ec064ebc9ed82ca7b3dd47e1e321b062ef153f795513b86feb00d08b0f742cbae5f272dd2ddf4f2c01c2b0b62f62b57e735a8981f3f0eea43587c8eb700ec0
|
7
|
+
data.tar.gz: bb8251380ad780db82f9a15171ba543787d78a25ee4c1368beffab81070d7ea4ff235333b99b74470d413cc34ebd926ee35299f8f43729b490cfe564a03086b6
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# Poniard History
|
2
|
+
|
3
|
+
## 1.0.0 - 20 April 2014
|
4
|
+
|
5
|
+
* `respond_with` returns 406 for unknown formats.
|
6
|
+
* Remove `OpenStruct` usage and other known cache-busting constructs.
|
7
|
+
* `redirect_to` supports redirects to arbitrary locations when passed a string
|
8
|
+
parameter.
|
9
|
+
* Complete documentation of public API.
|
10
|
+
|
11
|
+
## 0.0.3 - 19 March 2014
|
12
|
+
|
13
|
+
* Support `head` responses.
|
14
|
+
|
15
|
+
## 0.0.2 - 26 January 2013
|
16
|
+
|
17
|
+
* Hashes can be used directly as sources.
|
18
|
+
* Provide default argument for `response.default`.
|
19
|
+
|
20
|
+
## 0.0.1 - 25 November 2012
|
21
|
+
|
22
|
+
* Initial release.
|
23
|
+
|
data/README.md
CHANGED
@@ -3,23 +3,23 @@ Poniard
|
|
3
3
|
|
4
4
|
A lightweight gem that provides an alternative to Rails controllers. It uses
|
5
5
|
parameter based dependency injection to explicitly make dependencies available,
|
6
|
-
rather than mixing them all in from a base class. This allows
|
7
|
-
|
8
|
-
|
9
|
-
|
6
|
+
rather than mixing them all in from a base class. This allows controllers to be
|
7
|
+
properly tested in isolation, bringing all the design benefits of TDD (as
|
8
|
+
opposed to "test-first" development, which is more common with the standard
|
9
|
+
integration style controller tests).
|
10
10
|
|
11
|
-
Poniard is designed to be compatible with standard controllers.
|
11
|
+
Poniard is designed to be compatible with standard controllers. It can be used
|
12
12
|
for your entire application, just one action, or anything in between.
|
13
13
|
|
14
14
|
Example
|
15
15
|
-------
|
16
16
|
|
17
17
|
A poniard controller is slightly more verbose than the what you may be used to.
|
18
|
-
In particular,
|
19
|
-
|
20
|
-
|
21
|
-
values
|
22
|
-
|
18
|
+
In particular, all the dependencies of a method (`response` in the following
|
19
|
+
example) must be declared as parameters to your method. Poniard will introspect
|
20
|
+
the method before calling, and ensure that the correct values are passed. These
|
21
|
+
values will for the most part be the same objects you normally deal with in
|
22
|
+
Rails (`session`, `flash`, etc...).
|
23
23
|
|
24
24
|
The following controller renders the default index template, setting the
|
25
25
|
instance variables `@message`.
|
@@ -34,12 +34,12 @@ module Controller
|
|
34
34
|
end
|
35
35
|
```
|
36
36
|
|
37
|
-
This is
|
38
|
-
|
39
|
-
|
40
|
-
|
37
|
+
This is differs from traditional controllers in two ways: passing variables to
|
38
|
+
the template is done with an explicit method call rather than instance variable
|
39
|
+
assignment, and dependencies that would normally be made available by
|
40
|
+
a superclass are passed in as parameters to the method.
|
41
41
|
|
42
|
-
Wiring this controller into
|
42
|
+
Wiring this controller into an application is a one-liner in the normal
|
43
43
|
controller definition.
|
44
44
|
|
45
45
|
```ruby
|
@@ -50,7 +50,7 @@ class RegistrationsController < ApplicationController
|
|
50
50
|
end
|
51
51
|
```
|
52
52
|
|
53
|
-
|
53
|
+
Traditional and poniard styles can be used together. Some actions can be
|
54
54
|
implemented in the normal controller, others can be provided by an injectable
|
55
55
|
one.
|
56
56
|
|
@@ -74,8 +74,8 @@ end
|
|
74
74
|
### Sources
|
75
75
|
|
76
76
|
Poniard knows about all the standard controller objects such as `response`,
|
77
|
-
`session` and `flash`.
|
78
|
-
|
77
|
+
`session` and `flash`. Domain specific definitions are then layered on top by
|
78
|
+
creating **sources**:
|
79
79
|
|
80
80
|
```ruby
|
81
81
|
class Source
|
@@ -91,7 +91,7 @@ class Source
|
|
91
91
|
end
|
92
92
|
```
|
93
93
|
|
94
|
-
|
94
|
+
This is wired up in the `provided_by` call:
|
95
95
|
|
96
96
|
```ruby
|
97
97
|
provided_by Controller::Registration, sources: [
|
@@ -99,8 +99,8 @@ provided_by Controller::Registration, sources: [
|
|
99
99
|
]
|
100
100
|
```
|
101
101
|
|
102
|
-
|
103
|
-
|
102
|
+
Any number of sources can be used, making it easy to reuse logic across
|
103
|
+
controllers.
|
104
104
|
|
105
105
|
Testing
|
106
106
|
-------
|
@@ -139,40 +139,23 @@ Techniques
|
|
139
139
|
|
140
140
|
### Built-in sources
|
141
141
|
|
142
|
-
|
143
|
-
|
142
|
+
See the [YARD docs][yard] for all the built in controller sources.
|
143
|
+
|
144
|
+
[yard]: http://rubydoc.info/github/xaviershay/poniard/master/Poniard/ControllerSource/
|
144
145
|
|
145
146
|
### Layouts
|
146
147
|
|
147
|
-
If
|
148
|
-
|
149
|
-
|
148
|
+
If a `layout` method is implemented in a controller, it will be used to select
|
149
|
+
a layout for the controller. This is equivalent to adding a custom `layout`
|
150
|
+
method to a standard controller.
|
150
151
|
|
151
152
|
### Mime types
|
152
153
|
|
153
|
-
The Rails `
|
154
|
-
Poniard provides
|
155
|
-
much easier to work with.
|
156
|
-
|
157
|
-
```ruby
|
158
|
-
module Controller
|
159
|
-
class Registration
|
160
|
-
def index(response, finder)
|
161
|
-
response.respond_with RegistrationsIndexResponse, finder.all
|
162
|
-
end
|
154
|
+
The Rails `respond_to` API is not very Object-Oriented, so is hard to test in
|
155
|
+
isolation. Poniard provides an alternative [`respond_with`][respond-with] that
|
156
|
+
allows you to provide a response object, which is much easier to work with.
|
163
157
|
|
164
|
-
|
165
|
-
def html(response)
|
166
|
-
response.default registrations: registrations
|
167
|
-
end
|
168
|
-
|
169
|
-
def json(response)
|
170
|
-
response.render json: registrations.to_json
|
171
|
-
end
|
172
|
-
end
|
173
|
-
end
|
174
|
-
end
|
175
|
-
```
|
158
|
+
[respond-with]: http://rubydoc.info/github/xaviershay/poniard/master/Poniard/ControllerSource/Response.html#respond_with-instance_method
|
176
159
|
|
177
160
|
### Authorization
|
178
161
|
|
@@ -182,19 +165,19 @@ can then be handled in a standard manner using `rescue_from`.
|
|
182
165
|
```ruby
|
183
166
|
module Source
|
184
167
|
class Admin
|
185
|
-
def
|
186
|
-
|
168
|
+
def current_admin(session)
|
169
|
+
User.find_by_id(session[:admin_id])
|
187
170
|
end
|
188
171
|
|
189
|
-
def
|
190
|
-
|
172
|
+
def authorized_admin(current_admin)
|
173
|
+
current_admin || raise(ResponseException::Unauthorized)
|
191
174
|
end
|
192
175
|
end
|
193
176
|
end
|
194
177
|
```
|
195
178
|
|
196
|
-
This can be slightly weird if the method
|
197
|
-
need to interact with the
|
179
|
+
This can be slightly weird if the method being authorized does not actually
|
180
|
+
need to interact with the admin, since it will have a method parameter that
|
198
181
|
is never used.
|
199
182
|
|
200
183
|
```ruby
|
@@ -209,7 +192,7 @@ def instance_method(name)
|
|
209
192
|
end
|
210
193
|
|
211
194
|
it 'requires authorization' do
|
212
|
-
instance_method(:index).should have_param(:
|
195
|
+
instance_method(:index).should have_param(:authorized_admin)
|
213
196
|
end
|
214
197
|
```
|
215
198
|
|
@@ -218,15 +201,12 @@ Developing
|
|
218
201
|
|
219
202
|
### Status
|
220
203
|
|
221
|
-
|
222
|
-
|
223
|
-
or it is even proved to be useful.
|
204
|
+
Not widely used. May be some obvious things missing from built-in controller
|
205
|
+
sources that you will have to add.
|
224
206
|
|
225
207
|
### Compatibility
|
226
208
|
|
227
|
-
Requires 1.9
|
228
|
-
1.9 style hashes and probably relies on `methods` calls returning symbols
|
229
|
-
rather than strings.
|
209
|
+
Requires 1.9 or above.
|
230
210
|
|
231
211
|
### Support
|
232
212
|
|
data/lib/poniard/controller.rb
CHANGED
@@ -1,17 +1,30 @@
|
|
1
1
|
module Poniard
|
2
|
+
# Mixing this module into a Rails controller provides the poniard DSL to that
|
3
|
+
# controller. To enable poniard on all controllers, mix this in to
|
4
|
+
# `ApplicationController`.
|
2
5
|
module Controller
|
6
|
+
# @private
|
3
7
|
def self.included(klass)
|
4
8
|
klass.extend(ClassMethods)
|
5
9
|
end
|
6
10
|
|
11
|
+
# Call the given method via the poniard injector. A `ControllerSource` is
|
12
|
+
# provided as default, along with any sources passed to `provided_by`.
|
7
13
|
def inject(method)
|
8
14
|
injector = Injector.new [
|
9
15
|
ControllerSource.new(self)
|
10
16
|
] + self.class.sources.map(&:new)
|
11
|
-
injector.
|
17
|
+
injector.eager_dispatch self.class.provided_by.new.method(method)
|
12
18
|
end
|
13
19
|
|
20
|
+
# Class methods that are automatically added when `Controller` is included.
|
14
21
|
module ClassMethods
|
22
|
+
# For every non-inherited public instance method on the given class,
|
23
|
+
# generates a method of the same name that calls it via the injector.
|
24
|
+
#
|
25
|
+
# If a `layout` method is present on the class, it is given special
|
26
|
+
# treatment and set up so that it will be called using the Rails `layout`
|
27
|
+
# DSL method.
|
15
28
|
def provided_by(klass = nil, opts = {})
|
16
29
|
if klass
|
17
30
|
methods = klass.public_instance_methods(false)
|
@@ -43,6 +56,10 @@ module Poniard
|
|
43
56
|
end
|
44
57
|
end
|
45
58
|
|
59
|
+
# An array of sources to be used for all injected methods on the host
|
60
|
+
# class. This is typically specified using the `sources` option to
|
61
|
+
# `provided_by`, however you can override it for more complicated dynamic
|
62
|
+
# behaviour.
|
46
63
|
def sources
|
47
64
|
@sources
|
48
65
|
end
|
@@ -1,26 +1,74 @@
|
|
1
1
|
module Poniard
|
2
|
+
# Poniard source providing access to Rails controller features. It is always
|
3
|
+
# available to poniard controllers added using
|
4
|
+
# {Poniard::Controller::ClassMethods#provided_by}.
|
2
5
|
class ControllerSource
|
3
|
-
|
6
|
+
# A wrapper around the Rails response that provides a more OO-friendly
|
7
|
+
# interface, specifically designed with isolated unit testing in mind.
|
8
|
+
#
|
9
|
+
# This class should not be explictly constructed by users. It is provided
|
10
|
+
# in the response parameter provided by {ControllerSource}.
|
11
|
+
class Response
|
12
|
+
# @private
|
13
|
+
attr_reader :controller, :injector
|
14
|
+
|
15
|
+
# @private
|
16
|
+
def initialize(controller, injector)
|
17
|
+
@controller = controller
|
18
|
+
@injector = injector
|
19
|
+
end
|
20
|
+
|
21
|
+
# Calls the given route method with arguments, then redirects to the
|
22
|
+
# result. +_path+ is automatically appended so does not need to be
|
23
|
+
# included in the path. +_url+ suffixes are handled correctly, in that
|
24
|
+
# they are used as is without adding a +_path+ suffix. That is a little
|
25
|
+
# magic, justified by +_path+ being what you want 90% of the time.
|
26
|
+
#
|
27
|
+
# If +path+ is a string, redirect to it as is without calling any route
|
28
|
+
# helpers.
|
29
|
+
#
|
30
|
+
# @example
|
31
|
+
# def index(response)
|
32
|
+
# response.redirect_to :user, 1 # user_path(1)
|
33
|
+
# end
|
4
34
|
def redirect_to(path, *args)
|
5
|
-
|
6
|
-
|
35
|
+
case path
|
36
|
+
when Symbol
|
37
|
+
unless path.to_s.ends_with?('_url')
|
38
|
+
path = "#{path}_path"
|
39
|
+
end
|
40
|
+
|
41
|
+
controller.redirect_to(controller.send(path, *args))
|
42
|
+
else
|
43
|
+
controller.redirect_to path, *args
|
7
44
|
end
|
8
|
-
|
9
|
-
controller.redirect_to(controller.send(path, *args))
|
10
45
|
end
|
11
46
|
|
47
|
+
# Redirect to the given action on the current controller.
|
12
48
|
def redirect_to_action(action)
|
13
49
|
controller.redirect_to action: action
|
14
50
|
end
|
15
51
|
|
52
|
+
# Render the view associated with given action with the given instance
|
53
|
+
# variables.
|
54
|
+
#
|
55
|
+
# @param action[Symbol/String] name of the action
|
56
|
+
# @param ivars[Hash] instance variables to be set for view rendering
|
16
57
|
def render_action(action, ivars = {})
|
17
58
|
render action: action, ivars: ivars
|
18
59
|
end
|
19
60
|
|
61
|
+
# Delegates directly to {ActionController::Head#head}.
|
20
62
|
def head(*args)
|
21
63
|
controller.head *args
|
22
64
|
end
|
23
65
|
|
66
|
+
# Delegates to `ActionController::Base#render`, with two exceptions if
|
67
|
+
# the last argument is a hash:
|
68
|
+
#
|
69
|
+
# * +ivars+ is deleted from the hash and used to set instance variables
|
70
|
+
# that can then be accessed by any templates.
|
71
|
+
# * +headers+ is deleted from the hash and used to set response headers.
|
24
72
|
def render(*args)
|
25
73
|
opts = args.last
|
26
74
|
if opts.is_a?(Hash)
|
@@ -41,43 +89,91 @@ module Poniard
|
|
41
89
|
controller.render *args
|
42
90
|
end
|
43
91
|
|
92
|
+
# Renders default template for the current action. Equivalent to not call
|
93
|
+
# any `render` method in a normal Rails controller. Unlike Rails, poniard
|
94
|
+
# does not support empty method bodies.
|
95
|
+
#
|
96
|
+
# @param ivars[Hash] instance variables to be set for view rendering
|
44
97
|
def default(ivars = {})
|
45
98
|
ivars.each do |name, val|
|
46
99
|
controller.instance_variable_set("@#{name}", val)
|
47
100
|
end
|
48
101
|
end
|
49
102
|
|
103
|
+
# Object-oriented replacement for
|
104
|
+
# {ActionController::MimeResponds#respond_to} method. The given class is
|
105
|
+
# instantiated with the remaining arguments, and is expected to implement
|
106
|
+
# a method named after each format it supports. The methods are called
|
107
|
+
# via the injector.
|
108
|
+
#
|
109
|
+
# If the requested format is not implemented, a 406 "Not Acceptable"
|
110
|
+
# response is returned.
|
111
|
+
#
|
112
|
+
# @example
|
113
|
+
# class MyController
|
114
|
+
# IndexResponse = Struct.new(:results) do
|
115
|
+
# def html(response)
|
116
|
+
# response.default results: results
|
117
|
+
# end
|
118
|
+
#
|
119
|
+
# def json(response)
|
120
|
+
# response.render json: results
|
121
|
+
# end
|
122
|
+
# end
|
123
|
+
#
|
124
|
+
# def index(response)
|
125
|
+
# response.respond_with IndexResponse, Things.all
|
126
|
+
# end
|
127
|
+
# end
|
50
128
|
def respond_with(klass, *args)
|
51
129
|
obj = klass.new(*args)
|
52
130
|
format = controller.request.format.symbol
|
53
131
|
if obj.respond_to?(format)
|
54
132
|
injector.dispatch obj.method(format)
|
133
|
+
else
|
134
|
+
head(406) # Not acceptable
|
55
135
|
end
|
56
136
|
end
|
57
137
|
|
138
|
+
# Delegates directly to {ActionController::DataStreaming#send_data}.
|
58
139
|
def send_data(*args)
|
59
140
|
controller.send_data(*args)
|
60
141
|
end
|
61
142
|
end
|
62
143
|
|
144
|
+
# @private
|
63
145
|
def initialize(controller)
|
64
146
|
@controller = controller
|
65
147
|
end
|
66
148
|
|
149
|
+
# Provides direct access to +request+.
|
67
150
|
def request; @controller.request; end
|
151
|
+
|
152
|
+
# Provides direct access to +params+.
|
68
153
|
def params; @controller.params; end
|
154
|
+
|
155
|
+
# Provides direct access to +session+.
|
69
156
|
def session; @controller.session; end
|
157
|
+
|
158
|
+
# Provides direct access to +flash+.
|
70
159
|
def flash; @controller.flash; end
|
160
|
+
|
161
|
+
# Provides access to +flash.now+. It is useful, particularly when testing,
|
162
|
+
# to treat this as a completely separate concept from +flash+.
|
71
163
|
def now_flash; @controller.flash.now; end
|
72
164
|
|
165
|
+
# Provides access to +render+ and friends abstracted behind {Response a
|
166
|
+
# nice OO interface}.
|
73
167
|
def response(injector)
|
74
168
|
Response.new(@controller, injector)
|
75
169
|
end
|
76
170
|
|
171
|
+
# Provides direct access to +Rails.env+.
|
77
172
|
def env
|
78
173
|
Rails.env
|
79
174
|
end
|
80
175
|
|
176
|
+
# Provides direct access to +Rails.application.config+.
|
81
177
|
def app_config
|
82
178
|
Rails.application.config
|
83
179
|
end
|
data/lib/poniard/injector.rb
CHANGED
@@ -1,52 +1,113 @@
|
|
1
|
-
require 'ostruct'
|
2
|
-
|
3
1
|
module Poniard
|
2
|
+
# A parameter pased dependency injector. Figures out which arguments to call
|
3
|
+
# a method with based on the names of method's parameters. Multiple sources
|
4
|
+
# can be provided to lookup parameter values. They are checked in reverse
|
5
|
+
# order.
|
6
|
+
#
|
7
|
+
# The injector itself is always available to methods via the +injector+
|
8
|
+
# parameter.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# def my_method(printer)
|
12
|
+
# printer.("hello!")
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# Injector.new([{
|
16
|
+
# printer: ->(msg) { puts msg }
|
17
|
+
# }]).dispatch(method(:my_method))
|
4
18
|
class Injector
|
5
19
|
attr_reader :sources
|
6
20
|
|
7
21
|
def initialize(sources = [])
|
8
22
|
@sources = sources.map {|source|
|
9
23
|
if source.is_a? Hash
|
10
|
-
|
24
|
+
HashSource.new(source)
|
11
25
|
else
|
12
|
-
source
|
26
|
+
ObjectSource.new(self, source)
|
13
27
|
end
|
14
|
-
}+ [self]
|
28
|
+
} + [HashSource.new(injector: self)]
|
15
29
|
end
|
16
30
|
|
31
|
+
# Call the given method with arguments. If a parameter is not provided by
|
32
|
+
# any source, an {UnknownInjectable} instance is passed instead.
|
17
33
|
def dispatch(method, overrides = {})
|
34
|
+
dispatch_method(method, UnknownInjectable.method(:new), overrides)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Same as {#dispatch}, except it raises an exception immediately if any
|
38
|
+
# parameter is not provided by sources.
|
39
|
+
def eager_dispatch(method, overrides = {})
|
40
|
+
dispatch_method(method, ->(name) {
|
41
|
+
::Kernel.raise UnknownParam, name
|
42
|
+
}, overrides)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def dispatch_method(method, unknown_param_f, overrides = {})
|
18
48
|
args = method.parameters.map {|_, name|
|
19
49
|
source = sources_for(overrides).detect {|source|
|
20
|
-
source.
|
50
|
+
source.provides?(name)
|
21
51
|
}
|
22
52
|
|
23
53
|
if source
|
24
|
-
|
54
|
+
source.dispatch(name, overrides)
|
25
55
|
else
|
26
|
-
|
56
|
+
unknown_param_f.(name)
|
27
57
|
end
|
28
58
|
}
|
29
59
|
method.(*args)
|
30
60
|
end
|
31
61
|
|
32
|
-
def
|
33
|
-
|
62
|
+
def sources_for(overrides)
|
63
|
+
[HashSource.new(overrides)] + sources
|
34
64
|
end
|
65
|
+
end
|
35
66
|
|
36
|
-
|
67
|
+
# @private
|
68
|
+
class HashSource
|
69
|
+
def initialize(hash)
|
70
|
+
@hash = hash
|
71
|
+
end
|
37
72
|
|
38
|
-
def
|
39
|
-
|
73
|
+
def provides?(name)
|
74
|
+
@hash.has_key?(name)
|
75
|
+
end
|
76
|
+
|
77
|
+
def dispatch(name, _)
|
78
|
+
@hash.fetch(name)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# @private
|
83
|
+
class ObjectSource
|
84
|
+
def initialize(injector, object)
|
85
|
+
@injector = injector
|
86
|
+
@object = object
|
87
|
+
end
|
88
|
+
|
89
|
+
def provides?(name)
|
90
|
+
@object.respond_to?(name)
|
91
|
+
end
|
92
|
+
|
93
|
+
def dispatch(name, overrides)
|
94
|
+
@injector.dispatch(@object.method(name), overrides)
|
40
95
|
end
|
41
96
|
end
|
42
97
|
|
98
|
+
# Raised by {Injector#eager_dispatch} if a parameter is not provided by any
|
99
|
+
# sources.
|
43
100
|
class UnknownParam < RuntimeError; end
|
44
101
|
|
102
|
+
# An object that will raise an exception if any method is called on it. This
|
103
|
+
# is particularly useful in testing so that you only need to inject the
|
104
|
+
# parameters that should actually be called during the test.
|
45
105
|
class UnknownInjectable < BasicObject
|
46
106
|
def initialize(name)
|
47
107
|
@name = name
|
48
108
|
end
|
49
109
|
|
110
|
+
# @private
|
50
111
|
def method_missing(*args)
|
51
112
|
::Kernel.raise UnknownParam,
|
52
113
|
"Tried to call method on an uninjected param: #{@name}"
|
data/lib/poniard/version.rb
CHANGED
data/poniard.gemspec
CHANGED
@@ -14,7 +14,7 @@ Gem::Specification.new do |gem|
|
|
14
14
|
gem.required_ruby_version = '>= 1.9.0'
|
15
15
|
gem.files = Dir.glob("{spec,lib}/**/*.rb") + %w(
|
16
16
|
README.md
|
17
|
-
|
17
|
+
CHANGELOG.md
|
18
18
|
LICENSE
|
19
19
|
poniard.gemspec
|
20
20
|
)
|
@@ -24,5 +24,4 @@ Gem::Specification.new do |gem|
|
|
24
24
|
gem.version = Poniard::VERSION
|
25
25
|
gem.has_rdoc = false
|
26
26
|
gem.add_development_dependency 'rspec', '~> 2.11'
|
27
|
-
gem.add_development_dependency 'rake'
|
28
27
|
end
|
@@ -0,0 +1,294 @@
|
|
1
|
+
require 'integration_helper'
|
2
|
+
|
3
|
+
describe Poniard::ControllerSource, type: :controller do
|
4
|
+
describe 'Response' do
|
5
|
+
context '#default' do
|
6
|
+
render_views
|
7
|
+
|
8
|
+
poniard_controller do
|
9
|
+
def index(response)
|
10
|
+
response.default message: "implicit"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'renders template matching method name with instance variables' do
|
15
|
+
get :index
|
16
|
+
expect(response.body).to match(/Message: implicit/)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context '#render' do
|
21
|
+
render_views
|
22
|
+
|
23
|
+
poniard_controller do
|
24
|
+
def index(response)
|
25
|
+
response.render action: 'index',
|
26
|
+
headers: {'Content-Type' => 'text/plain'},
|
27
|
+
ivars: {message: 'explicit'}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'renders explicit template with instance variables' do
|
32
|
+
get :index
|
33
|
+
expect(response.body).to match(/Message: explicit/)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'can set headers' do
|
37
|
+
get :index
|
38
|
+
expect(response.headers['Content-Type']).to eq('text/plain')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context '#render_action' do
|
43
|
+
render_views
|
44
|
+
|
45
|
+
poniard_controller do
|
46
|
+
def index(response)
|
47
|
+
response.render_action 'index', message: 'explicit'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'renders explicit template with instance variables' do
|
52
|
+
get :index
|
53
|
+
expect(response.body).to match(/Message: explicit/)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe '#head' do
|
58
|
+
poniard_controller do
|
59
|
+
def index(response)
|
60
|
+
response.head 422
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'sets response code' do
|
65
|
+
get :index
|
66
|
+
expect(response.status).to eq(422)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe '#redirect_to' do
|
71
|
+
# See fake admin_* route methods in integration_helper.rb
|
72
|
+
|
73
|
+
context 'path' do
|
74
|
+
poniard_controller do
|
75
|
+
def index(response)
|
76
|
+
response.redirect_to :admin, :secret
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'redirects' do
|
81
|
+
get :index
|
82
|
+
expect(response).to redirect_to("/admin/secret")
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context 'url' do
|
87
|
+
poniard_controller do
|
88
|
+
def index(response)
|
89
|
+
response.redirect_to :admin_url, :secret
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'redirects' do
|
94
|
+
get :index
|
95
|
+
expect(response).to redirect_to("http://foo/admin/secret")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
context 'arbitrary' do
|
100
|
+
poniard_controller do
|
101
|
+
def index(response)
|
102
|
+
response.redirect_to '/some_path'
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'redirects' do
|
107
|
+
get :index
|
108
|
+
expect(response).to redirect_to("/some_path")
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
context '#redirect_to_action' do
|
114
|
+
poniard_controller do
|
115
|
+
def index(response)
|
116
|
+
response.redirect_to_action :new
|
117
|
+
end
|
118
|
+
|
119
|
+
def new
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'redirects' do
|
124
|
+
pending "Not sure correct rspec-rails scaffolding to make this work"
|
125
|
+
get :index
|
126
|
+
expect(response).to redirect_to(:new)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe '#send_data' do
|
131
|
+
poniard_controller do
|
132
|
+
def index(response)
|
133
|
+
response.send_data "data", type: "text/plain"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'sends body' do
|
138
|
+
get :index
|
139
|
+
expect(response.body).to eq("data")
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'sets headers' do
|
143
|
+
get :index
|
144
|
+
expect(response.headers['Content-Type']).to eq("text/plain")
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
describe '#respond_with' do
|
149
|
+
render_views
|
150
|
+
|
151
|
+
poniard_controller do
|
152
|
+
def index(response)
|
153
|
+
responder = Class.new do
|
154
|
+
def initialize(body)
|
155
|
+
@body = body
|
156
|
+
end
|
157
|
+
|
158
|
+
def html(response); response.render text: "<b>#{@body}</b>" end
|
159
|
+
def text(response); response.render text: @body end
|
160
|
+
end
|
161
|
+
|
162
|
+
response.respond_with responder, "body"
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'responds with html' do
|
167
|
+
get :index
|
168
|
+
expect(response.body).to eq("<b>body</b>")
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'responds with text' do
|
172
|
+
get :index, format: :text
|
173
|
+
expect(response.body).to eq("body")
|
174
|
+
end
|
175
|
+
|
176
|
+
it 'handles unknown format' do
|
177
|
+
get :index, format: :json
|
178
|
+
expect(response.status).to eq(406)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
context 'request source' do
|
184
|
+
poniard_controller do
|
185
|
+
def index(response, request)
|
186
|
+
response.render text: request.path
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
it 'provides access to raw rails request' do
|
191
|
+
get :index
|
192
|
+
expect(response.body).to eq("/anonymous")
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
context 'params source' do
|
197
|
+
poniard_controller do
|
198
|
+
def index(response, params)
|
199
|
+
response.render text: params[:q]
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
it 'provides access to raw rails params' do
|
204
|
+
get :index, q: 'query'
|
205
|
+
expect(response.body).to eq("query")
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
context 'session source' do
|
210
|
+
poniard_controller do
|
211
|
+
def index(response, session)
|
212
|
+
response.render text: session[:user_id]
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
it 'provides access to raw rails session' do
|
217
|
+
get :index, {}, {user_id: '1'}
|
218
|
+
expect(response.body).to eq("1")
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
context 'flash source' do
|
223
|
+
poniard_controller do
|
224
|
+
def index(response, flash)
|
225
|
+
flash[:message] = "FLASH"
|
226
|
+
response.render text: ""
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
it 'provides access to raw rails flash' do
|
231
|
+
get :index
|
232
|
+
expect(flash[:message]).to eq("FLASH")
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
context 'flash now source' do
|
237
|
+
poniard_controller do
|
238
|
+
def index(response, now_flash)
|
239
|
+
now_flash[:message] = "FLASH"
|
240
|
+
response.render text: ""
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
it 'provides access to raw rails flash' do
|
245
|
+
get :index
|
246
|
+
expect(flash[:message]).to eq("FLASH")
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
context 'env source' do
|
251
|
+
poniard_controller do
|
252
|
+
def index(response, env)
|
253
|
+
response.render text: env
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
it 'provides access to current environment' do
|
258
|
+
get :index
|
259
|
+
expect(response.body).to eq("test")
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
context 'config source' do
|
264
|
+
poniard_controller do
|
265
|
+
def index(response, app_config)
|
266
|
+
response.render text: app_config.secret_token
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
it 'provides access to current config' do
|
271
|
+
get :index
|
272
|
+
expect(response.body).to eq(PoniardApp.config.secret_token)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
context 'with layout' do
|
277
|
+
render_views
|
278
|
+
|
279
|
+
poniard_controller do
|
280
|
+
def layout
|
281
|
+
'admin'
|
282
|
+
end
|
283
|
+
|
284
|
+
def index(response, app_config)
|
285
|
+
response.render text: "secret", layout: true
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
it 'uses the provided layout' do
|
290
|
+
get :index
|
291
|
+
expect(response.body).to match(/ADMIN: secret/)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
data/spec/injector_spec.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
1
3
|
require 'poniard/injector'
|
2
4
|
|
3
5
|
describe Poniard::Injector do
|
@@ -66,6 +68,13 @@ describe Poniard::Injector do
|
|
66
68
|
called.should == injector
|
67
69
|
end
|
68
70
|
|
71
|
+
it 'allows nil values in hash sources' do
|
72
|
+
value = nil
|
73
|
+
injector = described_class.new
|
74
|
+
injector.dispatch ->(x) { value = x.nil? }, x: nil
|
75
|
+
value.should == true
|
76
|
+
end
|
77
|
+
|
69
78
|
it 'yields a fail object when source is unknown' do
|
70
79
|
called = false
|
71
80
|
m = ->(unknown) {
|
@@ -80,4 +89,13 @@ describe Poniard::Injector do
|
|
80
89
|
described_class.new.dispatch(m)
|
81
90
|
called.should be_true
|
82
91
|
end
|
92
|
+
|
93
|
+
describe '#eager_dispatch' do
|
94
|
+
it 'raises when source is unknown' do
|
95
|
+
m = ->(unknown) {}
|
96
|
+
expect {
|
97
|
+
described_class.new.eager_dispatch(m)
|
98
|
+
}.to raise_error(Poniard::UnknownParam, "unknown")
|
99
|
+
end
|
100
|
+
end
|
83
101
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
ENV['RAILS_ENV'] = 'test'
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
require 'action_controller/railtie'
|
6
|
+
require 'rspec/rails'
|
7
|
+
|
8
|
+
require 'poniard'
|
9
|
+
|
10
|
+
Rails.backtrace_cleaner.remove_silencers!
|
11
|
+
class PoniardApp < Rails::Application
|
12
|
+
config.secret_token = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
|
13
|
+
config.session_store :cookie_store, :key => '_myproject_session'
|
14
|
+
config.active_support.deprecation = :log
|
15
|
+
config.eager_load = false
|
16
|
+
config.root = File.dirname(__FILE__)
|
17
|
+
end
|
18
|
+
PoniardApp.initialize!
|
19
|
+
|
20
|
+
class ApplicationController < ActionController::Base
|
21
|
+
before_filter :prepend_view_paths
|
22
|
+
|
23
|
+
def prepend_view_paths
|
24
|
+
prepend_view_path File.expand_path("../views", __FILE__)
|
25
|
+
end
|
26
|
+
|
27
|
+
def admin_path(page)
|
28
|
+
"/admin/#{page}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def admin_url(page)
|
32
|
+
"http://foo/admin/#{page}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
RSpec.configure do |config|
|
37
|
+
def poniard_controller(&block)
|
38
|
+
controller(ApplicationController) do
|
39
|
+
include Poniard::Controller
|
40
|
+
|
41
|
+
provided_by(Class.new(&block))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
CHANGED
@@ -1,43 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: poniard
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Xavier Shay
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-04-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - ~>
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '2.11'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - ~>
|
24
|
+
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '2.11'
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: rake
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - '>='
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '0'
|
34
|
-
type: :development
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - '>='
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '0'
|
41
27
|
description: A dependency injector for Rails, allows you to write clean controllers.
|
42
28
|
email:
|
43
29
|
- contact@xaviershay.com
|
@@ -45,16 +31,19 @@ executables: []
|
|
45
31
|
extensions: []
|
46
32
|
extra_rdoc_files: []
|
47
33
|
files:
|
48
|
-
-
|
34
|
+
- CHANGELOG.md
|
35
|
+
- LICENSE
|
36
|
+
- README.md
|
37
|
+
- lib/poniard.rb
|
49
38
|
- lib/poniard/controller.rb
|
50
39
|
- lib/poniard/controller_source.rb
|
51
40
|
- lib/poniard/injector.rb
|
52
41
|
- lib/poniard/version.rb
|
53
|
-
- lib/poniard.rb
|
54
|
-
- README.md
|
55
|
-
- HISTORY.md
|
56
|
-
- LICENSE
|
57
42
|
- poniard.gemspec
|
43
|
+
- spec/controller_source_spec.rb
|
44
|
+
- spec/injector_spec.rb
|
45
|
+
- spec/integration_helper.rb
|
46
|
+
- spec/spec_helper.rb
|
58
47
|
homepage: http://github.com/xaviershay/poniard
|
59
48
|
licenses: []
|
60
49
|
metadata: {}
|
@@ -64,20 +53,23 @@ require_paths:
|
|
64
53
|
- lib
|
65
54
|
required_ruby_version: !ruby/object:Gem::Requirement
|
66
55
|
requirements:
|
67
|
-
- -
|
56
|
+
- - ">="
|
68
57
|
- !ruby/object:Gem::Version
|
69
58
|
version: 1.9.0
|
70
59
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
60
|
requirements:
|
72
|
-
- -
|
61
|
+
- - ">="
|
73
62
|
- !ruby/object:Gem::Version
|
74
63
|
version: '0'
|
75
64
|
requirements: []
|
76
65
|
rubyforge_project:
|
77
|
-
rubygems_version: 2.0
|
66
|
+
rubygems_version: 2.2.0
|
78
67
|
signing_key:
|
79
68
|
specification_version: 4
|
80
69
|
summary: A dependency injector for Rails, allows you to write clean controllers.
|
81
70
|
test_files:
|
71
|
+
- spec/controller_source_spec.rb
|
82
72
|
- spec/injector_spec.rb
|
73
|
+
- spec/integration_helper.rb
|
74
|
+
- spec/spec_helper.rb
|
83
75
|
has_rdoc: false
|
data/HISTORY.md
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
# Poniard History
|
2
|
-
|
3
|
-
## 0.0.3 - 19 March 2014
|
4
|
-
|
5
|
-
* Support `head` responses.
|
6
|
-
|
7
|
-
## 0.0.2 - 26 January 2013
|
8
|
-
|
9
|
-
* Hashes can be used directly as sources.
|
10
|
-
* Provide default argument for `response.default`.
|
11
|
-
|
12
|
-
## 0.0.1 - 25 November 2012
|
13
|
-
|
14
|
-
* Initial release.
|
15
|
-
|