responder_controller 0.2.0 → 0.3.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.
@@ -0,0 +1,118 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+
3
+ module ResponderController
4
+ # Instance methods that support the Actions module.
5
+ module InstanceMethods
6
+ delegate :model_class_name, :model_class, :scopes, :responds_within, :serves_scopes,
7
+ :to => "self.class"
8
+
9
+ # Apply scopes to the given query.
10
+ #
11
+ # Applicable scopes come from two places. They are either declared at the class level with
12
+ # <tt>ClassMethods#scope</tt>, or named in the request itself. The former is good for
13
+ # defining topics or enforcing security, while the latter is free slicing and dicing for
14
+ # clients.
15
+ #
16
+ # Class-level scopes are applied first. Request scopes come after, and are discovered by
17
+ # examining +params+. If any +params+ key matches a name found in
18
+ # <tt>ClassMethods#model_class.scopes.keys</tt>, then it is taken to be a scope and is
19
+ # applied. The values under that +params+ key are passed along as arguments.
20
+ def scope(query)
21
+ query = (scopes || []).inject(query) do |query, scope|
22
+ if Symbol === scope and model_class.scopes.key? scope
23
+ query.send scope
24
+ elsif Proc === scope
25
+ instance_exec query, &scope
26
+ else
27
+ raise ArgumentError.new "Unknown scope #{model_class}.#{scope}"
28
+ end
29
+ end
30
+
31
+ requested = (model_class.scopes.keys & params.keys.collect { |k| k.to_sym })
32
+ raise ForbiddenScope if serves_scopes[:only] && (requested - serves_scopes[:only]).any?
33
+ raise ForbiddenScope if serves_scopes[:except] && (requested & serves_scopes[:except]).any?
34
+
35
+ query = requested.inject(query) do |query, scope|
36
+ query.send scope, *params[scope.to_s] rescue raise BadScope
37
+ end
38
+
39
+ query
40
+ end
41
+
42
+ # Find all models in #scope.
43
+ #
44
+ # The initial query is <tt>ClassMethods#model_class.scoped</tt>.
45
+ def find_models
46
+ scope model_class.scoped
47
+ end
48
+
49
+ # Find a particular model.
50
+ #
51
+ # #find_models is asked to find a model with <tt>params[:id]</tt>. This ensures that
52
+ # class-level scopes are enforced (potentially for security.)
53
+ def find_model
54
+ find_models.find(params[:id])
55
+ end
56
+
57
+ # The underscored model class name, as a symbol.
58
+ #
59
+ # Model modules are omitted.
60
+ def model_slug
61
+ model_class_name.split('/').last.to_sym
62
+ end
63
+
64
+ # Like #model_slug, but plural.
65
+ def models_slug
66
+ model_slug.to_s.pluralize.to_sym
67
+ end
68
+
69
+ # The name of the instance variable holding a single model instance.
70
+ def model_ivar
71
+ "@#{model_slug}"
72
+ end
73
+
74
+ # The name of the instance variable holding a collection of models.
75
+ def models_ivar
76
+ model_ivar.pluralize
77
+ end
78
+
79
+ # Retrive #models_ivar
80
+ def models
81
+ instance_variable_get models_ivar
82
+ end
83
+
84
+ # Assign #models_ivar
85
+ def models=(models)
86
+ instance_variable_set models_ivar, models
87
+ end
88
+
89
+ # Retrive #model_ivar
90
+ def model
91
+ instance_variable_get model_ivar
92
+ end
93
+
94
+ # Assign #model_ivar
95
+ def model=(model)
96
+ instance_variable_set model_ivar, model
97
+ end
98
+
99
+ # Apply ClassMethods#responds_within to the given model (or symbol.)
100
+ #
101
+ # "Apply" just means turning +responds_within+ into an array and appending +model+ to the
102
+ # end. If +responds_within+ is an array, it used directly.
103
+ #
104
+ # If it is a proc, it is called with +instance_exec+, passing +model+ in. It should return an
105
+ # array, which +model+ will be appended to. (So, don't include it in the return value.)
106
+ def responder_context(model)
107
+ context = responds_within.collect do |o|
108
+ o = instance_exec model, &o if o.is_a? Proc
109
+ o
110
+ end.flatten + [model]
111
+ end
112
+
113
+ # Pass +model+ through InstanceMethods#responder_context, and pass that to #respond_with.
114
+ def respond_with_contextual(model)
115
+ respond_with *responder_context(model)
116
+ end
117
+ end
118
+ end
@@ -5,7 +5,7 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{responder_controller}
8
- s.version = "0.2.0"
8
+ s.version = "0.3.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Phil Smith"]
@@ -25,7 +25,13 @@ Gem::Specification.new do |s|
25
25
  "Rakefile",
