make_resourceful 1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +31 -0
- data/Readme.rdoc +229 -0
- data/VERSION +1 -0
- data/lib/make_resourceful.rb +11 -0
- data/lib/resourceful/base.rb +63 -0
- data/lib/resourceful/builder.rb +405 -0
- data/lib/resourceful/default/accessors.rb +418 -0
- data/lib/resourceful/default/actions.rb +101 -0
- data/lib/resourceful/default/callbacks.rb +51 -0
- data/lib/resourceful/default/responses.rb +118 -0
- data/lib/resourceful/default/urls.rb +136 -0
- data/lib/resourceful/generators/resourceful_scaffold/resourceful_scaffold_generator.rb +87 -0
- data/lib/resourceful/generators/resourceful_scaffold/templates/controller.rb +5 -0
- data/lib/resourceful/generators/resourceful_scaffold/templates/fixtures.yml +10 -0
- data/lib/resourceful/generators/resourceful_scaffold/templates/functional_test.rb +50 -0
- data/lib/resourceful/generators/resourceful_scaffold/templates/helper.rb +2 -0
- data/lib/resourceful/generators/resourceful_scaffold/templates/migration.rb +13 -0
- data/lib/resourceful/generators/resourceful_scaffold/templates/model.rb +2 -0
- data/lib/resourceful/generators/resourceful_scaffold/templates/unit_test.rb +7 -0
- data/lib/resourceful/generators/resourceful_scaffold/templates/view__form.haml +5 -0
- data/lib/resourceful/generators/resourceful_scaffold/templates/view_edit.haml +11 -0
- data/lib/resourceful/generators/resourceful_scaffold/templates/view_index.haml +5 -0
- data/lib/resourceful/generators/resourceful_scaffold/templates/view_new.haml +9 -0
- data/lib/resourceful/generators/resourceful_scaffold/templates/view_partial.haml +12 -0
- data/lib/resourceful/generators/resourceful_scaffold/templates/view_show.haml +14 -0
- data/lib/resourceful/maker.rb +92 -0
- data/lib/resourceful/response.rb +33 -0
- data/lib/resourceful/serialize.rb +185 -0
- data/spec/accessors_spec.rb +474 -0
- data/spec/actions_spec.rb +310 -0
- data/spec/base_spec.rb +12 -0
- data/spec/builder_spec.rb +332 -0
- data/spec/callbacks_spec.rb +71 -0
- data/spec/integration_spec.rb +394 -0
- data/spec/maker_spec.rb +91 -0
- data/spec/response_spec.rb +37 -0
- data/spec/responses_spec.rb +314 -0
- data/spec/serialize_spec.rb +133 -0
- data/spec/urls_spec.rb +282 -0
- metadata +97 -0
@@ -0,0 +1,405 @@
|
|
1
|
+
require 'resourceful/response'
|
2
|
+
require 'resourceful/serialize'
|
3
|
+
require 'resourceful/default/actions'
|
4
|
+
|
5
|
+
module Resourceful
|
6
|
+
# The Maker#make_resourceful block is evaluated in the context
|
7
|
+
# of an instance of this class.
|
8
|
+
# It provides various methods for customizing the behavior of the actions
|
9
|
+
# built by make_resourceful.
|
10
|
+
#
|
11
|
+
# All instance methods of this class are available in the +make_resourceful+ block.
|
12
|
+
class Builder
|
13
|
+
# The klass of the controller on which the builder is working.
|
14
|
+
attr :controller, true
|
15
|
+
|
16
|
+
# The constructor is only meant to be called internally.
|
17
|
+
#
|
18
|
+
# This takes the klass (class object) of a controller
|
19
|
+
# and constructs a Builder ready to apply the make_resourceful
|
20
|
+
# additions to the controller.
|
21
|
+
def initialize(kontroller)
|
22
|
+
@controller = kontroller
|
23
|
+
@inherited = !kontroller.resourceful_responses.blank?
|
24
|
+
@action_module = Resourceful::Default::Actions.dup
|
25
|
+
@ok_actions = []
|
26
|
+
@callbacks = {:before => {}, :after => {}}
|
27
|
+
@responses = {}
|
28
|
+
@publish = {}
|
29
|
+
@parents = []
|
30
|
+
@shallow_parent = nil
|
31
|
+
@custom_member_actions = []
|
32
|
+
@custom_collection_actions = []
|
33
|
+
end
|
34
|
+
|
35
|
+
# This method is only meant to be called internally.
|
36
|
+
#
|
37
|
+
# Applies all the changes that have been declared
|
38
|
+
# via the instance methods of this Builder
|
39
|
+
# to the kontroller passed in to the constructor.
|
40
|
+
def apply
|
41
|
+
apply_publish
|
42
|
+
|
43
|
+
kontroller = @controller
|
44
|
+
|
45
|
+
Resourceful::ACTIONS.each do |action_named|
|
46
|
+
# See if this is a method listed by #actions
|
47
|
+
unless @ok_actions.include? action_named
|
48
|
+
# If its not listed, then remove the method
|
49
|
+
# No one can hit it... if its DEAD!
|
50
|
+
@action_module.send :remove_method, action_named
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
kontroller.hidden_actions.reject! &@ok_actions.method(:include?)
|
55
|
+
kontroller.send :include, @action_module
|
56
|
+
|
57
|
+
merged_callbacks = kontroller.resourceful_callbacks.merge @callbacks
|
58
|
+
merged_responses = kontroller.resourceful_responses.merge @responses
|
59
|
+
|
60
|
+
kontroller.resourceful_callbacks = merged_callbacks
|
61
|
+
kontroller.resourceful_responses = merged_responses
|
62
|
+
kontroller.made_resourceful = true
|
63
|
+
|
64
|
+
kontroller.parents = @parents
|
65
|
+
kontroller.shallow_parent = @shallow_parent
|
66
|
+
kontroller.model_namespace = @model_namespace
|
67
|
+
kontroller.before_filter :load_object, :only => (@ok_actions & SINGULAR_PRELOADED_ACTIONS) + @custom_member_actions
|
68
|
+
kontroller.before_filter :load_objects, :only => (@ok_actions & PLURAL_ACTIONS) + @custom_collection_actions
|
69
|
+
kontroller.before_filter :load_parent_object, :only => @ok_actions + @custom_member_actions + @custom_collection_actions
|
70
|
+
end
|
71
|
+
|
72
|
+
# :call-seq:
|
73
|
+
# actions(*available_actions)
|
74
|
+
# actions :all
|
75
|
+
#
|
76
|
+
# Adds the default RESTful actions to the controller.
|
77
|
+
#
|
78
|
+
# If the only argument is <tt>:all</tt>,
|
79
|
+
# adds all the actions listed in Resourceful::ACTIONS[link:classes/Resourceful.html]
|
80
|
+
# (or Resourceful::SINGULAR_ACTIONS[link:classes/Resourceful.html]
|
81
|
+
# for a singular controller).
|
82
|
+
#
|
83
|
+
# Otherwise, this adds all actions
|
84
|
+
# whose names were passed as arguments.
|
85
|
+
#
|
86
|
+
# For example:
|
87
|
+
#
|
88
|
+
# actions :show, :new, :create
|
89
|
+
#
|
90
|
+
# This adds the +show+, +new+, and +create+ actions
|
91
|
+
# to the controller.
|
92
|
+
#
|
93
|
+
# The available actions are defined in Default::Actions.
|
94
|
+
def actions(*available_actions)
|
95
|
+
# FIXME HACK
|
96
|
+
# made all methods private, so plural?, too.
|
97
|
+
# Did not want to make an exception for that and i do not like it to
|
98
|
+
# come up on actions_methods.
|
99
|
+
# TODO: maybe we can define plural? as class_method
|
100
|
+
if available_actions.first == :all
|
101
|
+
if controller.respond_to?(:new_without_capture)
|
102
|
+
available_actions = controller.new_without_capture.send(:plural?) ? ACTIONS : SINGULAR_ACTIONS
|
103
|
+
else
|
104
|
+
available_actions = controller.new.send(:plural?) ? ACTIONS : SINGULAR_ACTIONS
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
available_actions.each { |action| @ok_actions << action.to_sym }
|
109
|
+
end
|
110
|
+
alias build actions
|
111
|
+
|
112
|
+
# :call-seq:
|
113
|
+
# member_actions(*available_actions)
|
114
|
+
#
|
115
|
+
# Registers custom member actions which will use the load_object before_filter.
|
116
|
+
# These actions are not created, but merely registered for filtering.
|
117
|
+
def member_actions(*available_actions)
|
118
|
+
available_actions.each { |action| @custom_member_actions << action.to_sym }
|
119
|
+
end
|
120
|
+
|
121
|
+
# :call-seq:
|
122
|
+
# collection_actions(*available_actions)
|
123
|
+
#
|
124
|
+
# Registers custom collection actions which will use the load_objects before_filter.
|
125
|
+
# These actions are not created, but merely registered for filtering.
|
126
|
+
def collection_actions(*available_actions)
|
127
|
+
available_actions.each { |action| @custom_collection_actions << action.to_sym }
|
128
|
+
end
|
129
|
+
|
130
|
+
# :call-seq:
|
131
|
+
# before(*events) { ... }
|
132
|
+
#
|
133
|
+
# Sets up a block of code to run before one or more events.
|
134
|
+
#
|
135
|
+
# All the default actions can be used as +before+ events:
|
136
|
+
# <tt>:index</tt>, <tt>:show</tt>, <tt>:create</tt>, <tt>:update</tt>, <tt>:new</tt>, <tt>:edit</tt>, and <tt>:destroy</tt>.
|
137
|
+
#
|
138
|
+
# +before+ events are run after any objects are loaded[link:classes/Resourceful/Default/Accessors.html#M000015],
|
139
|
+
# but before any database operations or responses.
|
140
|
+
#
|
141
|
+
# For example:
|
142
|
+
#
|
143
|
+
# before :show, :edit do
|
144
|
+
# @page_title = current_object.title
|
145
|
+
# end
|
146
|
+
#
|
147
|
+
# This will set the <tt>@page_title</tt> variable
|
148
|
+
# to the current object's title
|
149
|
+
# for the show and edit actions.
|
150
|
+
#
|
151
|
+
# Successive before blocks for the same action will be chained and executed
|
152
|
+
# in order when the event occurs.
|
153
|
+
#
|
154
|
+
# For example:
|
155
|
+
#
|
156
|
+
# before :show, :edit do
|
157
|
+
# @page_title = current_object.title
|
158
|
+
# end
|
159
|
+
#
|
160
|
+
# before :show do
|
161
|
+
# @side_bar = true
|
162
|
+
# end
|
163
|
+
#
|
164
|
+
# These before blocks will both be executed for the show action and in the
|
165
|
+
# same order as they were defined.
|
166
|
+
def before(*events, &block)
|
167
|
+
add_callback :before, *events, &block
|
168
|
+
end
|
169
|
+
|
170
|
+
# :call-seq:
|
171
|
+
# after(*events) { ... }
|
172
|
+
#
|
173
|
+
# Sets up a block of code to run after one or more events.
|
174
|
+
#
|
175
|
+
# There are two sorts of +after+ events.
|
176
|
+
# <tt>:create</tt>, <tt>:update</tt>, and <tt>:destroy</tt>
|
177
|
+
# are run after their respective database operations
|
178
|
+
# have been completed successfully.
|
179
|
+
# <tt>:create_fails</tt>, <tt>:update_fails</tt>, and <tt>:destroy_fails</tt>,
|
180
|
+
# on the other hand,
|
181
|
+
# are run after the database operations fail.
|
182
|
+
#
|
183
|
+
# +after+ events are run after the database operations
|
184
|
+
# but before any responses.
|
185
|
+
#
|
186
|
+
# For example:
|
187
|
+
#
|
188
|
+
# after :create_fails, :update_fails do
|
189
|
+
# current_object.password = nil
|
190
|
+
# end
|
191
|
+
#
|
192
|
+
# This will nillify the password of the current object
|
193
|
+
# if the object creation/modification failed.
|
194
|
+
def after(*events, &block)
|
195
|
+
add_callback :after, *events, &block
|
196
|
+
end
|
197
|
+
|
198
|
+
# :call-seq:
|
199
|
+
# response_for(*actions) { ... }
|
200
|
+
# response_for(*actions) { |format| ... }
|
201
|
+
#
|
202
|
+
# Sets up a block of code to run
|
203
|
+
# instead of the default responses for one or more events.
|
204
|
+
#
|
205
|
+
# If the block takes a format parameter,
|
206
|
+
# it has the same semantics as Rails' +respond_to+ method.
|
207
|
+
# Various format methods are called on the format object
|
208
|
+
# with blocks that say what to do for each format.
|
209
|
+
# For example:
|
210
|
+
#
|
211
|
+
# response_for :index do |format|
|
212
|
+
# format.html
|
213
|
+
# format.atom do
|
214
|
+
# headers['Content-Type'] = 'application/atom+xml; charset=utf-8'
|
215
|
+
# render :action => 'atom', :layout => false
|
216
|
+
# end
|
217
|
+
# end
|
218
|
+
#
|
219
|
+
# This doesn't do anything special for the HTML
|
220
|
+
# other than ensure that the proper view will be rendered,
|
221
|
+
# but for ATOM it sets the proper content type
|
222
|
+
# and renders the atom template.
|
223
|
+
#
|
224
|
+
# If you only need to set the HTML response,
|
225
|
+
# you can omit the format parameter.
|
226
|
+
# For example:
|
227
|
+
#
|
228
|
+
# response_for :new do
|
229
|
+
# render :action => 'edit'
|
230
|
+
# end
|
231
|
+
#
|
232
|
+
# This is the same as
|
233
|
+
#
|
234
|
+
# response_for :new do |format|
|
235
|
+
# format.html { render :action => 'edit' }
|
236
|
+
# end
|
237
|
+
#
|
238
|
+
# The default responses are defined by
|
239
|
+
# Default::Responses.included[link:classes/Resourceful/Default/Responses.html#M000011].
|
240
|
+
def response_for(*actions, &block)
|
241
|
+
raise "Must specify one or more actions for response_for." if actions.empty?
|
242
|
+
|
243
|
+
if block.arity < 1
|
244
|
+
response_for(*actions) do |format|
|
245
|
+
format.html(&block)
|
246
|
+
end
|
247
|
+
else
|
248
|
+
response = Response.new
|
249
|
+
block.call response
|
250
|
+
|
251
|
+
actions.each do |action|
|
252
|
+
@responses[action.to_sym] = response.formats
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
# :call-seq:
|
258
|
+
# publish *formats, options = {}, :attributes => [ ... ]
|
259
|
+
#
|
260
|
+
# publish allows you to easily expose information about resourcess in a variety of formats.
|
261
|
+
# The +formats+ parameter is a list of formats
|
262
|
+
# in which to publish the resources.
|
263
|
+
# The formats supported by default are +xml+, +yaml+, and +json+,
|
264
|
+
# but other formats may be added by defining +to_format+ methods
|
265
|
+
# for the Array and Hash classes
|
266
|
+
# and registering the mime type with Rails' Mime::Type.register[http://api.rubyonrails.org/classes/Mime/Type.html#M001115].
|
267
|
+
# See Resourceful::Serialize for more details..
|
268
|
+
#
|
269
|
+
# The <tt>:attributes</tt> option is mandatory.
|
270
|
+
# It takes an array of attributes (as symbols) to make public.
|
271
|
+
# These attributes can refer to any method on current_object;
|
272
|
+
# they aren't limited to database fields.
|
273
|
+
# For example:
|
274
|
+
#
|
275
|
+
# # posts_controller.rb
|
276
|
+
# publish :yaml, :attributes => [:title, :created_at, :rendered_content]
|
277
|
+
#
|
278
|
+
# Then GET /posts/12.yaml would render
|
279
|
+
#
|
280
|
+
# ---
|
281
|
+
# post:
|
282
|
+
# title: Cool Stuff
|
283
|
+
# rendered_content: |-
|
284
|
+
# <p>This is a post.</p>
|
285
|
+
# <p>It's about <strong>really</strong> cool stuff.</p>
|
286
|
+
# created_at: 2007-04-28 04:32:08 -07:00
|
287
|
+
#
|
288
|
+
# The <tt>:attributes</tt> array can even contain attributes
|
289
|
+
# that are themselves models.
|
290
|
+
# In this case, you must use a hash to specify their attributes as well.
|
291
|
+
# For example:
|
292
|
+
#
|
293
|
+
# # person_controller.rb
|
294
|
+
# publish :xml, :json, :attributes => [
|
295
|
+
# :name, :favorite_color, {
|
296
|
+
# :pet_cat => [:name, :breed],
|
297
|
+
# :hat => [:type]
|
298
|
+
# }]
|
299
|
+
#
|
300
|
+
# Then GET /people/18.xml would render
|
301
|
+
#
|
302
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
303
|
+
# <person>
|
304
|
+
# <name>Nathan</name>
|
305
|
+
# <favorite-color>blue</favorite_color>
|
306
|
+
# <pet-cat>
|
307
|
+
# <name>Jasmine</name>
|
308
|
+
# <breed>panther</breed>
|
309
|
+
# </pet-cat>
|
310
|
+
# <hat>
|
311
|
+
# <type>top</type>
|
312
|
+
# </hat>
|
313
|
+
# </person>
|
314
|
+
#
|
315
|
+
# publish will also allow the +index+ action
|
316
|
+
# to render lists of objects.
|
317
|
+
# An example would be too big,
|
318
|
+
# but play with it a little on your own to see.
|
319
|
+
#
|
320
|
+
# publish takes only one optional option: <tt>only</tt>.
|
321
|
+
# This specifies which action to publish the resources for.
|
322
|
+
# By default, they're published for both +show+ and +index+.
|
323
|
+
# For example:
|
324
|
+
#
|
325
|
+
# # cats_controller.rb
|
326
|
+
# publish :json, :only => :index, :attributes => [:name, :breed]
|
327
|
+
#
|
328
|
+
# Then GET /cats.json would work, but GET /cats/294.json would fail.
|
329
|
+
def publish(*formats)
|
330
|
+
options = {
|
331
|
+
:only => [:show, :index]
|
332
|
+
}.merge(Hash === formats.last ? formats.pop : {})
|
333
|
+
raise "Must specify :attributes option" unless options[:attributes]
|
334
|
+
|
335
|
+
Array(options.delete(:only)).each do |action|
|
336
|
+
@publish[action] ||= []
|
337
|
+
formats.each do |format|
|
338
|
+
format = format.to_sym
|
339
|
+
@publish[action] << [format, proc do
|
340
|
+
render_action = [:json, :xml].include?(format) ? format : :text
|
341
|
+
render render_action => (plural_action? ? current_objects : current_object).serialize(format, options)
|
342
|
+
end]
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
# Specifies parent resources for the current resource.
|
348
|
+
# Each of these parents will be loaded automatically
|
349
|
+
# if the proper id parameter is given.
|
350
|
+
# For example,
|
351
|
+
#
|
352
|
+
# # cake_controller.rb
|
353
|
+
# belongs_to :baker, :customer
|
354
|
+
#
|
355
|
+
# Then on GET /bakers/12/cakes,
|
356
|
+
#
|
357
|
+
# params[:baker_id] #=> 12
|
358
|
+
# parent? #=> true
|
359
|
+
# parent_name #=> "baker"
|
360
|
+
# parent_model #=> Baker
|
361
|
+
# parent_object #=> Baker.find(12)
|
362
|
+
# current_objects #=> Baker.find(12).cakes
|
363
|
+
#
|
364
|
+
def belongs_to(*parents)
|
365
|
+
options = parents.extract_options!
|
366
|
+
@parents = parents.map(&:to_s)
|
367
|
+
if options[:shallow]
|
368
|
+
options[:shallow] = options[:shallow].to_s
|
369
|
+
raise ArgumentError, ":shallow needs the name of a parent resource" unless @parents.include? options[:shallow]
|
370
|
+
@shallow_parent = options[:shallow]
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
# Specifies a namespace for the resource model. It can be given as a
|
375
|
+
# Module::NameSpace, 'Module::NameSpace' (in a string), or
|
376
|
+
# 'module/name_space' (underscored form).
|
377
|
+
def model_namespace(ns)
|
378
|
+
@model_namespace = ns.to_s.camelize
|
379
|
+
end
|
380
|
+
|
381
|
+
# This method is only meant to be called internally.
|
382
|
+
#
|
383
|
+
# Returns whether or not the Builder's controller
|
384
|
+
# inherits make_resourceful settings from a parent controller.
|
385
|
+
def inherited?
|
386
|
+
@inherited
|
387
|
+
end
|
388
|
+
|
389
|
+
private
|
390
|
+
|
391
|
+
def apply_publish
|
392
|
+
@publish.each do |action, types|
|
393
|
+
@responses[action.to_sym] ||= []
|
394
|
+
@responses[action.to_sym] += types
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
def add_callback(type, *events, &block)
|
399
|
+
events.each do |event|
|
400
|
+
@callbacks[type][event.to_sym] ||= []
|
401
|
+
@callbacks[type][event.to_sym] << block
|
402
|
+
end
|
403
|
+
end
|
404
|
+
end
|
405
|
+
end
|