responder_controller 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Phil Smith
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,93 @@
1
+ = responder_controller
2
+
3
+ Rails 3 responders wrap up as much crud controller code as possible without finding or mutating
4
+ models on your behalf. This is a sensible cut-off for framework support, but it still leaves a
5
+ fair amount of duplicate code in crud controllers. App developers are free to abstract more.
6
+
7
+ This is me abstracting more for my own apps. If it's handy for you, go nuts.
8
+
9
+ == Example
10
+
11
+ # app/models/post.rb
12
+ class Post < ActiveRecord::Base
13
+ belongs_to :user
14
+
15
+ scope :authored_by, lambda { |user_id| where(:user_id => user_id) }
16
+ scope :recent, lambda { |count| order("updated_at DESC").limit(limit.to_i) }
17
+ end
18
+
19
+ # app/controllers/posts_controller.rb
20
+ class PostsController < ApplicationController
21
+ include ResponderController
22
+
23
+ respond_to :html, :xml, :json
24
+
25
+ # restrict to just the current user's posts
26
+ scope { |posts| posts.authored_by current_user.id }
27
+ end
28
+
29
+ # Client-side
30
+ GET /posts.html # renders Post.authored_by(your_id)
31
+ GET /posts.html?recent=10 # renders Post.authored_by(your_id).recent(10)
32
+ GET /posts/1.html # renders post 1 if it is authored by you, otherwise 404
33
+ PUT /posts/1.html # update same
34
+ DELETE /posts/1.html # or delete it
35
+
36
+ === Point it at a different model class:
37
+
38
+ class ProfilesController < ApplicationController
39
+ include ResponderController
40
+ serves_model :user
41
+ end
42
+
43
+ === Serve resources in a namespace:
44
+
45
+ class PostsController < ApplicationController
46
+ include ResponderController
47
+ responds_within 'my-blog'
48
+ end
49
+
50
+ # Client-side
51
+ GET /my-blog/posts.html
52
+
53
+ === A nested resource, using blocks for dynamic behavior:
54
+
55
+ class CommentsController < ApplicationController
56
+ include ResponderController
57
+
58
+ # Only get comments for the identified post
59
+ scope do |comments|
60
+ comments.where :post_id => params[:post_id]
61
+ end
62
+
63
+ # Nest the comments under the post
64
+ responds_within do |comments|
65
+ Post.find(params[:post_id])
66
+ end
67
+ end
68
+
69
+ === The same:
70
+
71
+ class CommentsController < ApplicationController
72
+ include ResponderController
73
+ children_of :post
74
+ end
75
+
76
+ == Note on Patches/Pull Requests
77
+
78
+ * Fork the project.
79
+ * Make your feature addition or bug fix.
80
+ * Add tests for it. This is important so I don't break it in a
81
+ future version unintentionally.
82
+ * Commit, do not mess with rakefile, version, or history.
83
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can
84
+ ignore when I pull)
85
+ * Send me a pull request. Bonus points for topic branches.
86
+
87
+ == Thanks
88
+
89
+ Thanks to SEOmoz (http://seomoz.org) for letting me build this at my desk in the afternoons instead of on the couch in the middle of the night ^_^.
90
+
91
+ == Copyright
92
+
93
+ Copyright (c) 2010 Phil Smith. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,46 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "responder_controller"
8
+ gem.summary = %Q{like resources_controller, but for rails 3 responders}
9
+ gem.description = %Q{Responders make crud controllers tiny, this wraps the rest.}
10
+ gem.email = "phil.h.smith@gmail.com"
11
+ gem.homepage = "http://github.com/phs/responder_controller"
12
+ gem.authors = ["Phil Smith"]
13
+ gem.add_dependency "activesupport", ">= 3.0.0.beta2"
14
+ gem.add_development_dependency "rspec", ">= 1.2.9"
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ end
17
+ Jeweler::GemcutterTasks.new
18
+ rescue LoadError
19
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
20
+ end
21
+
22
+ require 'spec/rake/spectask'
23
+ Spec::Rake::SpecTask.new(:spec) do |spec|
24
+ spec.libs << 'lib' << 'spec'
25
+ spec.spec_files = FileList['spec/**/*_spec.rb']
26
+ end
27
+
28
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
29
+ spec.libs << 'lib' << 'spec'
30
+ spec.pattern = 'spec/**/*_spec.rb'
31
+ spec.rcov = true
32
+ end
33
+
34
+ task :spec => :check_dependencies
35
+
36
+ task :default => :spec
37
+
38
+ require 'rake/rdoctask'
39
+ Rake::RDocTask.new do |rdoc|
40
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
41
+
42
+ rdoc.rdoc_dir = 'rdoc'
43
+ rdoc.title = "responder_controller #{version}"
44
+ rdoc.rdoc_files.include('README*')
45
+ rdoc.rdoc_files.include('lib/**/*.rb')
46
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,278 @@
1
+ require 'active_support/inflector'
2
+ require 'active_support/core_ext/string/inflections'
3
+ require 'active_support/core_ext/module/delegation'
4
+
5
+ module ResponderController
6
+ def self.included(mod)
7
+ mod.extend ClassMethods
8
+ mod.send :include, InstanceMethods
9
+ mod.send :include, Actions
10
+ end
11
+
12
+ # Configure how the controller finds and serves models of what flavor.
13
+ module ClassMethods
14
+ # The underscored, fully-qualified name of the served model class.
15
+ #
16
+ # By default, it is the underscored controller class name, without +_controller+.
17
+ def model_class_name
18
+ @model_class_name || name.underscore.gsub(/_controller$/, '').singularize
19
+ end
20
+
21
+ # Declare the underscored, fully-qualified name of the served model class.
22
+ #
23
+ # Modules are declared with separating slashes, such as in <tt>admin/setting</tt>. Strings
24
+ # or symbols are accepted, but other values (including actual classes) will raise
25
+ # <tt>ArgumentError</tt>s.
26
+ def serves_model(model_class_name)
27
+ unless model_class_name.is_a? String or model_class_name.is_a? Symbol
28
+ raise ArgumentError.new "Model must be a string or symbol"
29
+ end
30
+
31
+ @model_class_name = model_class_name.to_s
32
+ end
33
+
34
+ # Declare leading arguments ("responder context") for +respond_with+ calls.
35
+ #
36
+ # +respond_with+ creates urls from models. To avoid strongly coupling models to a url
37
+ # structure, it can take any number of leading parameters a la +polymorphic_url+.
38
+ # +responds_within+ declares these leading parameters, to be used on each +respond_with+ call.
39
+ #
40
+ # It takes either a varargs or a block, but not both. In
41
+ # InstanceMethods#respond_with_contextual, the blocks are called with +instance_exec+, taking
42
+ # the model (or models) as a parameter. They should return an array.
43
+ def responds_within(*args, &block)
44
+ if block and args.any?
45
+ raise ArgumentError.new("responds_within can take arguments or a block, but not both")
46
+ elsif block or args.any?
47
+ @responds_within ||= []
48
+ if not args.empty?
49
+ @responds_within.concat args
50
+ else
51
+ @responds_within << block
52
+ end
53
+ end
54
+
55
+ @responds_within || model_class_name.split('/')[0...-1].collect { |m| m.to_sym }
56
+ end
57
+
58
+ # The served model class, identified by #model_class_name.
59
+ def model_class
60
+ model_class_name.camelize.constantize
61
+ end
62
+
63
+ # Declare a class-level scope for model collections.
64
+ #
65
+ # The model class is expected to respond to +all+, returning an Enumerable of models.
66
+ # Declared scopes are applied to (and replace) this collection, suitable for active record
67
+ # scopes.
68
+ #
69
+ # It takes one of a string, symbol or block. Symbols and strings are called as methods on the
70
+ # collection without arguments. Blocks are called with +instance_exec+ taking the current,
71
+ # accumulated query and returning the new, scoped one.
72
+ def scope(*args, &block)
73
+ scope = args.first || block
74
+
75
+ scope = scope.to_sym if String === scope
76
+ unless scope.is_a? Symbol or scope.is_a? Proc
77
+ raise ArgumentError.new "Scope must be a string, symbol or block"
78
+ end
79
+
80
+ (@scopes ||= []) << scope
81
+ end
82
+
83
+ # The array of declared class-level scopes, as symbols or procs.
84
+ attr_reader :scopes
85
+
86
+ # Declare a (non-singleton) parent resource class.
87
+ #
88
+ # <tt>children_of 'accounts/user'</tt> implies a scope and some responder context. The scope
89
+ # performs an ActiveRecord <tt>where :user_id => params[:user_id]</tt>. The responder context
90
+ # is a call to <tt>#responds_within</tt> declaring the parent model's modules along with the
91
+ # parent itself, found with <tt>Accounts::User.find(params[:user_id])</tt>.
92
+ def children_of(parent_model_class_name)
93
+ parent_model_class_name = parent_model_class_name.to_s.underscore
94
+
95
+ parent_name_parts = parent_model_class_name.split('/')
96
+ parent_modules = parent_name_parts[0...-1].collect(&:to_sym)
97
+ parent_id = "#{parent_name_parts.last}_id".to_sym
98
+
99
+ scope do |query|
100
+ query.where parent_id => params[parent_id]
101
+ end
102
+
103
+ responds_within do
104
+ parent = parent_model_class_name.camelize.constantize.find params[parent_id]
105
+ parent_modules + [parent]
106
+ end
107
+ end
108
+ end
109
+
110
+ # Instance methods that support the Actions module.
111
+ module InstanceMethods
112
+ delegate :model_class_name, :model_class, :scopes, :responds_within, :to => "self.class"
113
+
114
+ # Apply scopes to the given query.
115
+ #
116
+ # Applicable scopes come from two places. They are either declared at the class level with
117
+ # <tt>ClassMethods#scope</tt>, or named in the request itself. The former is good for
118
+ # defining topics or enforcing security, while the latter is free slicing and dicing for
119
+ # clients.
120
+ #
121
+ # Class-level scopes are applied first. Request scopes come after, and are discovered by
122
+ # examining +params+. If any +params+ key matches a name found in
123
+ # <tt>ClassMethods#model_class.scopes.keys</tt>, then it is taken to be a scope and is
124
+ # applied. The values under that +params+ key are passed along as arguments.
125
+ #
126
+ # TODO: and if the scope taketh arguments not?
127
+ def scope(query)
128
+ query = (scopes || []).inject(query) do |query, scope|
129
+ if Symbol === scope and model_class.scopes.key? scope
130
+ query.send scope
131
+ elsif Proc === scope
132
+ instance_exec query, &scope
133
+ else
134
+ raise ArgumentError.new "Unknown scope #{model_class}.#{scope}"
135
+ end
136
+ end
137
+
138
+ scopes_from_request = (model_class.scopes.keys & params.keys.collect { |k| k.to_sym })
139
+ query = scopes_from_request.inject(query) do |query, scope|
140
+ query.send scope, *params[scope.to_s]
141
+ end
142
+
143
+ query
144
+ end
145
+
146
+ # Find all models in #scope.
147
+ #
148
+ # The initial, unscoped is <tt>ClassMethods#model_class.all</tt>.
149
+ def find_models
150
+ scope model_class.all
151
+ end
152
+
153
+ # Find a particular model.
154
+ #
155
+ # #find_models is asked to find a model with <tt>params[:id]</tt>. This ensures that
156
+ # class-level scopes are enforced (potentially for security.)
157
+ def find_model
158
+ find_models.find(params[:id])
159
+ end
160
+
161
+ # The underscored model class name, as a symbol.
162
+ #
163
+ # Model modules are omitted.
164
+ def model_slug
165
+ model_class_name.split('/').last.to_sym
166
+ end
167
+
168
+ # Like #model_slug, but plural.
169
+ def models_slug
170
+ model_slug.to_s.pluralize.to_sym
171
+ end
172
+
173
+ # The name of the instance variable holding a single model instance.
174
+ def model_ivar
175
+ "@#{model_slug}"
176
+ end
177
+
178
+ # The name of the instance variable holding a collection of models.
179
+ def models_ivar
180
+ model_ivar.pluralize
181
+ end
182
+
183
+ # Retrive #models_ivar
184
+ def models
185
+ instance_variable_get models_ivar
186
+ end
187
+
188
+ # Assign #models_ivar
189
+ def models=(models)
190
+ instance_variable_set models_ivar, models
191
+ end
192
+
193
+ # Retrive #model_ivar
194
+ def model
195
+ instance_variable_get model_ivar
196
+ end
197
+
198
+ # Assign #model_ivar
199
+ def model=(model)
200
+ instance_variable_set model_ivar, model
201
+ end
202
+
203
+ # Apply ClassMethods#responds_within to the given model (or symbol.)
204
+ #
205
+ # "Apply" just means turning +responds_within+ into an array and appending +model+ to the
206
+ # end. If +responds_within+ is an array, it used directly.
207
+ #
208
+ # If it is a proc, it is called with +instance_exec+, passing +model+ in. It should return an
209
+ # array, which +model+ will be appended to. (So, don't include it in the return value.)
210
+ def responder_context(model)
211
+ context = responds_within.collect do |o|
212
+ o = instance_exec model, &o if o.is_a? Proc
213
+ o
214
+ end.flatten + [model]
215
+ end
216
+
217
+ # Pass +model+ through InstanceMethods#responder_context, and pass that to #respond_with.
218
+ def respond_with_contextual(model)
219
+ respond_with *responder_context(model)
220
+ end
221
+ end
222
+
223
+ # The seven standard restful actions.
224
+ module Actions
225
+ # Find, assign and respond with models.
226
+ def index
227
+ self.models = find_models
228
+ respond_with_contextual models
229
+ end
230
+
231
+ # Find, assign and respond with a single model.
232
+ def show
233
+ self.model = find_model
234
+ respond_with_contextual model
235
+ end
236
+
237
+ # Build (but do not save), assign and respond with a new model.
238
+ #
239
+ # The new model is built from the <tt>InstanceMethods#find_models</tt> collection, meaning it
240
+ # could inherit any properties implied by those scopes.
241
+ def new
242
+ self.model = find_models.build
243
+ respond_with_contextual model
244
+ end
245
+
246
+ # Find, assign and respond with a single model.
247
+ def edit
248
+ self.model = find_model
249
+ respond_with_contextual model
250
+ end
251
+
252
+ # Build, save, assign and respond with a new model.
253
+ #
254
+ # The model is created with attributes from the request params, under the
255
+ # <tt>InstanceMethods#model_slug</tt> key.
256
+ def create
257
+ self.model = find_models.build(params[model_slug])
258
+ model.save
259
+ respond_with_contextual model
260
+ end
261
+
262
+ # Find, update, assign and respond with a single model.
263
+ #
264
+ # The new attributes are taken from the request params, under the
265
+ # <tt>InstanceMethods#model_slug</tt> key.
266
+ def update
267
+ self.model = find_model
268
+ model.update_attributes(params[model_slug])
269
+ respond_with_contextual model
270
+ end
271
+
272
+ # Find and destroy a model. Respond with <tt>InstanceMethods#models_slug</tt>.
273
+ def destroy
274
+ find_model.destroy
275
+ respond_with_contextual models_slug
276
+ end
277
+ end
278
+ end
@@ -0,0 +1,58 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{responder_controller}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Phil Smith"]
12
+ s.date = %q{2010-05-12}
13
+ s.description = %q{Responders make crud controllers tiny, this wraps the rest.}
14
+ s.email = %q{phil.h.smith@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "lib/responder_controller.rb",
27
+ "responder_controller.gemspec",
28
+ "spec/responder_controller_spec.rb",
29
+ "spec/spec.opts",
30
+ "spec/spec_helper.rb"
31
+ ]
32
+ s.homepage = %q{http://github.com/phs/responder_controller}
33
+ s.rdoc_options = ["--charset=UTF-8"]
34
+ s.require_paths = ["lib"]
35
+ s.rubygems_version = %q{1.3.6}
36
+ s.summary = %q{like resources_controller, but for rails 3 responders}
37
+ s.test_files = [
38
+ "spec/responder_controller_spec.rb",
39
+ "spec/spec_helper.rb"
40
+ ]
41
+
42
+ if s.respond_to? :specification_version then
43
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
44
+ s.specification_version = 3
45
+
46
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
47
+ s.add_runtime_dependency(%q<activesupport>, [">= 3.0.0.beta2"])
48
+ s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
49
+ else
50
+ s.add_dependency(%q<activesupport>, [">= 3.0.0.beta2"])
51
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
52
+ end
53
+ else
54
+ s.add_dependency(%q<activesupport>, [">= 3.0.0.beta2"])
55
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
56
+ end
57
+ end
58
+
@@ -0,0 +1,511 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/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" 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_name' do
83
+ it "is .model_class_name" do
84
+ PostsController.new.model_class_name.should == PostsController.model_class_name
85
+ end
86
+ end
87
+
88
+ describe '.model_class' do
89
+ it 'is the constant named by model_class_name' do
90
+ PostsController.model_class.should == Post
91
+ end
92
+
93
+ it 'handles modules in the name' do
94
+ Admin::SettingsController.model_class.should == Admin::Setting
95
+ end
96
+ end
97
+
98
+ describe '#scope', 'by default' do
99
+ it "passes its argument out" do
100
+ PostsController.new.scope(@query).should == @query
101
+ end
102
+ end
103
+
104
+ describe '#model_class' do
105
+ it 'is .model_class' do
106
+ PostsController.new.model_class.should == PostsController.model_class
107
+ end
108
+ end
109
+
110
+ describe '.scope' do
111
+ it 'takes a string naming a scope on model_class' do
112
+ PostsController.scope 'unpublished'
113
+ end
114
+
115
+ it 'can take a symbol' do
116
+ PostsController.scope :recent
117
+ end
118
+ end
119
+
120
+ describe '.scopes' do
121
+ it 'is an array of scopes in order, as symbols' do
122
+ PostsController.scopes.should == [:unpublished, :recent]
123
+ end
124
+ end
125
+
126
+ describe '#scopes' do
127
+ it 'is .scopes' do
128
+ PostsController.new.scopes.should == PostsController.scopes
129
+ end
130
+ end
131
+
132
+ describe '#scope', 'with explicit scopes' do
133
+ it "asks model_class for the declared scopes in order" do
134
+ @query.should_receive(:unpublished).and_return(@query)
135
+ @query.should_receive(:recent).and_return(@query)
136
+ PostsController.new.scope(@query).should == @query
137
+ end
138
+ end
139
+
140
+ describe '.scope', 'with a block' do
141
+ it 'omits the name and can reference params' do
142
+ PostsController.scope do |posts|
143
+ posts.owned_by(params[:user_id])
144
+ end
145
+ end
146
+
147
+ it 'puts a lambda on .scopes' do
148
+ PostsController.scopes.last.should be_a Proc
149
+ end
150
+ end
151
+
152
+ describe '.scope', 'with something that is not a string, symbol or block' do
153
+ it 'explodes' do
154
+ lambda do
155
+ PostsController.scope [:not_a, :string_symbol_or_block]
156
+ end.should raise_error ArgumentError
157
+ end
158
+ end
159
+
160
+ describe '#scope', 'with a block scope' do
161
+ it 'instance_execs the block while passing in the current query' do
162
+ @query.should_receive(:owned_by).with('me').and_return(@query)
163
+
164
+ controller = PostsController.new
165
+ controller.scope(@query).should == @query
166
+ end
167
+ end
168
+
169
+ describe '#scope', 'with an unknown scope' do
170
+ it 'explodes' do
171
+ PostsController.scope :furst_p0sts
172
+
173
+ lambda do
174
+ PostsController.new.scope
175
+ end.should raise_error ArgumentError
176
+
177
+ PostsController.scopes.pop
178
+ end
179
+ end
180
+
181
+ describe '#scope', 'with request parameters naming scopes' do
182
+ before :each do
183
+ @controller = PostsController.new
184
+ @controller.params['commented_on_by'] = 'you'
185
+ end
186
+
187
+ it 'applies the requested scopes in order' do
188
+ @query.should_receive(:commented_on_by).with('you').and_return(@query)
189
+ @controller.scope @query
190
+ end
191
+
192
+ it 'is applied after class-level scopes' do
193
+ class_level_query = mock("class-level scoped query")
194
+ @query.should_receive(:owned_by).and_return(class_level_query) # last class-level scope
195
+
196
+ class_level_query.should_receive(:commented_on_by).with('you').and_return(class_level_query)
197
+ @controller.scope(@query).should == class_level_query
198
+ end
199
+ end
200
+
201
+ describe '#find_models' do
202
+ it 'is #scope #model_class.find(:all)' do
203
+ controller = PostsController.new
204
+
205
+ Post.should_receive(:all).and_return(@query)
206
+ controller.should_receive(:scope).with(@query).and_return(@query)
207
+
208
+ controller.find_models.should == @query
209
+ end
210
+ end
211
+
212
+ describe '#find_model' do
213
+ it 'is #find_models.find(params[:id])' do
214
+ controller = PostsController.new
215
+ controller.should_receive(:find_models).and_return(@query)
216
+ @query.should_receive(:find).with(controller.params[:id]).and_return(post = mock("the post"))
217
+
218
+ controller.find_model.should == post
219
+ end
220
+ end
221
+
222
+ describe '#model_slug' do
223
+ it 'is the model class name' do
224
+ PostsController.new.model_slug.should == :post
225
+ end
226
+
227
+ it 'drops the leading module names, if any' do
228
+ Admin::SettingsController.new.model_slug.should == :setting
229
+ end
230
+ end
231
+
232
+ describe '#models_slug' do
233
+ it 'is ths symbolized plural of #model_slug' do
234
+ PostsController.new.models_slug.should == :posts
235
+ end
236
+ end
237
+
238
+ describe '#model_ivar' do
239
+ it 'is the #model_slug with a leading @' do
240
+ PostsController.new.model_ivar.should == '@post'
241
+ end
242
+ end
243
+
244
+ describe '#models_ivar' do
245
+ it 'is the plural #model_ivar' do
246
+ (controller = PostsController.new).models_ivar.should == controller.model_ivar.pluralize
247
+ end
248
+ end
249
+
250
+ describe "#models" do
251
+ it "gets #models_ivar" do
252
+ (controller = PostsController.new).instance_variable_set("@posts", :some_posts)
253
+ controller.models.should == :some_posts
254
+ end
255
+ end
256
+
257
+ describe "#model" do
258
+ it "gets #model_ivar" do
259
+ (controller = PostsController.new).instance_variable_set("@post", :a_post)
260
+ controller.model.should == :a_post
261
+ end
262
+ end
263
+
264
+ describe "#models=" do
265
+ it "assigns to #models_ivar" do
266
+ assigned = mock("some models")
267
+ (controller = PostsController.new).models = assigned
268
+ controller.instance_variable_get("@posts").should == assigned
269
+ end
270
+ end
271
+
272
+ describe "#model=" do
273
+ it "assigns to #model_ivar" do
274
+ assigned = mock("a model")
275
+ (controller = PostsController.new).model = assigned
276
+ controller.instance_variable_get("@post").should == assigned
277
+ end
278
+ end
279
+
280
+ describe '.responds_within' do
281
+ it "contains the model's enclosing module names as symbols by default" do
282
+ PostsController.responds_within.should == []
283
+ Admin::SettingsController.responds_within.should == [:admin]
284
+ end
285
+
286
+ it "takes, saves and returns a varargs" do
287
+ PostsController.responds_within(:foo, :bar, :baz).should == [:foo, :bar, :baz]
288
+ PostsController.responds_within.should == [:foo, :bar, :baz]
289
+ end
290
+
291
+ it "accumulates between calls" do
292
+ PostsController.responds_within(:foo).should == [:foo]
293
+ PostsController.responds_within(:bar, :baz).should == [:foo, :bar, :baz]
294
+ PostsController.responds_within.should == [:foo, :bar, :baz]
295
+ end
296
+
297
+ it "can take a block instead" do
298
+ block = lambda {}
299
+ PostsController.responds_within(&block).should == [block]
300
+ PostsController.responds_within.should == [block]
301
+ end
302
+
303
+ it "whines if both positional arguments and a block are passed" do
304
+ lambda do
305
+ PostsController.responds_within(:foo, :bar, :baz) {}
306
+ end.should raise_error ArgumentError
307
+ end
308
+
309
+ after :each do
310
+ PostsController.instance_variable_set "@responds_within", nil # clear out the garbage
311
+ end
312
+ end
313
+
314
+ describe '.children_of' do
315
+ it 'takes a underscored model class name' do
316
+ PostsController.children_of 'accounts/user'
317
+ end
318
+
319
+ it "can take symbols" do
320
+ PostsController.children_of 'accounts/user'.to_sym
321
+ end
322
+
323
+ it "creates a scope filtering by the parent model's foreign key as passed in params" do
324
+ PostsController.children_of 'accounts/user'
325
+ controller = PostsController.new
326
+ controller.params[:user_id] = :the_user_id
327
+
328
+ user_query = mock("user-restricted query")
329
+ @query.should_receive(:where).with(:user_id => :the_user_id).and_return(user_query)
330
+ controller.scope(@query).should == user_query
331
+ end
332
+
333
+ it "adds a responds_within context, of the parent modules followed by the parent itself" do
334
+ PostsController.children_of 'accounts/user'
335
+ controller = PostsController.new
336
+ controller.params[:user_id] = :the_user_id
337
+
338
+ Accounts::User.should_receive(:find).with(:the_user_id).and_return(user = mock("the user"))
339
+
340
+ controller.responder_context(:argument).should == [:accounts, user, :argument]
341
+ end
342
+
343
+ after :each do
344
+ PostsController.instance_variable_set "@responds_within", nil # clear out the garbage
345
+ PostsController.scopes.clear
346
+ end
347
+ end
348
+
349
+ describe '#responds_within' do
350
+ it 'is .responds_within' do
351
+ PostsController.new.responds_within.should == PostsController.responds_within
352
+ end
353
+ end
354
+
355
+ describe '#responder_context' do
356
+ it "is the argument prepended with responds_within" do
357
+ Admin::SettingsController.new.responder_context(:argument).should == [:admin, :argument]
358
+ end
359
+
360
+ it "passes the argument to responds_within and prepends the result if it is a lambda" do
361
+ Admin::SettingsController.responds_within do |model|
362
+ model.should == :argument
363
+ [:nested, :namespace]
364
+ end
365
+
366
+ Admin::SettingsController.new.responder_context(:argument).should == [:nested, :namespace, :argument]
367
+ end
368
+
369
+ it "wraps the lambda result in an array if needed" do
370
+ Admin::SettingsController.responds_within { |model| :namespace }
371
+ Admin::SettingsController.new.responder_context(:argument).should == [:namespace, :argument]
372
+ end
373
+
374
+ after :each do
375
+ Admin::SettingsController.instance_variable_set "@responds_within", nil
376
+ end
377
+ end
378
+
379
+ describe '#respond_with_contextual' do
380
+ it 'passed #responder_context to #respond_with' do
381
+ controller = PostsController.new
382
+ controller.should_receive(:responder_context).with(:argument).and_return([:contextualized_argument])
383
+ controller.should_receive(:respond_with).with(:contextualized_argument)
384
+
385
+ controller.respond_with_contextual :argument
386
+ end
387
+ end
388
+
389
+ describe 'actions' do
390
+ before :each do
391
+ @controller = PostsController.new
392
+ @controller.stub!(:find_models).and_return(@posts = mock("some posts"))
393
+ @controller.stub!(:find_model).and_return(@post = mock("a post"))
394
+ @controller.stub!(:respond_with)
395
+
396
+ @posts.stub!(:build).and_return(@post)
397
+ end
398
+
399
+ describe '#index' do
400
+ it 'assigns #find_models to #models=' do
401
+ @controller.should_receive(:find_models).and_return(@posts)
402
+ @controller.index
403
+ @controller.instance_variable_get('@posts').should == @posts
404
+ end
405
+
406
+ it '#respond_with_contextual @models' do
407
+ @controller.should_receive(:respond_with_contextual).with(@posts)
408
+ @controller.index
409
+ end
410
+ end
411
+
412
+ [:show, :edit].each do |verb|
413
+ describe "##{verb}" do
414
+ it 'assigns #find_model to #model=' do
415
+ @controller.should_receive(:find_model).and_return(@post)
416
+ @controller.send verb
417
+ @controller.instance_variable_get('@post').should == @post
418
+ end
419
+
420
+ it '#respond_with_contextual @model' do
421
+ @controller.should_receive(:respond_with_contextual).with(@post)
422
+ @controller.send verb
423
+ end
424
+ end
425
+ end
426
+
427
+ describe '#new' do
428
+ it 'assigns #find_models.new to #model=' do
429
+ @posts.should_receive(:build).and_return(@post)
430
+ @controller.new
431
+ @controller.instance_variable_get('@post').should == @post
432
+ end
433
+
434
+ it '#respond_with_contextual @model' do
435
+ @controller.should_receive(:respond_with_contextual).with(@post)
436
+ @controller.new
437
+ end
438
+ end
439
+
440
+ describe '#create' do
441
+ before :each do
442
+ @post.stub!(:save)
443
+ end
444
+
445
+ it 'passes params[model_slug] to #find_models.new' do
446
+ @controller.params[:post] = :params_to_new
447
+ @posts.should_receive(:build).with(:params_to_new)
448
+ @controller.create
449
+ end
450
+
451
+ it 'assigns #find_models.new to #model=' do
452
+ @controller.create
453
+ @controller.instance_variable_get('@post').should == @post
454
+ end
455
+
456
+ it 'saves the model' do
457
+ @post.should_receive(:save)
458
+ @controller.create
459
+ end
460
+
461
+ it '#respond_with_contextual @model' do
462
+ @controller.should_receive(:respond_with_contextual).with(@post)
463
+ @controller.create
464
+ end
465
+ end
466
+
467
+ describe '#update' do
468
+ before :each do
469
+ @post.stub!(:update_attributes)
470
+ end
471
+
472
+ it 'assigns #find_model to #model=' do
473
+ @controller.should_receive(:find_model).and_return(@post)
474
+ @controller.update
475
+ @controller.instance_variable_get('@post').should == @post
476
+ end
477
+
478
+ it "updates the model's attributes" do
479
+ @controller.params[:post] = :params_to_update
480
+ @post.should_receive(:update_attributes).with(:params_to_update)
481
+ @controller.update
482
+ end
483
+
484
+ it '#respond_with_contextual @model' do
485
+ @controller.should_receive(:respond_with_contextual).with(@post)
486
+ @controller.update
487
+ end
488
+ end
489
+
490
+ describe '#destroy' do
491
+ before :each do
492
+ @post.stub!(:destroy)
493
+ end
494
+
495
+ it 'finds the model' do
496
+ @controller.should_receive(:find_model).and_return(@post)
497
+ @controller.destroy
498
+ end
499
+
500
+ it 'destroys the model' do
501
+ @post.should_receive(:destroy)
502
+ @controller.destroy
503
+ end
504
+
505
+ it '#respond_with_contextual #models_slug' do
506
+ @controller.should_receive(:respond_with_contextual).with(:posts)
507
+ @controller.destroy
508
+ end
509
+ end
510
+ end
511
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,13 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+
4
+ require 'rubygems'
5
+ gem 'activesupport'
6
+
7
+ require 'responder_controller'
8
+ require 'spec'
9
+ require 'spec/autorun'
10
+
11
+ Spec::Runner.configure do |config|
12
+
13
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: responder_controller
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Phil Smith
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-05-12 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: activesupport
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 3
29
+ - 0
30
+ - 0
31
+ - beta2
32
+ version: 3.0.0.beta2
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: rspec
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ segments:
43
+ - 1
44
+ - 2
45
+ - 9
46
+ version: 1.2.9
47
+ type: :development
48
+ version_requirements: *id002
49
+ description: Responders make crud controllers tiny, this wraps the rest.
50
+ email: phil.h.smith@gmail.com
51
+ executables: []
52
+
53
+ extensions: []
54
+
55
+ extra_rdoc_files:
56
+ - LICENSE
57
+ - README.rdoc
58
+ files:
59
+ - .document
60
+ - .gitignore
61
+ - LICENSE
62
+ - README.rdoc
63
+ - Rakefile
64
+ - VERSION
65
+ - lib/responder_controller.rb
66
+ - responder_controller.gemspec
67
+ - spec/responder_controller_spec.rb
68
+ - spec/spec.opts
69
+ - spec/spec_helper.rb
70
+ has_rdoc: true
71
+ homepage: http://github.com/phs/responder_controller
72
+ licenses: []
73
+
74
+ post_install_message:
75
+ rdoc_options:
76
+ - --charset=UTF-8
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ segments:
84
+ - 0
85
+ version: "0"
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ segments:
91
+ - 0
92
+ version: "0"
93
+ requirements: []
94
+
95
+ rubyforge_project:
96
+ rubygems_version: 1.3.6
97
+ signing_key:
98
+ specification_version: 3
99
+ summary: like resources_controller, but for rails 3 responders
100
+ test_files:
101
+ - spec/responder_controller_spec.rb
102
+ - spec/spec_helper.rb