26
26
  "VERSION",
27
27
  "lib/responder_controller.rb",
28
+ "lib/responder_controller/actions.rb",
29
+ "lib/responder_controller/class_methods.rb",
30
+ "lib/responder_controller/instance_methods.rb",
28
31
  "responder_controller.gemspec",
32
+ "spec/responder_controller/actions_spec.rb",
33
+ "spec/responder_controller/class_methods_spec.rb",
34
+ "spec/responder_controller/instance_methods_spec.rb",
29
35
  "spec/responder_controller_spec.rb",
30
36
  "spec/spec.opts",
31
37
  "spec/spec_helper.rb"
@@ -36,7 +42,10 @@ Gem::Specification.new do |s|
36
42
  s.rubygems_version = %q{1.3.6}
37
43
  s.summary = %q{like resources_controller, but for rails 3 responders}
38
44
  s.test_files = [
39
- "spec/responder_controller_spec.rb",
45
+ "spec/responder_controller/actions_spec.rb",
46
+ "spec/responder_controller/class_methods_spec.rb",
47
+ "spec/responder_controller/instance_methods_spec.rb",
48
+ "spec/responder_controller_spec.rb",
40
49
  "spec/spec_helper.rb"
41
50
  ]
42
51
 
@@ -45,14 +54,14 @@ Gem::Specification.new do |s|
45
54
  s.specification_version = 3
46
55
 
47
56
  if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
48
- s.add_runtime_dependency(%q<activesupport>, [">= 3.0.0.beta2"])
57
+ s.add_runtime_dependency(%q<activesupport>, [">= 3.0.0.beta3"])
49
58
  s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
50
59
  else
51
- s.add_dependency(%q<activesupport>, [">= 3.0.0.beta2"])
60
+ s.add_dependency(%q<activesupport>, [">= 3.0.0.beta3"])
52
61
  s.add_dependency(%q<rspec>, [">= 1.2.9"])
53
62
  end
54
63
  else
55
- s.add_dependency(%q<activesupport>, [">= 3.0.0.beta2"])
64
+ s.add_dependency(%q<activesupport>, [">= 3.0.0.beta3"])
56
65
  s.add_dependency(%q<rspec>, [">= 1.2.9"])
57
66
  end
58
67
  end
