poniard 0.0.1
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 +5 -0
- data/LICENSE +14 -0
- data/README.md +220 -0
- data/lib/poniard.rb +3 -0
- data/lib/poniard/controller.rb +51 -0
- data/lib/poniard/controller_source.rb +81 -0
- data/lib/poniard/injector.rb +43 -0
- data/lib/poniard/version.rb +3 -0
- data/poniard.gemspec +28 -0
- data/spec/injector_spec.rb +15 -0
- metadata +88 -0
data/HISTORY.md
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
|
2
|
+
Copyright 2012 Xavier Shay
|
3
|
+
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
you may not use this file except in compliance with the License.
|
6
|
+
You may obtain a copy of the License at
|
7
|
+
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
See the License for the specific language governing permissions and
|
14
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,220 @@
|
|
1
|
+
Poniard
|
2
|
+
=======
|
3
|
+
|
4
|
+
A lightweight gem that provides an alternative to Rails controllers. It uses
|
5
|
+
parameter based dependency injection to explicitly make dependencies available,
|
6
|
+
rather than mixing them all in from a base class. This allows you to properly
|
7
|
+
unit test your controllers in isolation, bringing all the design benefits of
|
8
|
+
TDD (as opposed to "test-first" development which is more common with the
|
9
|
+
standard integration style controller tests).
|
10
|
+
|
11
|
+
Poniard is designed to be compatible with standard controllers. You can use it
|
12
|
+
for your entire application, just one action, or anything in between.
|
13
|
+
|
14
|
+
Example
|
15
|
+
-------
|
16
|
+
|
17
|
+
A poniard controller is slightly more verbose than the what you may be used to.
|
18
|
+
In particular, you need to specify all the different dependencies you wish to
|
19
|
+
use (`response` and `finder` in this example) as parameters to your method.
|
20
|
+
Poniard will introspect the method before calling, and ensure that the correct
|
21
|
+
values are passed. These values will for the most part be the same objects you
|
22
|
+
normally deal with in Rails (`session`, `flash`, etc...).
|
23
|
+
|
24
|
+
The following controller renders the default index template, setting the
|
25
|
+
instance variables `@message`.
|
26
|
+
|
27
|
+
module Controller
|
28
|
+
class Registration
|
29
|
+
def index(response)
|
30
|
+
response.default message: "hello"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
This is more explicit than traditional controllers in two ways: passing
|
36
|
+
variables to the template is done with an explicit method call rather than
|
37
|
+
instance variable assignment, and dependencies that would normally be made
|
38
|
+
available by a superclass are passed in as parameters to the method.
|
39
|
+
|
40
|
+
Wiring this controller into your application is a one-liner in your normal
|
41
|
+
controller definition.
|
42
|
+
|
43
|
+
class RegistrationsController < ApplicationController
|
44
|
+
include Poniard::Controller
|
45
|
+
|
46
|
+
provided_by Controller::Registration
|
47
|
+
end
|
48
|
+
|
49
|
+
You can mix and match traditional and poniard styles. Some actions can be
|
50
|
+
implemented in the normal controller, others can be provided by an injectable
|
51
|
+
one.
|
52
|
+
|
53
|
+
class RegistrationsController < ApplicationController
|
54
|
+
include Poniard::Controller
|
55
|
+
|
56
|
+
# index action provided by this class
|
57
|
+
provided_by Controller::Registration
|
58
|
+
|
59
|
+
# All controller features work in harmony with poniard, such as this
|
60
|
+
before_filter :require_user
|
61
|
+
|
62
|
+
# update action implemented normally
|
63
|
+
def update
|
64
|
+
# ...
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
### Sources
|
69
|
+
|
70
|
+
Poniard knows about all the standard controller objects such as `response`,
|
71
|
+
`session` and `flash`. You then layer your own domain specific definitions on
|
72
|
+
top by creating **sources**:
|
73
|
+
|
74
|
+
class Source
|
75
|
+
class Registration
|
76
|
+
def finder
|
77
|
+
Registration.accepted
|
78
|
+
end
|
79
|
+
|
80
|
+
def current_registration
|
81
|
+
Registration.find(params[:id])
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
Wire this up in the `provided_by` call:
|
87
|
+
|
88
|
+
provided_by Controller::Registration, sources: [
|
89
|
+
Source::Registration
|
90
|
+
]
|
91
|
+
|
92
|
+
You can specify as many sources as you like, making it easy to reuse logic
|
93
|
+
across controllers.
|
94
|
+
|
95
|
+
Testing
|
96
|
+
-------
|
97
|
+
|
98
|
+
Set up a common injector for the scope of your controller that knows about
|
99
|
+
common sources that all tests require (such as `response`). Add extra required
|
100
|
+
sources on a per test basis (`finder` in the below example).
|
101
|
+
|
102
|
+
require 'poniard/injector'
|
103
|
+
require 'controller/registration'
|
104
|
+
|
105
|
+
describe Controller::Registration do
|
106
|
+
let(:response) { double("Poniard::ControllerSource") }
|
107
|
+
let(:injector) { Poniard::Injector.new([OpenStruct.new(
|
108
|
+
response: response.as_null_object
|
109
|
+
)]) }
|
110
|
+
|
111
|
+
def dispatch(action, overrides = {})
|
112
|
+
injector.dispatch described_class.new.method(action), overrides
|
113
|
+
end
|
114
|
+
|
115
|
+
describe '#index' do
|
116
|
+
it 'should render default action with all registrations' do
|
117
|
+
finder = double(all: ['r1'])
|
118
|
+
response.should_receive(:default).with(registrations: ['r1'])
|
119
|
+
|
120
|
+
dispatch :index, finder: finder
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
Techniques
|
126
|
+
----------
|
127
|
+
|
128
|
+
### Built-in sources
|
129
|
+
|
130
|
+
Undocumented, but it is a pretty straight-forward mapping to Rails objects with
|
131
|
+
the exception of `response`. The code is in `lib/poniard/controller_source.rb`.
|
132
|
+
|
133
|
+
### Layouts
|
134
|
+
|
135
|
+
If you implement a `layout` method in your controller, it will be used to
|
136
|
+
select a layout for the controller. This is equivalent to adding a custom
|
137
|
+
`layout` method to a standard controller.
|
138
|
+
|
139
|
+
### Mime types
|
140
|
+
|
141
|
+
The Rails `respond_with` API is not very OO, so is hard to test in isolation.
|
142
|
+
Poniard provides a wrapper that allows you to provide a response object that is
|
143
|
+
much easier to work with.
|
144
|
+
|
145
|
+
module Controller
|
146
|
+
class Registration
|
147
|
+
def index(response, finder)
|
148
|
+
response.respond_with RegistrationsIndexResponse, finder.all
|
149
|
+
end
|
150
|
+
|
151
|
+
RegistrationsIndexResponse = Struct.new(:registrations) do
|
152
|
+
def html(response)
|
153
|
+
response.default registrations: registrations
|
154
|
+
end
|
155
|
+
|
156
|
+
def json(response)
|
157
|
+
response.render json: registrations.to_json
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
### Authorization
|
164
|
+
|
165
|
+
Poniard sources can raise exceptions to indicate authorization failure, which
|
166
|
+
can then be handled in a standard manner using `rescue_from`.
|
167
|
+
|
168
|
+
module Source
|
169
|
+
class Admin
|
170
|
+
def current_organiser(session)
|
171
|
+
Organiser.find_by_id(session[:organiser_id])
|
172
|
+
end
|
173
|
+
|
174
|
+
def authorized_organiser(current_organiser)
|
175
|
+
current_organiser || raise(ResponseException::Unauthorized)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
This can be slightly weird if the method you are authorizing does not actually
|
181
|
+
need to interact with the organiser, since it will have a method parameter that
|
182
|
+
is never used.
|
183
|
+
|
184
|
+
RSpec::Matchers.define :have_param do |attribute|
|
185
|
+
match do |obj|
|
186
|
+
obj.parameters.map(&:last).include?(attribute)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def instance_method(name)
|
191
|
+
described_class.new.method(name)
|
192
|
+
end
|
193
|
+
|
194
|
+
it 'requires authorization' do
|
195
|
+
instance_method(:index).should have_param(:authorized_organiser)
|
196
|
+
end
|
197
|
+
|
198
|
+
Developing
|
199
|
+
----------
|
200
|
+
|
201
|
+
### Status
|
202
|
+
|
203
|
+
Experimental. I've backported an existing app, added minor new features, and it
|
204
|
+
was a pleasant experience. It needs a lot more usage before the API stabilizes,
|
205
|
+
or it is even proved to be useful.
|
206
|
+
|
207
|
+
### Compatibility
|
208
|
+
|
209
|
+
Requires 1.9, should be easy to backport to 1.8 if anyone is interested. Use
|
210
|
+
1.9 style hashes and probably relies on `methods` calls returning symbols
|
211
|
+
rather than strings.
|
212
|
+
|
213
|
+
## Support
|
214
|
+
|
215
|
+
Make a [new github issue](https://github.com/xaviershay/poniard/issues/new).
|
216
|
+
|
217
|
+
## Contributing
|
218
|
+
|
219
|
+
Fork and patch! Please update the README and other documentation if you add
|
220
|
+
features.
|
data/lib/poniard.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
module Poniard
|
2
|
+
module Controller
|
3
|
+
def self.included(klass)
|
4
|
+
klass.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
def inject(method)
|
8
|
+
injector = Injector.new [
|
9
|
+
ControllerSource.new(self)
|
10
|
+
] + self.class.sources.map(&:new)
|
11
|
+
injector.dispatch self.class.provided_by.new.method(method)
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
def provided_by(klass = nil, opts = {})
|
16
|
+
if klass
|
17
|
+
methods = klass.public_instance_methods(false)
|
18
|
+
|
19
|
+
layout_method = methods.delete(:layout)
|
20
|
+
|
21
|
+
methods.each do |m|
|
22
|
+
class_eval <<-RUBY
|
23
|
+
def #{m}
|
24
|
+
inject :#{m}
|
25
|
+
end
|
26
|
+
RUBY
|
27
|
+
end
|
28
|
+
|
29
|
+
if layout_method
|
30
|
+
layout :layout_for_controller
|
31
|
+
|
32
|
+
class_eval <<-RUBY
|
33
|
+
def layout_for_controller
|
34
|
+
inject :layout
|
35
|
+
end
|
36
|
+
RUBY
|
37
|
+
end
|
38
|
+
|
39
|
+
@provided_by = klass
|
40
|
+
@sources = opts.fetch(:sources, []).reverse
|
41
|
+
else
|
42
|
+
@provided_by
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def sources
|
47
|
+
@sources
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Poniard
|
2
|
+
class ControllerSource
|
3
|
+
Response = Struct.new(:controller, :injector) do
|
4
|
+
def redirect_to(path, *args)
|
5
|
+
unless path.to_s.ends_with?('_url')
|
6
|
+
path = "#{path}_path"
|
7
|
+
end
|
8
|
+
|
9
|
+
controller.redirect_to(controller.send(path, *args))
|
10
|
+
end
|
11
|
+
|
12
|
+
def redirect_to_action(action)
|
13
|
+
controller.redirect_to action: action
|
14
|
+
end
|
15
|
+
|
16
|
+
def render_action(action, ivars = {})
|
17
|
+
render action: action, ivars: ivars
|
18
|
+
end
|
19
|
+
|
20
|
+
def render(*args)
|
21
|
+
opts = args.last
|
22
|
+
if opts.is_a?(Hash)
|
23
|
+
ivars = opts.delete(:ivars)
|
24
|
+
headers = opts.delete(:headers)
|
25
|
+
end
|
26
|
+
ivars ||= {}
|
27
|
+
headers ||= {}
|
28
|
+
|
29
|
+
ivars.each do |name, val|
|
30
|
+
controller.instance_variable_set("@#{name}", val)
|
31
|
+
end
|
32
|
+
|
33
|
+
headers.each do |name, val|
|
34
|
+
controller.headers[name] = val
|
35
|
+
end
|
36
|
+
|
37
|
+
controller.render *args
|
38
|
+
end
|
39
|
+
|
40
|
+
def default(ivars)
|
41
|
+
ivars.each do |name, val|
|
42
|
+
controller.instance_variable_set("@#{name}", val)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def respond_with(klass, *args)
|
47
|
+
obj = klass.new(*args)
|
48
|
+
format = controller.request.format.symbol
|
49
|
+
if obj.respond_to?(format)
|
50
|
+
injector.dispatch obj.method(format)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def send_data(*args)
|
55
|
+
controller.send_data(*args)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def initialize(controller)
|
60
|
+
@controller = controller
|
61
|
+
end
|
62
|
+
|
63
|
+
def request; @controller.request; end
|
64
|
+
def params; @controller.params; end
|
65
|
+
def session; @controller.session; end
|
66
|
+
def flash; @controller.flash; end
|
67
|
+
def now_flash; @controller.flash.now; end
|
68
|
+
|
69
|
+
def response(injector)
|
70
|
+
Response.new(@controller, injector)
|
71
|
+
end
|
72
|
+
|
73
|
+
def env
|
74
|
+
Rails.env
|
75
|
+
end
|
76
|
+
|
77
|
+
def app_config
|
78
|
+
Rails.application.config
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module Poniard
|
4
|
+
class Injector
|
5
|
+
attr_reader :sources
|
6
|
+
|
7
|
+
def initialize(sources)
|
8
|
+
@sources = sources + [self]
|
9
|
+
end
|
10
|
+
|
11
|
+
def dispatch(method, overrides = {})
|
12
|
+
args = method.parameters.map {|_, name|
|
13
|
+
source = (
|
14
|
+
[OpenStruct.new(overrides)] +
|
15
|
+
sources
|
16
|
+
).detect {|source| source.respond_to?(name) }
|
17
|
+
if source
|
18
|
+
dispatch(source.method(name), overrides)
|
19
|
+
else
|
20
|
+
UnknownInjectable.new(name)
|
21
|
+
end
|
22
|
+
}
|
23
|
+
method.call(*args)
|
24
|
+
end
|
25
|
+
|
26
|
+
def injector
|
27
|
+
self
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class UnknownParam < RuntimeError; end
|
32
|
+
|
33
|
+
class UnknownInjectable < BasicObject
|
34
|
+
def initialize(name)
|
35
|
+
@name = name
|
36
|
+
end
|
37
|
+
|
38
|
+
def method_missing(*args)
|
39
|
+
::Kernel.raise UnknownParam,
|
40
|
+
"Tried to call method on an uninjected param: #{@name}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/poniard.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/poniard/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Xavier Shay"]
|
6
|
+
gem.email = ["contact@xaviershay.com"]
|
7
|
+
gem.description =
|
8
|
+
%q{A dependency injector for Rails, allows you to write clean controllers.}
|
9
|
+
gem.summary =
|
10
|
+
%q{A dependency injector for Rails, allows you to write clean controllers.}
|
11
|
+
gem.homepage = "http://github.com/xaviershay/poniard"
|
12
|
+
|
13
|
+
gem.executables = []
|
14
|
+
gem.required_ruby_version = '>= 1.9.0'
|
15
|
+
gem.files = Dir.glob("{spec,lib}/**/*.rb") + %w(
|
16
|
+
README.md
|
17
|
+
HISTORY.md
|
18
|
+
LICENSE
|
19
|
+
poniard.gemspec
|
20
|
+
)
|
21
|
+
gem.test_files = Dir.glob("spec/**/*.rb")
|
22
|
+
gem.name = "poniard"
|
23
|
+
gem.require_paths = ["lib"]
|
24
|
+
gem.version = Poniard::VERSION
|
25
|
+
gem.has_rdoc = false
|
26
|
+
gem.add_development_dependency 'rspec', '~> 2.11'
|
27
|
+
gem.add_development_dependency 'rake'
|
28
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'poniard/injector'
|
2
|
+
|
3
|
+
describe Poniard::Injector do
|
4
|
+
it 'yields a fail object when source is unknown' do
|
5
|
+
called = false
|
6
|
+
m = ->(unknown) {
|
7
|
+
->{
|
8
|
+
unknown.bogus
|
9
|
+
}.should raise_error(Poniard::UnknownParam)
|
10
|
+
called = true
|
11
|
+
}
|
12
|
+
Poniard::Injector.new([]).dispatch(m)
|
13
|
+
called.should be_true
|
14
|
+
end
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: poniard
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Xavier Shay
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-11-25 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '2.11'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '2.11'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: A dependency injector for Rails, allows you to write clean controllers.
|
47
|
+
email:
|
48
|
+
- contact@xaviershay.com
|
49
|
+
executables: []
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files: []
|
52
|
+
files:
|
53
|
+
- spec/injector_spec.rb
|
54
|
+
- lib/poniard/controller.rb
|
55
|
+
- lib/poniard/controller_source.rb
|
56
|
+
- lib/poniard/injector.rb
|
57
|
+
- lib/poniard/version.rb
|
58
|
+
- lib/poniard.rb
|
59
|
+
- README.md
|
60
|
+
- HISTORY.md
|
61
|
+
- LICENSE
|
62
|
+
- poniard.gemspec
|
63
|
+
homepage: http://github.com/xaviershay/poniard
|
64
|
+
licenses: []
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options: []
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ! '>='
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 1.9.0
|
75
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
76
|
+
none: false
|
77
|
+
requirements:
|
78
|
+
- - ! '>='
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
requirements: []
|
82
|
+
rubyforge_project:
|
83
|
+
rubygems_version: 1.8.23
|
84
|
+
signing_key:
|
85
|
+
specification_version: 3
|
86
|
+
summary: A dependency injector for Rails, allows you to write clean controllers.
|
87
|
+
test_files:
|
88
|
+
- spec/injector_spec.rb
|