reststop 0.5.1 → 0.5.2

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.
@@ -1,450 +1,451 @@
1
- # Right now you'll have to do some weird gymnastics to get this hooked in to a Camping app...
2
- # Something like:
3
- #
4
- # Camping.goes :Blog
5
- #
6
- # module Blog
7
- # include Camping::Session
8
- # include Reststop
9
- #
10
- # Controllers.extend Reststop::Controllers
11
- # end
12
- #
13
- # module Blog::Base
14
- # alias camping_render render
15
- # alias camping_service service
16
- # alias camping_lookup lookup
17
- # include Reststop::Base
18
- # alias service reststop_service
19
- # alias render reststop_render
20
- #
21
- # # Overrides the new Tilt-centric lookup method In camping
22
- # # RESTstop needs to have a first try at looking up the view
23
- # # located in the Views::HTML module.
24
- # def lookup(n)
25
- # T.fetch(n.to_sym) do |k|
26
- # t = Blog::Views::HTML.method_defined?(k) || camping_lookup(n)
27
- # end
28
- # end
29
-
30
- # end
31
- #
32
- # module Blog::Controllers
33
- # extend Reststop::Controllers
34
- # ...
35
- # end
36
- #
37
- # module Blog::Helpers
38
- # alias_method :_R, :R
39
- # remove_method :R
40
- # include Reststop::Helpers
41
- # ...
42
- # end
43
- #
44
- # module Blog::Views
45
- # extend Reststop::Views
46
- # ...
47
- # end
48
- #
49
- # The hope is that this could all get taken care of in a
50
- # `include Reststop` call (via overriding of #extended)
51
- #
52
- # See examples/blog.rb for a working example.
53
- require 'logger'
54
- $LOG = Logger.new(STDOUT)
55
-
56
- module Reststop
57
- module Base
58
- def reststop_service(*a)
59
- if @env['REQUEST_METHOD'] == 'POST' && (input['_method'] == 'put' || input['_method'] == 'delete')
60
- @env['REQUEST_METHOD'] = input._method.upcase
61
- @method = input._method
62
- end
63
- @a0=a[0] if !a.empty?
64
- camping_service(*a)
65
- end
66
-
67
- # Overrides Camping's render method to add the ability to specify a format
68
- # module when rendering a view.
69
- #
70
- # The format can also be specified in other ways (shown in this order
71
- # of precedence):
72
- #
73
- # 1. By providing a second parameter to render()
74
- # (eg: <tt>render(:foo, :HTML)</tt>)
75
- # 2. By setting the @format variable
76
- # 3. By providing a 'format' parameter in the request (i.e. input[:format])
77
- # 4. By adding a file-format extension to the url (e.g. /items.xml or
78
- # /items/2.html).
79
- #
80
- # For example, you could have:
81
- #
82
- # module Foobar::Views
83
- #
84
- # module HTML
85
- # def foo
86
- # # ... render some HTML content
87
- # end
88
- # end
89
- #
90
- # module RSS
91
- # def foo
92
- # # ... render some RSS content
93
- # end
94
- # end
95
- #
96
- # end
97
- #
98
- # Then in your controller, you would call render() like this:
99
- #
100
- # render(:foo, :HTML) # render the HTML version of foo
101
- #
102
- # or
103
- #
104
- # render(:foo, :RSS) # render the RSS version of foo
105
- #
106
- # or
107
- #
108
- # @format = :RSS
109
- # render(:foo) # render the RSS version of foo
110
- #
111
- # or
112
- #
113
- # # url is /foobar/1?format=RSS
114
- # render(:foo) # render the RSS version of foo
115
- #
116
- # or
117
- #
118
- # # url is /foobar/1.rss
119
- # render(:foo) # render the RSS version of foo
120
- #
121
- # If no format is specified, render() will behave like it normally does in
122
- # Camping, by looking for a matching view method directly
123
- # in the Views module.
124
- #
125
- # You can also specify a default format module by calling
126
- # <tt>default_format</tt> after the format module definition.
127
- # For example:
128
- #
129
- # module Foobar::Views
130
- # module HTML
131
- # # ... etc.
132
- # end
133
- # default_format :HTML
134
- # end
135
- #
136
- def reststop_render(action, format = nil)
137
- format = nil unless format.is_a? Symbol
138
-
139
- app_name = self.class.name.split("::").first # @techarch : get the name of the app
140
- format ||= @format
141
-
142
- if format.nil?
143
- begin
144
- ct = CONTENT_TYPE
145
- rescue NameError
146
- ct = 'text/html'
147
- end
148
-
149
- @headers['Content-Type'] ||= ct
150
- basic_render(action) # @techarch
151
- else
152
- mab = (app_name + '::Mab').constantize # @techarch : get the Mab class
153
- m = mab.new({}, self) # @techarch : instantiate Mab
154
- mod = (app_name + "::Views::#{format.to_s}").constantize # @techarch : get the right Views format class
155
-
156
- m.extend mod
157
-
158
- begin
159
- ct = mod::CONTENT_TYPE
160
- rescue NameError
161
- ct = "text/#{format.to_s.downcase}"
162
- end
163
- @headers['Content-Type'] = ct
164
-
165
- s = m.capture{m.send(action)}
166
- s = m.capture{send(:layout){s}} if /^_/!~@method.to_s and m.respond_to?(:layout) # @techarch : replaced a[0] by @method (not 100% sure that's right though)
167
- s
168
- end
169
- end
170
-
171
- # Performs a basic camping rendering (without use of a layout)
172
- # This method was added since the addition of Tilt support in camping
173
- # is assuming layout.
174
- def basic_render(action)
175
- app_name = self.class.name.split("::").first # @techarch : get the name of the app
176
- mab = (app_name + '::Mab').constantize # @techarch : get the Mab class
177
- m = mab.new({}, self) # @techarch : instantiate Mab
178
- s = m.capture{m.send(action)}
179
- end
180
- end
181
-
182
-
183
- module Views
184
- # Call this inside your Views module to set a default format.
185
- #
186
- # For example:
187
- #
188
- # module Foobar::Views
189
- # module HTML
190
- # # ... etc.
191
- # end
192
- # default_format :XML
193
- # end
194
- def default_format(m)
195
- mod = "#{self}::#{m.to_s}".constantize
196
- mab = self.to_s.gsub('::Views','::Mab').constantize # @techarch : get the Mab class
197
- mab.class_eval{include mod}
198
- end
199
- end
200
-
201
- module Helpers
202
- # Overrides Camping's routing helper to make it possible to route RESTful resources.
203
- #
204
- # Some usage examples:
205
- #
206
- # R(Kittens) # /kittens
207
- # R(Kittens, 'new') # /kittens/new
208
- # R(Kittens, 1, 'meow') # /kittens/1/meow
209
- # R(@kitten) # /kittens/1
210
- # R(@kitten, 'meow') # /kittens/1/meow
211
- # R(Kittens, 'list', :colour => 'black') # /kittens/list?colour=black
212
- #
213
- # The current output format is retained, so if the current <tt>@format</tt> is <tt>:XML</tt>,
214
- # the URL will be /kittens/1.xml rather than /kittens/1.
215
- #
216
- # Note that your controller names might not be loaded if you're calling <tt>R</tt> inside a
217
- # view module. In that case you should use the fully qualified name (i.e. Myapp::Controllers::Kittens)
218
- # or include the Controllers module into your view module.
219
- def R(c, *g)
220
-
221
- cl = c.class.name.split("::").last.pluralize
222
- app_name = c.class.name.split("::").first
223
- ctrl_cl = app_name + '::Controllers' # @techarch : get to the Controllers using the current app
224
- ctrl = (app_name != 'Class') ? ctrl_cl.constantize : Controllers
225
-
226
- if ctrl.constants.include?(cl) #@techarch updated to use new cl variable
227
- path = "/#{cl.underscore}/#{c.id}"
228
- path << ".#{@format.to_s.downcase}" if @format
229
- path << "/#{g.shift}" unless g.empty?
230
- self / path
231
- elsif c.respond_to?(:restful?) && c.restful?
232
- base = c.name.split("::").last.underscore
233
- id_or_action = g.shift
234
- if id_or_action.to_s =~ /\d+/ #@techarch needed a to_s after id_or_action to allow pattern matching
235
- id = id_or_action
236
- action = g.shift
237
- else
238
- action = id_or_action
239
- end
240
-
241
- path = "/#{base}"
242
- path << "/#{id}" if id
243
- path << "/#{action}" if action
244
- path << ".#{@format.to_s.downcase}" if @format
245
-
246
- #@techarch substituted U for u=Rack::Utils
247
- u=Rack::Utils
248
- path << "?#{g.collect{|a|a.collect{|k,v| u.escape(k)+"="+u.escape(v)}.join("&")}.join("&")}" unless g.empty? # FIXME: undefined behaviour if there are multiple arguments left
249
- return path
250
- else
251
- _R(c, *g)
252
- end
253
- end # def R
254
- end # module Helpers
255
-
256
- module Controllers
257
- def self.determine_format(input, env) #:nodoc:
258
- if input[:format] && !input[:format].empty?
259
- input[:format].upcase.intern
260
- elsif env['PATH_INFO'] =~ /\.([a-z]+)$/
261
- $~[1].upcase.intern
262
- end
263
- end
264
-
265
- # Calling <tt>REST "<resource name>"</tt> creates a controller with the
266
- # appropriate routes and maps your REST methods to standard
267
- # Camping controller mehods. This is meant to be used in your Controllers
268
- # module in place of <tt>R <routes></tt>.
269
- #
270
- # Your REST class should define the following methods:
271
- #
272
- # * create
273
- # * read(id)
274
- # * update(id)
275
- # * destroy(id)
276
- # * list
277
- #
278
- # Routes will be automatically created based on the resource name fed to the
279
- # REST method. <b>Your class must have the same (but CamelCaps'ed)
280
- # name as the resource name.</b> So if your resource name is 'kittens',
281
- # your controller class must be Kittens.
282
- #
283
- # For example:
284
- #
285
- # module Foobar::Controllers
286
- # class Kittens < REST 'kittens'
287
- # # POST /kittens
288
- # def create
289
- # end
290
- #
291
- # # GET /kittens/(\d+)
292
- # def read(id)
293
- # end
294
- #
295
- # # PUT /kittens/(\d+)
296
- # def update(id)
297
- # end
298
- #
299
- # # DELETE /kittens/(\d+)
300
- # def destroy(id)
301
- # end
302
- #
303
- # # GET /kittens
304
- # def list
305
- # end
306
- # end
307
- # end
308
- #
309
- # Custom actions are also possible. For example, to implement a 'meow'
310
- # action simply add a 'meow' method to the above controller:
311
- #
312
- # # POST/GET/PUT/DELETE /kittens/meow
313
- # # POST/GET/PUT/DELETE /kittens/(\d+)/meow
314
- # def meow(id)
315
- # end
316
- #
317
- # Note that a custom action will respond to all four HTTP methods
318
- # (POST/GET/PUT/DELETE).
319
- #
320
- # Optionally, you can specify a <tt>:prefix</tt> key that will prepend the
321
- # given string to the routes. For example, the following will create all
322
- # of the above routes, prefixed with "/pets"
323
- # (i.e. <tt>POST '/pets/kittens'</tt>, <tt>GET '/pets/kittens/(\d+)'</tt>,
324
- # etc.):
325
- #
326
- # module Foobar::Controllers
327
- # class Items < REST 'kittens', :prefix => '/pets'
328
- # # ...
329
- # end
330
- # end
331
- #
332
- # Format-based routing similar to that in ActiveResource is also implemented.
333
- # For example, to get a list of kittens in XML format, place a
334
- # <tt>GET</tt> call to <tt>/kittens.xml</tt>.
335
- # See the documentation for the render() method for more info.
336
- #
337
- def REST(r, options = {})
338
- crud = R "#{options[:prefix]}/#{r}/([0-9a-zA-Z]+)/([a-z_]+)(?:\.[a-z]+)?",
339
- "#{options[:prefix]}/#{r}/([0-9a-zA-Z]+)(?:\.[a-z]+)?",
340
- "#{options[:prefix]}/#{r}/([a-z_]+)(?:\.[a-z]+)?",
341
- "#{options[:prefix]}/#{r}(?:\.[a-z]+)?"
342
-
343
- crud.module_eval do
344
- meta_def(:restful?){true}
345
-
346
- $LOG.debug("Creating RESTful controller for #{r.inspect} using Reststop #{'pull version number here'}") if $LOG
347
-
348
- def get(id_or_custom_action = nil, custom_action = nil) # :nodoc:
349
- id = input['id'] if input['id']
350
-
351
- custom_action = input['action'] if input['action']
352
-
353
- if self.methods.include? id_or_custom_action
354
- custom_action ||= id_or_custom_action
355
- id ||= nil
356
- else
357
- id ||= id_or_custom_action
358
- end
359
-
360
- id = id.to_i if id && id =~ /^[0-9]+$/
361
-
362
- @format = Reststop::Controllers.determine_format(input, @env)
363
-
364
- begin
365
- if id.nil? && input['id'].nil?
366
- custom_action ? send(custom_action) : list
367
- else
368
- custom_action ? send(custom_action, id || input['id']) : read(id || input['id'])
369
- end
370
- rescue NoMethodError => e
371
- # FIXME: this is probably not a good way to do this, but we need to somehow differentiate
372
- # between 'no such route' vs. other NoMethodErrors
373
- if e.message =~ /no such method/
374
- return no_method(e)
375
- else
376
- raise e
377
- end
378
- rescue ActiveRecord::RecordNotFound => e
379
- return not_found(e)
380
- end
381
- end
382
-
383
-
384
- def post(custom_action = nil) # :nodoc:
385
- @format = Reststop::Controllers.determine_format(input, @env)
386
- custom_action ? send(custom_action) : create
387
- end
388
-
389
- def put(id, custom_action = nil) # :nodoc:
390
- id = id.to_i if id =~ /^[0-9]+$/
391
- @format = Reststop::Controllers.determine_format(input, @env)
392
- custom_action ? send(custom_action, id || input['id']) : update(id || input['id'])
393
- end
394
-
395
- def delete(id, custom_action = nil) # :nodoc:
396
- id = id.to_i if id =~ /^[0-9]+$/
397
- @format = Reststop::Controllers.determine_format(input, @env)
398
- custom_action ? send(custom_action, id || input['id']) : destroy(id || input['id'])
399
- end
400
-
401
- private
402
- def _error(message, status_code = 500, e = nil)
403
- @status = status_code
404
- @message = message
405
- begin
406
- render "error_#{status_code}".intern
407
- rescue NoMethodError
408
- if @format.to_s == 'XML'
409
- "<error code='#{status_code}'>#{@message}</error>"
410
- else
411
- out = "<strong>#{@message}</strong>"
412
- out += "<pre style='color: #bbb'><strong>#{e.class}: #{e}</strong>\n#{e.backtrace.join("\n")}</pre>" if e
413
- out
414
- end
415
- end
416
- end
417
-
418
- def no_method(e)
419
- _error("No controller method responds to this route!", 501, e)
420
- end
421
-
422
- def not_found(e)
423
- _error("Record not found!", 404, e)
424
- end
425
- end
426
- crud
427
- end # def REST
428
- end # module Controllers
429
- end # module Reststop
430
-
431
- module Markaby
432
- class Builder
433
- # Modifies Markaby's 'form' generator so that if a 'method' parameter
434
- # is supplied, a hidden '_method' input is automatically added.
435
- def form(*args, &block)
436
- options = args[0] if args && args[0] && args[0].kind_of?(Hash)
437
- inside = capture &block
438
-
439
- if options && options.has_key?(:method)
440
- inside = input(:type => 'hidden', :name => '_method', :value => options[:method]) +
441
- inside
442
- if options[:method].to_s === 'put' || options[:method].to_s == 'delete'
443
- options[:method] = 'post'
444
- end
445
- end
446
-
447
- tag!(:form, options || args[0]) {inside}
448
- end
449
- end
450
- end
1
+ # Right now you'll have to do some weird gymnastics to get this hooked in to a Camping app...
2
+ # Something like:
3
+ #
4
+ # Camping.goes :Blog
5
+ #
6
+ # module Blog
7
+ # include Camping::Session
8
+ # include Reststop
9
+ #
10
+ # Controllers.extend Reststop::Controllers
11
+ # end
12
+ #
13
+ # module Blog::Base
14
+ # alias camping_render render
15
+ # alias camping_service service
16
+ # alias camping_lookup lookup
17
+ # include Reststop::Base
18
+ # alias service reststop_service
19
+ # alias render reststop_render
20
+ #
21
+ # # Overrides the new Tilt-centric lookup method In camping
22
+ # # RESTstop needs to have a first try at looking up the view
23
+ # # located in the Views::HTML module.
24
+ # def lookup(n)
25
+ # T.fetch(n.to_sym) do |k|
26
+ # t = Blog::Views::HTML.method_defined?(k) || camping_lookup(n)
27
+ # end
28
+ # end
29
+
30
+ # end
31
+ #
32
+ # module Blog::Controllers
33
+ # extend Reststop::Controllers
34
+ # ...
35
+ # end
36
+ #
37
+ # module Blog::Helpers
38
+ # alias_method :_R, :R
39
+ # remove_method :R
40
+ # include Reststop::Helpers
41
+ # ...
42
+ # end
43
+ #
44
+ # module Blog::Views
45
+ # extend Reststop::Views
46
+ # ...
47
+ # end
48
+ #
49
+ # The hope is that this could all get taken care of in a
50
+ # `include Reststop` call (via overriding of #extended)
51
+ #
52
+ # See examples/blog.rb for a working example.
53
+ require 'logger'
54
+ $LOG = Logger.new(STDOUT)
55
+
56
+ module Reststop
57
+ module Base
58
+ def reststop_service(*a)
59
+ if @env['REQUEST_METHOD'] == 'POST' && (input['_method'] == 'put' || input['_method'] == 'delete')
60
+ @env['REQUEST_METHOD'] = input._method.upcase
61
+ @method = input._method
62
+ end
63
+ @a0=a[0] if !a.empty?
64
+ camping_service(*a)
65
+ end
66
+
67
+ # Overrides Camping's render method to add the ability to specify a format
68
+ # module when rendering a view.
69
+ #
70
+ # The format can also be specified in other ways (shown in this order
71
+ # of precedence):
72
+ #
73
+ # 1. By providing a second parameter to render()
74
+ # (eg: <tt>render(:foo, :HTML)</tt>)
75
+ # 2. By setting the @format variable
76
+ # 3. By providing a 'format' parameter in the request (i.e. input[:format])
77
+ # 4. By adding a file-format extension to the url (e.g. /items.xml or
78
+ # /items/2.html).
79
+ #
80
+ # For example, you could have:
81
+ #
82
+ # module Foobar::Views
83
+ #
84
+ # module HTML
85
+ # def foo
86
+ # # ... render some HTML content
87
+ # end
88
+ # end
89
+ #
90
+ # module RSS
91
+ # def foo
92
+ # # ... render some RSS content
93
+ # end
94
+ # end
95
+ #
96
+ # end
97
+ #
98
+ # Then in your controller, you would call render() like this:
99
+ #
100
+ # render(:foo, :HTML) # render the HTML version of foo
101
+ #
102
+ # or
103
+ #
104
+ # render(:foo, :RSS) # render the RSS version of foo
105
+ #
106
+ # or
107
+ #
108
+ # @format = :RSS
109
+ # render(:foo) # render the RSS version of foo
110
+ #
111
+ # or
112
+ #
113
+ # # url is /foobar/1?format=RSS
114
+ # render(:foo) # render the RSS version of foo
115
+ #
116
+ # or
117
+ #
118
+ # # url is /foobar/1.rss
119
+ # render(:foo) # render the RSS version of foo
120
+ #
121
+ # If no format is specified, render() will behave like it normally does in
122
+ # Camping, by looking for a matching view method directly
123
+ # in the Views module.
124
+ #
125
+ # You can also specify a default format module by calling
126
+ # <tt>default_format</tt> after the format module definition.
127
+ # For example:
128
+ #
129
+ # module Foobar::Views
130
+ # module HTML
131
+ # # ... etc.
132
+ # end
133
+ # default_format :HTML
134
+ # end
135
+ #
136
+ def reststop_render(action, format = nil)
137
+ format = nil unless format.is_a? Symbol
138
+
139
+ app_name = self.class.name.split("::").first # @techarch : get the name of the app
140
+ format ||= @format
141
+
142
+ if format.nil?
143
+ begin
144
+ ct = CONTENT_TYPE
145
+ rescue NameError
146
+ ct = 'text/html'
147
+ end
148
+
149
+ @headers['Content-Type'] ||= ct
150
+ basic_render(action) # @techarch
151
+ else
152
+ mab = (app_name + '::Mab').constantize # @techarch : get the Mab class
153
+ m = mab.new({}, self) # @techarch : instantiate Mab
154
+ mod = (app_name + "::Views::#{format.to_s}").constantize # @techarch : get the right Views format class
155
+
156
+ m.extend mod
157
+
158
+ begin
159
+ ct = mod::CONTENT_TYPE
160
+ rescue NameError
161
+ ct = "#{format == :HTML ? 'text' : 'application'}/#{format.to_s.downcase}" #@techarch - used to be text/***
162
+ end
163
+ @headers['Content-Type'] = ct
164
+
165
+ s = m.capture{m.send(action)}
166
+ s = m.capture{send(:layout){s}} if /^_/!~@method.to_s and m.respond_to?(:layout) # @techarch : replaced a[0] by @method (not 100% sure that's right though)
167
+ s
168
+ end
169
+ end
170
+
171
+ # Performs a basic camping rendering (without use of a layout)
172
+ # This method was added since the addition of Tilt support in camping
173
+ # is assuming layout.
174
+ def basic_render(action)
175
+ app_name = self.class.name.split("::").first # @techarch : get the name of the app
176
+ mab = (app_name + '::Mab').constantize # @techarch : get the Mab class
177
+ m = mab.new({}, self) # @techarch : instantiate Mab
178
+ s = m.capture{m.send(action)}
179
+ s = m.capture{send(:layout){s}} if /^_/!~@method.to_s and m.respond_to?(:layout) # @techarch : replaced a[0] by @method (not 100% sure that's right though)
180
+ end
181
+ end
182
+
183
+
184
+ module Views
185
+ # Call this inside your Views module to set a default format.
186
+ #
187
+ # For example:
188
+ #
189
+ # module Foobar::Views
190
+ # module HTML
191
+ # # ... etc.
192
+ # end
193
+ # default_format :XML
194
+ # end
195
+ def default_format(m)
196
+ mod = "#{self}::#{m.to_s}".constantize
197
+ mab = self.to_s.gsub('::Views','::Mab').constantize # @techarch : get the Mab class
198
+ mab.class_eval{include mod}
199
+ end
200
+ end
201
+
202
+ module Helpers
203
+ # Overrides Camping's routing helper to make it possible to route RESTful resources.
204
+ #
205
+ # Some usage examples:
206
+ #
207
+ # R(Kittens) # /kittens
208
+ # R(Kittens, 'new') # /kittens/new
209
+ # R(Kittens, 1, 'meow') # /kittens/1/meow
210
+ # R(@kitten) # /kittens/1
211
+ # R(@kitten, 'meow') # /kittens/1/meow
212
+ # R(Kittens, 'list', :colour => 'black') # /kittens/list?colour=black
213
+ #
214
+ # The current output format is retained, so if the current <tt>@format</tt> is <tt>:XML</tt>,
215
+ # the URL will be /kittens/1.xml rather than /kittens/1.
216
+ #
217
+ # Note that your controller names might not be loaded if you're calling <tt>R</tt> inside a
218
+ # view module. In that case you should use the fully qualified name (i.e. Myapp::Controllers::Kittens)
219
+ # or include the Controllers module into your view module.
220
+ def R(c, *g)
221
+
222
+ cl = c.class.name.split("::").last.pluralize
223
+ app_name = c.class.name.split("::").first
224
+ ctrl_cl = app_name + '::Controllers' # @techarch : get to the Controllers using the current app
225
+ ctrl = (app_name != 'Class') ? ctrl_cl.constantize : Controllers
226
+
227
+ if ctrl.constants.include?(cl) #@techarch updated to use new cl variable
228
+ path = "/#{cl.underscore}/#{c.id}"
229
+ path << ".#{@format.to_s.downcase}" if @format
230
+ path << "/#{g.shift}" unless g.empty?
231
+ self / path
232
+ elsif c.respond_to?(:restful?) && c.restful?
233
+ base = c.name.split("::").last.underscore
234
+ id_or_action = g.shift
235
+ if id_or_action.to_s =~ /\d+/ #@techarch needed a to_s after id_or_action to allow pattern matching
236
+ id = id_or_action
237
+ action = g.shift
238
+ else
239
+ action = id_or_action
240
+ end
241
+
242
+ path = "/#{base}"
243
+ path << "/#{id}" if id
244
+ path << "/#{action}" if action
245
+ path << ".#{@format.to_s.downcase}" if @format
246
+
247
+ #@techarch substituted U for u=Rack::Utils
248
+ u=Rack::Utils
249
+ path << "?#{g.collect{|a|a.collect{|k,v| u.escape(k)+"="+u.escape(v)}.join("&")}.join("&")}" unless g.empty? # FIXME: undefined behaviour if there are multiple arguments left
250
+ return path
251
+ else
252
+ _R(c, *g)
253
+ end
254
+ end # def R
255
+ end # module Helpers
256
+
257
+ module Controllers
258
+ def self.determine_format(input, env) #:nodoc:
259
+ if input[:format] && !input[:format].empty?
260
+ input[:format].upcase.intern
261
+ elsif env['PATH_INFO'] =~ /\.([a-z]+)$/
262
+ $~[1].upcase.intern
263
+ end
264
+ end
265
+
266
+ # Calling <tt>REST "<resource name>"</tt> creates a controller with the
267
+ # appropriate routes and maps your REST methods to standard
268
+ # Camping controller mehods. This is meant to be used in your Controllers
269
+ # module in place of <tt>R <routes></tt>.
270
+ #
271
+ # Your REST class should define the following methods:
272
+ #
273
+ # * create
274
+ # * read(id)
275
+ # * update(id)
276
+ # * destroy(id)
277
+ # * list
278
+ #
279
+ # Routes will be automatically created based on the resource name fed to the
280
+ # REST method. <b>Your class must have the same (but CamelCaps'ed)
281
+ # name as the resource name.</b> So if your resource name is 'kittens',
282
+ # your controller class must be Kittens.
283
+ #
284
+ # For example:
285
+ #
286
+ # module Foobar::Controllers
287
+ # class Kittens < REST 'kittens'
288
+ # # POST /kittens
289
+ # def create
290
+ # end
291
+ #
292
+ # # GET /kittens/(\d+)
293
+ # def read(id)
294
+ # end
295
+ #
296
+ # # PUT /kittens/(\d+)
297
+ # def update(id)
298
+ # end
299
+ #
300
+ # # DELETE /kittens/(\d+)
301
+ # def destroy(id)
302
+ # end
303
+ #
304
+ # # GET /kittens
305
+ # def list
306
+ # end
307
+ # end
308
+ # end
309
+ #
310
+ # Custom actions are also possible. For example, to implement a 'meow'
311
+ # action simply add a 'meow' method to the above controller:
312
+ #
313
+ # # POST/GET/PUT/DELETE /kittens/meow
314
+ # # POST/GET/PUT/DELETE /kittens/(\d+)/meow
315
+ # def meow(id)
316
+ # end
317
+ #
318
+ # Note that a custom action will respond to all four HTTP methods
319
+ # (POST/GET/PUT/DELETE).
320
+ #
321
+ # Optionally, you can specify a <tt>:prefix</tt> key that will prepend the
322
+ # given string to the routes. For example, the following will create all
323
+ # of the above routes, prefixed with "/pets"
324
+ # (i.e. <tt>POST '/pets/kittens'</tt>, <tt>GET '/pets/kittens/(\d+)'</tt>,
325
+ # etc.):
326
+ #
327
+ # module Foobar::Controllers
328
+ # class Items < REST 'kittens', :prefix => '/pets'
329
+ # # ...
330
+ # end
331
+ # end
332
+ #
333
+ # Format-based routing similar to that in ActiveResource is also implemented.
334
+ # For example, to get a list of kittens in XML format, place a
335
+ # <tt>GET</tt> call to <tt>/kittens.xml</tt>.
336
+ # See the documentation for the render() method for more info.
337
+ #
338
+ def REST(r, options = {})
339
+ crud = R "#{options[:prefix]}/#{r}/([0-9a-zA-Z]+)/([a-z_]+)(?:\.[a-z]+)?",
340
+ "#{options[:prefix]}/#{r}/([0-9a-zA-Z]+)(?:\.[a-z]+)?",
341
+ "#{options[:prefix]}/#{r}/([a-z_]+)(?:\.[a-z]+)?",
342
+ "#{options[:prefix]}/#{r}(?:\.[a-z]+)?"
343
+
344
+ crud.module_eval do
345
+ meta_def(:restful?){true}
346
+
347
+ $LOG.debug("Creating RESTful controller for #{r.inspect} using Reststop #{'pull version number here'}") if $LOG
348
+
349
+ def get(id_or_custom_action = nil, custom_action = nil) # :nodoc:
350
+ id = input['id'] if input['id']
351
+
352
+ custom_action = input['action'] if input['action']
353
+
354
+ if self.methods.include? id_or_custom_action
355
+ custom_action ||= id_or_custom_action
356
+ id ||= nil
357
+ else
358
+ id ||= id_or_custom_action
359
+ end
360
+
361
+ id = id.to_i if id && id =~ /^[0-9]+$/
362
+
363
+ @format = Reststop::Controllers.determine_format(input, @env)
364
+
365
+ begin
366
+ if id.nil? && input['id'].nil?
367
+ custom_action ? send(custom_action) : list
368
+ else
369
+ custom_action ? send(custom_action, id || input['id']) : read(id || input['id'])
370
+ end
371
+ rescue NoMethodError => e
372
+ # FIXME: this is probably not a good way to do this, but we need to somehow differentiate
373
+ # between 'no such route' vs. other NoMethodErrors
374
+ if e.message =~ /no such method/
375
+ return no_method(e)
376
+ else
377
+ raise e
378
+ end
379
+ rescue ActiveRecord::RecordNotFound => e
380
+ return not_found(e)
381
+ end
382
+ end
383
+
384
+
385
+ def post(custom_action = nil) # :nodoc:
386
+ @format = Reststop::Controllers.determine_format(input, @env)
387
+ custom_action ? send(custom_action) : create
388
+ end
389
+
390
+ def put(id, custom_action = nil) # :nodoc:
391
+ id = id.to_i if id =~ /^[0-9]+$/
392
+ @format = Reststop::Controllers.determine_format(input, @env)
393
+ custom_action ? send(custom_action, id || input['id']) : update(id || input['id'])
394
+ end
395
+
396
+ def delete(id, custom_action = nil) # :nodoc:
397
+ id = id.to_i if id =~ /^[0-9]+$/
398
+ @format = Reststop::Controllers.determine_format(input, @env)
399
+ custom_action ? send(custom_action, id || input['id']) : destroy(id || input['id'])
400
+ end
401
+
402
+ private
403
+ def _error(message, status_code = 500, e = nil)
404
+ @status = status_code
405
+ @message = message
406
+ begin
407
+ render "error_#{status_code}".intern
408
+ rescue NoMethodError
409
+ if @format.to_s == 'XML'
410
+ "<error code='#{status_code}'>#{@message}</error>"
411
+ else
412
+ out = "<strong>#{@message}</strong>"
413
+ out += "<pre style='color: #bbb'><strong>#{e.class}: #{e}</strong>\n#{e.backtrace.join("\n")}</pre>" if e
414
+ out
415
+ end
416
+ end
417
+ end
418
+
419
+ def no_method(e)
420
+ _error("No controller method responds to this route!", 501, e)
421
+ end
422
+
423
+ def not_found(e)
424
+ _error("Record not found!", 404, e)
425
+ end
426
+ end
427
+ crud
428
+ end # def REST
429
+ end # module Controllers
430
+ end # module Reststop
431
+
432
+ module Markaby
433
+ class Builder
434
+ # Modifies Markaby's 'form' generator so that if a 'method' parameter
435
+ # is supplied, a hidden '_method' input is automatically added.
436
+ def form(*args, &block)
437
+ options = args[0] if args && args[0] && args[0].kind_of?(Hash)
438
+ inside = capture &block
439
+
440
+ if options && options.has_key?(:method)
441
+ inside = input(:type => 'hidden', :name => '_method', :value => options[:method]) +
442
+ inside
443
+ if options[:method].to_s === 'put' || options[:method].to_s == 'delete'
444
+ options[:method] = 'post'
445
+ end
446
+ end
447
+
448
+ tag!(:form, options || args[0]) {inside}
449
+ end
450
+ end
451
+ end