responder_controller 0.1.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.
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