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 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