poniard 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/HISTORY.md ADDED
@@ -0,0 +1,5 @@
1
+ # Poniard History
2
+
3
+ ## 0.0.1 - 25 November 2012
4
+
5
+ * Initial release
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,3 @@
1
+ require 'poniard/controller'
2
+ require 'poniard/controller_source'
3
+ require 'poniard/injector'
@@ -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
@@ -0,0 +1,3 @@
1
+ module Poniard
2
+ VERSION = '0.0.1'
3
+ 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