@@ -0,0 +1,140 @@
1
+ require 'spec_helper'
2
+
3
+ describe ResponderController do
4
+
5
+ class ApplicationController
6
+ include ResponderController
7
+
8
+ def params
9
+ @params ||= { :user_id => 'me', :id => 7 }
10
+ end
11
+ end
12
+
13
+ class PostsController < ApplicationController
14
+ end
15
+
16
+ describe ResponderController::Actions do
17
+ before :each do
18
+ @controller = PostsController.new
19
+ @controller.stub!(:find_models).and_return(@posts = mock("some posts"))
20
+ @controller.stub!(:find_model).and_return(@post = mock("a post"))
21
+ @controller.stub!(:respond_with)
22
+
23
+ @posts.stub!(:build).and_return(@post)
24
+ @posts.stub!(:to_a).and_return([])
25
+ end
26
+
27
+ describe '#index' do
28
+ it 'assigns #find_models to #models=' do
29
+ @controller.should_receive(:find_models).and_return(@posts)
30
+ @controller.index
31
+ @controller.instance_variable_get('@posts').should == @posts
32
+ end
33
+
34
+ it '#respond_with_contextual @models.to_a' do
35
+ @posts.should_receive(:to_a).and_return(:posts_array)
36
+ @controller.should_receive(:respond_with_contextual).with(:posts_array)
37
+ @controller.index
38
+ end
39
+ end
40
+
41
+ [:show, :edit].each do |verb|
42
+ describe "##{verb}" do
43
+ it 'assigns #find_model to #model=' do
44
+ @controller.should_receive(:find_model).and_return(@post)
45
+ @controller.send verb
46
+ @controller.instance_variable_get('@post').should == @post
47
+ end
48
+
49
+ it '#respond_with_contextual @model' do
50
+ @controller.should_receive(:respond_with_contextual).with(@post)
51
+ @controller.send verb
52
+ end
53
+ end
54
+ end
55
+
56
+ describe '#new' do
57
+ it 'assigns #find_models.new to #model=' do
58
+ @posts.should_receive(:build).and_return(@post)
59
+ @controller.new
60
+ @controller.instance_variable_get('@post').should == @post
61
+ end
62
+
63
+ it '#respond_with_contextual @model' do
64
+ @controller.should_receive(:respond_with_contextual).with(@post)
65
+ @controller.new
66
+ end
67
+ end
68
+
69
+ describe '#create' do
70
+ before :each do
71
+ @post.stub!(:save)
72
+ end
73
+
74
+ it 'passes params[model_slug] to #find_models.new' do
75
+ @controller.params[:post] = :params_to_new
76
+ @posts.should_receive(:build).with(:params_to_new)
77
+ @controller.create
78
+ end
79
+
80
+ it 'assigns #find_models.new to #model=' do
81
+ @controller.create
82
+ @controller.instance_variable_get('@post').should == @post
83
+ end
84
+
85
+ it 'saves the model' do
86
+ @post.should_receive(:save)
87
+ @controller.create
88
+ end
89
+
90
+ it '#respond_with_contextual @model' do
91
+ @controller.should_receive(:respond_with_contextual).with(@post)
92
+ @controller.create
93
+ end
94
+ end
95
+
96
+ describe '#update' do
97
+ before :each do
98
+ @post.stub!(:update_attributes)
99
+ end
100
+
101
+ it 'assigns #find_model to #model=' do
102
+ @controller.should_receive(:find_model).and_return(@post)
103
+ @controller.update
104
+ @controller.instance_variable_get('@post').should == @post
105
+ end
106
+
107
+ it "updates the model's attributes" do
108
+ @controller.params[:post] = :params_to_update
109
+ @post.should_receive(:update_attributes).with(:params_to_update)
110
+ @controller.update
111
+ end
112
+
113
+ it '#respond_with_contextual @model' do
114
+ @controller.should_receive(:respond_with_contextual).with(@post)
115
+ @controller.update
116
+ end
117
+ end
118
+
119
+ describe '#destroy' do
120
+ before :each do
121
+ @post.stub!(:destroy)
122
+ end
123
+
124
+ it 'finds the model' do
125
+ @controller.should_receive(:find_model).and_return(@post)
126
+ @controller.destroy
127
+ end
128
+
129
+ it 'destroys the model' do
130
+ @post.should_receive(:destroy)
131
+ @controller.destroy
132
+ end
133
+
134
+ it '#respond_with_contextual #models_slug' do
135
+ @controller.should_receive(:respond_with_contextual).with(:posts)
136
+ @controller.destroy
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,266 @@
1
+ require 'spec_helper'
2
+
3
+ class Post
4
+ class <<self
5
+ def scopes
6
+ {
7
+ :recent => "a scope",
8
+ :unpublished => "another",
9
+ :authored_by => "and another",
10
+ :commented_on_by => "even one more",
11
+ :published_after => "man they just keep coming"
12
+ }
13
+ end
14
+ end
15
+ end
16
+
17
+ module Accounts
18
+ class User
19
+ end
20
+ end
21
+
22
+ module Admin
23
+ class Setting
24
+ end
25
+ end
26
+
27
+ describe ResponderController::ClassMethods do
28
+
29
+ class ApplicationController
30
+ include ResponderController
31
+
32
+ def params
33
+ @params ||= { :user_id => 'me', :id => 7 }
34
+ end
35
+ end
36
+
37
+ class PostsController < ApplicationController
38
+ end
39
+
40
+ class Admin::SettingsController < ApplicationController
41
+ end
42
+
43
+ before :each do
44
+ @query = mock("the scoped query")
45
+ @query.stub!(:unpublished).and_return(@query)
46
+ @query.stub!(:recent).and_return(@query)
47
+ @query.stub!(:owned_by).with('me').and_return(@query)
48
+ end
49
+
50
+ describe '.model_class_name', 'by default' do
51
+ it 'is taken from the controller class name' do
52
+ PostsController.model_class_name.should == 'post'
53
+ end
54
+
55
+ it "includes the controller's modules divided by whacks" do
56
+ Admin::SettingsController.model_class_name.should == 'admin/setting'
57
+ end
58
+ end
59
+
60
+ describe '.serves_model' do
61
+ it 'sets the model class name to the passed value' do
62
+ PostsController.serves_model 'some_other_model'
63
+ PostsController.model_class_name.should == 'some_other_model'
64
+ end
65
+
66
+ it 'accepts symbols as well as strings' do
67
+ PostsController.serves_model :some_other_model
68
+ PostsController.model_class_name.should == 'some_other_model'
69
+ end
70
+
71
+ it 'raises ArgumentError for other values' do
72
+ lambda do
73
+ PostsController.serves_model [:not_a, :string_or_symbol]
74
+ end.should raise_error ArgumentError
75
+ end
76
+
77
+ after :each do
78
+ PostsController.serves_model :post
79
+ end
80
+ end
81
+
82
+ describe '.model_class' do
83
+ it 'is the constant named by model_class_name' do
84
+ PostsController.model_class.should == Post
85
+ end
86
+
87
+ it 'handles modules in the name' do
88
+ Admin::SettingsController.model_class.should == Admin::Setting
89
+ end
90
+ end
91
+
92
+ describe '.scope' do
93
+ it 'takes a string naming a scope on model_class' do
94
+ PostsController.scope 'unpublished'
95
+ end
96
+
97
+ it 'can take a symbol' do
98
+ PostsController.scope :recent
99
+ end
100
+ end
101
+
102
+ describe '.scopes' do
103
+ it 'is an array of scopes in order, as symbols' do
104
+ PostsController.scopes.should == [:unpublished, :recent]
105
+ end
106
+ end
107
+
108
+ describe '.scope', 'with a block' do
109
+ it 'omits the name and can reference params' do
110
+ PostsController.scope do |posts|
111
+ posts.owned_by(params[:user_id])
112
+ end
113
+ end
114
+
115
+ it 'puts a lambda on .scopes' do
116
+ PostsController.scopes.last.should be_a Proc
117
+ end
118
+ end
119
+
120
+ describe '.scope', 'with something that is not a string, symbol or block' do
121
+ it 'explodes' do
122
+ lambda do
123
+ PostsController.scope [:not_a, :string_symbol_or_block]
124
+ end.should raise_error ArgumentError
125
+ end
126
+ end
127
+
128
+ describe '.serves_scopes' do
129
+ before :each do
130
+ @controller = PostsController.new
131
+ @controller.params['commented_on_by'] = 'you'
132
+ end
133
+
134
+ it 'can specify a white list of active record scopes to serve' do
135
+ PostsController.serves_scopes :only => [:recent, :authored_by, :published_after]
136
+ lambda do
137
+ @controller.scope @query
138
+ end.should raise_error(ResponderController::ForbiddenScope)
139
+ end
140
+
141
+ it 'can specify just one scope to white list' do
142
+ PostsController.serves_scopes :only => :recent
143
+ lambda do
144
+ @controller.scope @query
145
+ end.should raise_error(ResponderController::ForbiddenScope)
146
+ end
147
+
148
+ it 'can specify a black list of active record scopes to deny' do
149
+ PostsController.serves_scopes :except => [:commented_on_by, :unpublished]
150
+ lambda do
151
+ @controller.scope @query
152
+ end.should raise_error(ResponderController::ForbiddenScope)
153
+ end
154
+
155
+ it 'can specify just one scope to black list' do
156
+ PostsController.serves_scopes :except => :commented_on_by
157
+ lambda do
158
+ @controller.scope @query
159
+ end.should raise_error(ResponderController::ForbiddenScope)
160
+ end
161
+
162
+ it 'whines if passed anything other than a hash' do
163
+ lambda do
164
+ PostsController.serves_scopes 'cupcakes!'
165
+ end.should raise_error TypeError
166
+ end
167
+
168
+ it 'whines about keys other than :only and :except' do
169
+ lambda do
170
+ PostsController.serves_scopes 'only' => :recent
171
+ end.should raise_error ArgumentError
172
+ end
173
+
174
+ it 'whines when both :only and :except are passed' do
175
+ lambda do
176
+ PostsController.serves_scopes :only => :recent, :except => :commented_on_by
177
+ end.should raise_error ArgumentError
178
+ end
179
+
180
+ it 'whines if both :only and :except are passed between different calls' do
181
+ PostsController.serves_scopes :only => :recent
182
+ lambda do
183
+ PostsController.serves_scopes :except => :commented_on_by
184
+ end.should raise_error ArgumentError
185
+ end
186
+
187
+ it 'accumulates scopes passed over multiple calls' do
188
+ PostsController.serves_scopes :only => :recent
189
+ PostsController.serves_scopes :only => :authored_by
190
+ PostsController.serves_scopes[:only].should == [:recent, :authored_by]
191
+ end
192
+
193
+ after :each do
194
+ PostsController.serves_scopes.clear # clean up
195
+ end
196
+ end
197
+
198
+ describe '.responds_within' do
199
+ it "contains the model's enclosing module names as symbols by default" do
200
+ PostsController.responds_within.should == []
201
+ Admin::SettingsController.responds_within.should == [:admin]
202
+ end
203
+
204
+ it "takes, saves and returns a varargs" do
205
+ PostsController.responds_within(:foo, :bar, :baz).should == [:foo, :bar, :baz]
206
+ PostsController.responds_within.should == [:foo, :bar, :baz]
207
+ end
208
+
209
+ it "accumulates between calls" do
210
+ PostsController.responds_within(:foo).should == [:foo]
211
+ PostsController.responds_within(:bar, :baz).should == [:foo, :bar, :baz]
212
+ PostsController.responds_within.should == [:foo, :bar, :baz]
213
+ end
214
+
215
+ it "can take a block instead" do
216
+ block = lambda {}
217
+ PostsController.responds_within(&block).should == [block]
218
+ PostsController.responds_within.should == [block]
219
+ end
220
+
221
+ it "whines if both positional arguments and a block are passed" do
222
+ lambda do
223
+ PostsController.responds_within(:foo, :bar, :baz) {}
224
+ end.should raise_error ArgumentError
225
+ end
226
+
227
+ after :each do
228
+ PostsController.instance_variable_set "@responds_within", nil # clear out the garbage
229
+ end
230
+ end
231
+
232
+ describe '.children_of' do
233
+ it 'takes a underscored model class name' do
234
+ PostsController.children_of 'accounts/user'
235
+ end
236
+
237
+ it "can take symbols" do
238
+ PostsController.children_of 'accounts/user'.to_sym
239
+ end
240
+
241
+ it "creates a scope filtering by the parent model's foreign key as passed in params" do
242
+ PostsController.children_of 'accounts/user'
243
+ controller = PostsController.new
244
+ controller.params[:user_id] = :the_user_id
245
+
246
+ user_query = mock("user-restricted query")
247
+ @query.should_receive(:where).with(:user_id => :the_user_id).and_return(user_query)
248
+ controller.scope(@query).should == user_query
249
+ end
250
+
251
+ it "adds a responds_within context, of the parent modules followed by the parent itself" do
252
+ PostsController.children_of 'accounts/user'
253
+ controller = PostsController.new
254
+ controller.params[:user_id] = :the_user_id
255
+
256
+ Accounts::User.should_receive(:find).with(:the_user_id).and_return(user = mock("the user"))
257
+
258
+ controller.responder_context(:argument).should == [:accounts, user, :argument]
259
+ end
260
+
261
+ after :each do
262
+ PostsController.instance_variable_set "@responds_within", nil # clear out the garbage
263
+ PostsController.scopes.clear
264
+ end
265
+ end
266
+ end