poniard 0.0.1 → 0.0.2
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.
- data/HISTORY.md +6 -1
- data/README.md +105 -87
- data/lib/poniard/controller_source.rb +1 -1
- data/lib/poniard/injector.rb +19 -7
- data/lib/poniard/version.rb +1 -1
- data/spec/injector_spec.rb +70 -2
- metadata +2 -2
data/HISTORY.md
CHANGED
data/README.md
CHANGED
@@ -24,13 +24,15 @@ normally deal with in Rails (`session`, `flash`, etc...).
|
|
24
24
|
The following controller renders the default index template, setting the
|
25
25
|
instance variables `@message`.
|
26
26
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
end
|
27
|
+
```ruby
|
28
|
+
module Controller
|
29
|
+
class Registration
|
30
|
+
def index(response)
|
31
|
+
response.default message: "hello"
|
33
32
|
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
```
|
34
36
|
|
35
37
|
This is more explicit than traditional controllers in two ways: passing
|
36
38
|
variables to the template is done with an explicit method call rather than
|
@@ -40,30 +42,34 @@ available by a superclass are passed in as parameters to the method.
|
|
40
42
|
Wiring this controller into your application is a one-liner in your normal
|
41
43
|
controller definition.
|
42
44
|
|
43
|
-
|
44
|
-
|
45
|
+
```ruby
|
46
|
+
class RegistrationsController < ApplicationController
|
47
|
+
include Poniard::Controller
|
45
48
|
|
46
|
-
|
47
|
-
|
49
|
+
provided_by Controller::Registration
|
50
|
+
end
|
51
|
+
```
|
48
52
|
|
49
53
|
You can mix and match traditional and poniard styles. Some actions can be
|
50
54
|
implemented in the normal controller, others can be provided by an injectable
|
51
55
|
one.
|
52
56
|
|
53
|
-
|
54
|
-
|
57
|
+
```ruby
|
58
|
+
class RegistrationsController < ApplicationController
|
59
|
+
include Poniard::Controller
|
55
60
|
|
56
|
-
|
57
|
-
|
61
|
+
# index action provided by this class
|
62
|
+
provided_by Controller::Registration
|
58
63
|
|
59
|
-
|
60
|
-
|
64
|
+
# All controller features work in harmony with poniard, such as this
|
65
|
+
before_filter :require_user
|
61
66
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
+
# update action implemented normally
|
68
|
+
def update
|
69
|
+
# ...
|
70
|
+
end
|
71
|
+
end
|
72
|
+
```
|
67
73
|
|
68
74
|
### Sources
|
69
75
|
|
@@ -71,23 +77,27 @@ Poniard knows about all the standard controller objects such as `response`,
|
|
71
77
|
`session` and `flash`. You then layer your own domain specific definitions on
|
72
78
|
top by creating **sources**:
|
73
79
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
80
|
+
```ruby
|
81
|
+
class Source
|
82
|
+
class Registration
|
83
|
+
def finder
|
84
|
+
Registration.accepted
|
85
|
+
end
|
79
86
|
|
80
|
-
|
81
|
-
|
82
|
-
end
|
83
|
-
end
|
87
|
+
def current_registration
|
88
|
+
Registration.find(params[:id])
|
84
89
|
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
```
|
85
93
|
|
86
94
|
Wire this up in the `provided_by` call:
|
87
95
|
|
88
|
-
|
89
|
-
|
90
|
-
|
96
|
+
```ruby
|
97
|
+
provided_by Controller::Registration, sources: [
|
98
|
+
Source::Registration
|
99
|
+
]
|
100
|
+
```
|
91
101
|
|
92
102
|
You can specify as many sources as you like, making it easy to reuse logic
|
93
103
|
across controllers.
|
@@ -99,28 +109,30 @@ Set up a common injector for the scope of your controller that knows about
|
|
99
109
|
common sources that all tests require (such as `response`). Add extra required
|
100
110
|
sources on a per test basis (`finder` in the below example).
|
101
111
|
|
102
|
-
|
103
|
-
|
112
|
+
```ruby
|
113
|
+
require 'poniard/injector'
|
114
|
+
require 'controller/registration'
|
104
115
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
116
|
+
describe Controller::Registration do
|
117
|
+
let(:response) { double("Poniard::ControllerSource") }
|
118
|
+
let(:injector) { Poniard::Injector.new([
|
119
|
+
response: response.as_null_object
|
120
|
+
]) }
|
110
121
|
|
111
|
-
|
112
|
-
|
113
|
-
|
122
|
+
def dispatch(action, overrides = {})
|
123
|
+
injector.dispatch described_class.new.method(action), overrides
|
124
|
+
end
|
114
125
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
126
|
+
describe '#index' do
|
127
|
+
it 'should render default action with all registrations' do
|
128
|
+
finder = double(all: ['r1'])
|
129
|
+
response.should_receive(:default).with(registrations: ['r1'])
|
119
130
|
|
120
|
-
|
121
|
-
end
|
122
|
-
end
|
131
|
+
dispatch :index, finder: finder
|
123
132
|
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
```
|
124
136
|
|
125
137
|
Techniques
|
126
138
|
----------
|
@@ -142,58 +154,64 @@ The Rails `respond_with` API is not very OO, so is hard to test in isolation.
|
|
142
154
|
Poniard provides a wrapper that allows you to provide a response object that is
|
143
155
|
much easier to work with.
|
144
156
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
end
|
157
|
+
```ruby
|
158
|
+
module Controller
|
159
|
+
class Registration
|
160
|
+
def index(response, finder)
|
161
|
+
response.respond_with RegistrationsIndexResponse, finder.all
|
162
|
+
end
|
163
|
+
|
164
|
+
RegistrationsIndexResponse = Struct.new(:registrations) do
|
165
|
+
def html(response)
|
166
|
+
response.default registrations: registrations
|
167
|
+
end
|
168
|
+
|
169
|
+
def json(response)
|
170
|
+
response.render json: registrations.to_json
|
160
171
|
end
|
161
172
|
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
```
|
162
176
|
|
163
177
|
### Authorization
|
164
178
|
|
165
179
|
Poniard sources can raise exceptions to indicate authorization failure, which
|
166
180
|
can then be handled in a standard manner using `rescue_from`.
|
167
181
|
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
182
|
+
```ruby
|
183
|
+
module Source
|
184
|
+
class Admin
|
185
|
+
def current_organiser(session)
|
186
|
+
Organiser.find_by_id(session[:organiser_id])
|
187
|
+
end
|
173
188
|
|
174
|
-
|
175
|
-
|
176
|
-
end
|
177
|
-
end
|
189
|
+
def authorized_organiser(current_organiser)
|
190
|
+
current_organiser || raise(ResponseException::Unauthorized)
|
178
191
|
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
```
|
179
195
|
|
180
196
|
This can be slightly weird if the method you are authorizing does not actually
|
181
197
|
need to interact with the organiser, since it will have a method parameter that
|
182
198
|
is never used.
|
183
199
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
200
|
+
```ruby
|
201
|
+
RSpec::Matchers.define :have_param do |attribute|
|
202
|
+
match do |obj|
|
203
|
+
obj.parameters.map(&:last).include?(attribute)
|
204
|
+
end
|
205
|
+
end
|
189
206
|
|
190
|
-
|
191
|
-
|
192
|
-
|
207
|
+
def instance_method(name)
|
208
|
+
described_class.new.method(name)
|
209
|
+
end
|
193
210
|
|
194
|
-
|
195
|
-
|
196
|
-
|
211
|
+
it 'requires authorization' do
|
212
|
+
instance_method(:index).should have_param(:authorized_organiser)
|
213
|
+
end
|
214
|
+
```
|
197
215
|
|
198
216
|
Developing
|
199
217
|
----------
|
@@ -210,11 +228,11 @@ Requires 1.9, should be easy to backport to 1.8 if anyone is interested. Use
|
|
210
228
|
1.9 style hashes and probably relies on `methods` calls returning symbols
|
211
229
|
rather than strings.
|
212
230
|
|
213
|
-
|
231
|
+
### Support
|
214
232
|
|
215
233
|
Make a [new github issue](https://github.com/xaviershay/poniard/issues/new).
|
216
234
|
|
217
|
-
|
235
|
+
### Contributing
|
218
236
|
|
219
237
|
Fork and patch! Please update the README and other documentation if you add
|
220
238
|
features.
|
data/lib/poniard/injector.rb
CHANGED
@@ -4,28 +4,40 @@ module Poniard
|
|
4
4
|
class Injector
|
5
5
|
attr_reader :sources
|
6
6
|
|
7
|
-
def initialize(sources)
|
8
|
-
@sources = sources
|
7
|
+
def initialize(sources = [])
|
8
|
+
@sources = sources.map {|source|
|
9
|
+
if source.is_a? Hash
|
10
|
+
OpenStruct.new(source)
|
11
|
+
else
|
12
|
+
source
|
13
|
+
end
|
14
|
+
}+ [self]
|
9
15
|
end
|
10
16
|
|
11
17
|
def dispatch(method, overrides = {})
|
12
18
|
args = method.parameters.map {|_, name|
|
13
|
-
source = (
|
14
|
-
|
15
|
-
|
16
|
-
|
19
|
+
source = sources_for(overrides).detect {|source|
|
20
|
+
source.respond_to?(name)
|
21
|
+
}
|
22
|
+
|
17
23
|
if source
|
18
24
|
dispatch(source.method(name), overrides)
|
19
25
|
else
|
20
26
|
UnknownInjectable.new(name)
|
21
27
|
end
|
22
28
|
}
|
23
|
-
method.
|
29
|
+
method.(*args)
|
24
30
|
end
|
25
31
|
|
26
32
|
def injector
|
27
33
|
self
|
28
34
|
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def sources_for(overrides)
|
39
|
+
[OpenStruct.new(overrides)] + sources
|
40
|
+
end
|
29
41
|
end
|
30
42
|
|
31
43
|
class UnknownParam < RuntimeError; end
|
data/lib/poniard/version.rb
CHANGED
data/spec/injector_spec.rb
CHANGED
@@ -1,15 +1,83 @@
|
|
1
1
|
require 'poniard/injector'
|
2
2
|
|
3
3
|
describe Poniard::Injector do
|
4
|
+
let(:thing) { Object.new }
|
5
|
+
|
6
|
+
it 'calls a lambda' do
|
7
|
+
called = false
|
8
|
+
described_class.new([]).dispatch -> { called = true }
|
9
|
+
called.should == true
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'calls a method' do
|
13
|
+
object = Class.new do
|
14
|
+
def a_number
|
15
|
+
12345
|
16
|
+
end
|
17
|
+
end.new
|
18
|
+
|
19
|
+
described_class.new([]).dispatch(object.method(:a_number)).should == 12345
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'calls a method with a parameter provided by a source' do
|
23
|
+
called = false
|
24
|
+
described_class.new([
|
25
|
+
thing: thing
|
26
|
+
]).dispatch ->(thing) { called = thing }
|
27
|
+
called.should == thing
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'recursively injects sources' do
|
31
|
+
two_source = Class.new do
|
32
|
+
def two_things(thing)
|
33
|
+
[thing, thing]
|
34
|
+
end
|
35
|
+
end.new
|
36
|
+
|
37
|
+
called = false
|
38
|
+
described_class.new([
|
39
|
+
{thing: thing},
|
40
|
+
two_source
|
41
|
+
]).dispatch ->(two_things) { called = two_things }
|
42
|
+
called.should == [thing, thing]
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'uses the first source that provides a parameter' do
|
46
|
+
called = false
|
47
|
+
described_class.new([
|
48
|
+
{thing: thing},
|
49
|
+
{thing: nil}
|
50
|
+
]).dispatch ->(thing) { called = thing }
|
51
|
+
called.should == thing
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'allows sources to be overriden at dispatch' do
|
55
|
+
called = false
|
56
|
+
described_class.new([
|
57
|
+
{thing: nil}
|
58
|
+
]).dispatch ->(thing) { called = thing }, thing: thing
|
59
|
+
called.should == thing
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'provides itself as a source' do
|
63
|
+
called = false
|
64
|
+
injector = described_class.new
|
65
|
+
injector.dispatch ->(injector) { called = injector }
|
66
|
+
called.should == injector
|
67
|
+
end
|
68
|
+
|
4
69
|
it 'yields a fail object when source is unknown' do
|
5
70
|
called = false
|
6
71
|
m = ->(unknown) {
|
7
72
|
->{
|
8
73
|
unknown.bogus
|
9
|
-
}.should raise_error(
|
74
|
+
}.should raise_error(
|
75
|
+
Poniard::UnknownParam,
|
76
|
+
"Tried to call method on an uninjected param: unknown"
|
77
|
+
)
|
10
78
|
called = true
|
11
79
|
}
|
12
|
-
|
80
|
+
described_class.new.dispatch(m)
|
13
81
|
called.should be_true
|
14
82
|
end
|
15
83
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: poniard
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-01-26 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|