reststop 0.4.1 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,401 +1,204 @@
1
- require 'camping'
2
- require 'reststop/version'
3
-
4
- #--
5
- # This file is part of Reststop.
6
- #
7
- # Reststop is free software; you can redistribute it and/or modify
8
- # it under the terms of the GNU Lesser General Public License as
9
- # published by the Free Software Foundation; either version 3 of
10
- # the License, or (at your option) any later version.
1
+ # Right now you'll have to do some weird gymnastics to get this hooked in to a Camping app...
2
+ # Something like:
11
3
  #
12
- # Reststop is distributed in the hope that it will be useful,
13
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
- # GNU General Public License for more details.
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
16
20
  #
17
- # You should have received a copy of the GNU Lesser General Public
18
- # License along with this program. If not, see
19
- # <http://www.gnu.org/licenses/>.
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
21
29
 
22
- # Extends and overrides Camping for convenient RESTfulness.
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
23
48
  #
24
- # Have a look at:
49
+ # The hope is that this could all get taken care of in a
50
+ # `include Reststop` call (via overriding of #extended)
25
51
  #
26
- # * Camping::Controllers#REST for help on using RESTful controllers
27
- # * Camping#render for help on grouping your views by output format
28
- #
29
- module Camping
30
-
31
- # Overrides Camping's goes() mechanism so that we can add our stuff.
32
- # ... there must be a saner way to do this? >:|
33
- #
34
- # Also modifies Camping's qsp() method to allow parsing of XML input data.
35
- #
36
- # FIXME: looks like this breaks auto-reloading when using the camping
37
- # server for launching apps :(
38
- S2 = IO.read(__FILE__).gsub(/^ S2 = I.+$/,'') # :nodoc:
39
- class << self
40
- # alias_method call is conditional only to make `rake package` happy
41
- alias_method :camping_goes, :goes
42
- def goes(m) # :nodoc:
43
- camping_goes m
44
- eval S2.gsub('Camping', m.to_s), TOPLEVEL_BINDING
45
- end
46
- end
52
+ # See examples/blog.rb for a working example.
53
+ require 'logger'
54
+ $LOG = Logger.new(STDOUT)
47
55
 
48
- # Overrides Camping's query parsing method so that XML input is parsed
49
- # into @input as an object usable more or less in the same manner as
50
- # a standard Hash input.
51
- #
52
- # This is necessary for dealing with ActiveResource calls, since ActiveResource
53
- # submits its POST and PUT data as XML instead of the standard CGI query
54
- # string.
55
- #
56
- # The method automatically determines whether input is XML or standard
57
- # CGI query and parses it accordingly.
58
- def self.qsp(qs, d='&;', y=nil, z=H[])
59
- if qs.kind_of?(String) && !qs.nil? && !qs.empty? && qs =~ /^<\?xml/
60
- qxp(qs)
61
- else
62
- m = proc {|_,o,n|o.u(n,&m)rescue([*o]<<n)}
63
- (qs||'').
64
- split(/[#{d}] */n).
65
- inject((b,z=z,H[])[0]) { |h,p| k, v=un(p).split('=',2)
66
- h.u(k.split(/[\]\[]+/).reverse.
67
- inject(y||v) { |x,i| H[i,x] },&m)
68
- }
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)
69
65
  end
70
- end
71
66
 
72
- # Parse an XML query (input) into a Hash usable more or less
73
- # the same way as a Camping's standard Hash input.
74
- def self.qxp(qxml)
75
- #xml = XmlSimple.xml_in_string(qxml, 'forcearray' => false)
76
- H.new Hash.from_xml(qxml)
77
- end
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
78
138
 
79
- # This override is taken and slightly modified from the Camping mailing list;
80
- # it fakes PUT/DELETE HTTP methods, since many browsers don't support them.
81
- #
82
- # In your forms you will have to add:
83
- #
84
- # input :name => '_method', :type => 'hidden', :value => 'VERB'
85
- #
86
- # ... where VERB is one of put, post, or delete. The form's actual :method
87
- # parameter must be 'post' (i.e. :method => post).
88
- #
89
- def service(*a)
90
-
91
- if @env.REQUEST_METHOD == 'POST' && (input['_method'] == 'put' || input['_method'] == 'delete')
92
- @env.REQUEST_METHOD = input._method.upcase
93
- @method = input._method
94
- end
95
-
96
- super(*a)
97
- end
98
-
99
- # Overrides Camping's render method to add the ability to specify a format
100
- # module when rendering a view.
101
- #
102
- # The format can also be specified in other ways (shown in this order
103
- # of precedence):
104
- #
105
- # 1. By providing a second parameter to render()
106
- # (eg: <tt>render(:foo, :HTML)</tt>)
107
- # 2. By setting the @format variable
108
- # 3. By providing a 'format' parameter in the request (i.e. input[:format])
109
- # 4. By adding a file-format extension to the url (e.g. /items.xml or
110
- # /items/2.html).
111
- #
112
- # For example, you could have:
113
- #
114
- # module Foobar::Views
115
- #
116
- # module HTML
117
- # def foo
118
- # # ... render some HTML content
119
- # end
120
- # end
121
- #
122
- # module RSS
123
- # def foo
124
- # # ... render some RSS content
125
- # end
126
- # end
127
- #
128
- # end
129
- #
130
- # Then in your controller, you would call render() like this:
131
- #
132
- # render(:foo, :HTML) # render the HTML version of foo
133
- #
134
- # or
135
- #
136
- # render(:foo, :RSS) # render the RSS version of foo
137
- #
138
- # or
139
- #
140
- # @format = :RSS
141
- # render(:foo) # render the RSS version of foo
142
- #
143
- # or
144
- #
145
- # # url is /foobar/1?format=RSS
146
- # render(:foo) # render the RSS version of foo
147
- #
148
- # or
149
- #
150
- # # url is /foobar/1.rss
151
- # render(:foo) # render the RSS version of foo
152
- #
153
- # If no format is specified, render() will behave like it normally does in
154
- # Camping, by looking for a matching view method directly
155
- # in the Views module.
156
- #
157
- # You can also specify a default format module by calling
158
- # <tt>default_format</tt> after the format module definition.
159
- # For example:
160
- #
161
- # module Foobar::Views
162
- # module HTML
163
- # # ... etc.
164
- # end
165
- # default_format :HTML
166
- # end
167
- #
168
- def render(action, format = nil)
169
- format ||= @format
170
-
171
- if format.nil?
172
- begin
173
- ct = CONTENT_TYPE
174
- rescue NameError
175
- ct = 'text/html'
176
- end
177
- @headers['Content-Type'] ||= ct
178
-
179
- super(action)
180
- else
181
- m = Mab.new({}, self)
182
- mod = "Camping::Views::#{format.to_s}".constantize
183
- m.extend mod
184
-
185
- begin
186
- ct = mod::CONTENT_TYPE
187
- rescue NameError
188
- ct = "text/#{format.to_s.downcase}"
189
- end
190
- @headers['Content-Type'] = ct
191
-
192
- s = m.capture{m.send(action)}
193
- s = m.capture{send(:layout){s}} if /^_/!~a[0].to_s and m.respond_to?(:layout)
194
- s
195
- end
196
- end
139
+ app_name = self.class.name.split("::").first # @techarch : get the name of the app
140
+ format ||= @format
197
141
 
198
- # See Camping#render
199
- module Views
200
- class << self
201
- # Call this inside your Views module to set a default format.
202
- #
203
- # For example:
204
- #
205
- # module Foobar::Views
206
- # module HTML
207
- # # ... etc.
208
- # end
209
- # default_format :XML
210
- # end
211
- def default_format(m)
212
- mod = "Camping::Views::#{m.to_s}".constantize
213
- Mab.class_eval{include mod}
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
214
168
  end
215
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
216
180
  end
181
+
217
182
 
218
- module Controllers
219
- class << self
220
- def read_format(input, env) #:nodoc:
221
- if input[:format] && !input[:format].empty?
222
- input[:format].upcase.intern
223
- elsif env['PATH_INFO'] =~ /\.([a-z]+)$/
224
- $~[1].upcase.intern
225
- end
226
- end
227
-
228
- # Calling <tt>REST "<resource name>"</tt> creates a controller with the
229
- # appropriate routes and maps your REST methods to standard
230
- # Camping controller mehods. This is meant to be used in your Controllers
231
- # module in place of <tt>R <routes></tt>.
232
- #
233
- # Your REST class should define the following methods:
234
- #
235
- # * create
236
- # * read(id)
237
- # * update(id)
238
- # * destroy(id)
239
- # * list
240
- #
241
- # Routes will be automatically created based on the resource name fed to the
242
- # REST method. <b>Your class must have the same (but CamelCaps'ed)
243
- # name as the resource name.</b> So if your resource name is 'kittens',
244
- # your controller class must be Kittens.
245
- #
246
- # For example:
247
- #
248
- # module Foobar::Controllers
249
- # class Kittens < REST 'kittens'
250
- # # POST /kittens
251
- # def create
252
- # end
253
- #
254
- # # GET /kittens/(\d+)
255
- # def read(id)
256
- # end
257
- #
258
- # # PUT /kittens/(\d+)
259
- # def update(id)
260
- # end
261
- #
262
- # # DELETE /kittens/(\d+)
263
- # def destroy(id)
264
- # end
265
- #
266
- # # GET /kittens
267
- # def list
268
- # end
269
- # end
270
- # end
271
- #
272
- # Custom actions are also possible. For example, to implement a 'meow'
273
- # action simply add a 'meow' method to the above controller:
274
- #
275
- # # POST/GET/PUT/DELETE /kittens/meow
276
- # # POST/GET/PUT/DELETE /kittens/(\d+)/meow
277
- # def meow(id)
278
- # end
279
- #
280
- # Note that a custom action will respond to all four HTTP methods
281
- # (POST/GET/PUT/DELETE).
282
- #
283
- # Optionally, you can specify a <tt>:prefix</tt> key that will prepend the
284
- # given string to the routes. For example, the following will create all
285
- # of the above routes, prefixed with "/pets"
286
- # (i.e. <tt>POST '/pets/kittens'</tt>, <tt>GET '/pets/kittens/(\d+)'</tt>,
287
- # etc.):
288
- #
289
- # module Foobar::Controllers
290
- # class Items < REST 'kittens', :prefix => '/pets'
291
- # # ...
292
- # end
293
- # end
294
- #
295
- # Format-based routing similar to that in ActiveResource is also implemented.
296
- # For example, to get a list of kittens in XML format, place a
297
- # <tt>GET</tt> call to <tt>/kittens.xml</tt>.
298
- # See the documentation for the render() method for more info.
299
- #
300
- def REST(r, options = {})
301
- crud = R "#{options[:prefix]}/#{r}/([0-9a-zA-Z]+)/([a-z_]+)(?:\.[a-z]+)?",
302
- "#{options[:prefix]}/#{r}/([0-9a-zA-Z]+)(?:\.[a-z]+)?",
303
- "#{options[:prefix]}/#{r}/([a-z_]+)(?:\.[a-z]+)?",
304
- "#{options[:prefix]}/#{r}(?:\.[a-z]+)?"
305
-
306
- crud.module_eval do
307
- meta_def(:restful?){true}
308
-
309
- $LOG.debug("Creating RESTful controller for #{r.inspect} using Reststop #{::Reststop::VERSION::STRING}") if $LOG
310
-
311
- def get(id_or_custom_action = nil, custom_action = nil) # :nodoc:
312
- id = input['id'] if input['id']
313
-
314
- custom_action = input['action'] if input['action']
315
-
316
- if self.methods.include? id_or_custom_action
317
- custom_action ||= id_or_custom_action
318
- id ||= nil
319
- else
320
- id ||= id_or_custom_action
321
- end
322
-
323
- id = id.to_i if id && id =~ /^[0-9]+$/
324
-
325
- @format = Controllers.read_format(input, @env)
326
-
327
- begin
328
- if id.nil? && input['id'].nil?
329
- custom_action ? send(custom_action) : list
330
- else
331
- custom_action ? send(custom_action, id || input['id']) : read(id || input['id'])
332
- end
333
- rescue NoMethodError => e
334
- # FIXME: this is probably not a good way to do this, but we need to somehow differentiate
335
- # between 'no such route' vs. other NoMethodErrors
336
- if e.message =~ /no such method/
337
- return no_method(e)
338
- else
339
- raise e
340
- end
341
- rescue ActiveRecord::RecordNotFound => e
342
- return not_found(e)
343
- end
344
- end
345
-
346
-
347
- def post(custom_action = nil) # :nodoc:
348
- @format = Controllers.read_format(input, @env)
349
- custom_action ? send(custom_action) : create
350
- end
351
-
352
-
353
- def put(id, custom_action = nil) # :nodoc:
354
- id = id.to_i if id =~ /^[0-9]+$/
355
- @format = Controllers.read_format(input, @env)
356
- custom_action ? send(custom_action, id || input['id']) : update(id || input['id'])
357
- end
358
-
359
-
360
- def delete(id, custom_action = nil) # :nodoc:
361
- id = id.to_i if id =~ /^[0-9]+$/
362
- @format = Controllers.read_format(input, @env)
363
- custom_action ? send(custom_action, id || input['id']) : destroy(id || input['id'])
364
- end
365
-
366
- private
367
- def _error(message, status_code = 500, e = nil)
368
- @status = status_code
369
- @message = message
370
- begin
371
- render "error_#{status_code}".intern
372
- rescue NoMethodError
373
- if @format.to_s == 'XML'
374
- "<error code='#{status_code}'>#{@message}</error>"
375
- else
376
- out = "<strong>#{@message}</strong>"
377
- out += "<pre style='color: #bbb'><strong>#{e.class}: #{e}</strong>\n#{e.backtrace.join("\n")}</pre>" if e
378
- out
379
- end
380
- end
381
- end
382
-
383
- def no_method(e)
384
- _error("No controller method responds to this route!", 501, e)
385
- end
386
-
387
- def not_found(e)
388
- _error("Record not found!", 404, e)
389
- end
390
- end
391
- crud
392
- end
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}
393
198
  end
394
199
  end
395
200
 
396
201
  module Helpers
397
- alias_method :_R, :R
398
-
399
202
  # Overrides Camping's routing helper to make it possible to route RESTful resources.
400
203
  #
401
204
  # Some usage examples:
@@ -407,41 +210,223 @@ module Camping
407
210
  # R(@kitten, 'meow') # /kittens/1/meow
408
211
  # R(Kittens, 'list', :colour => 'black') # /kittens/list?colour=black
409
212
  #
410
- # The current output format is retained, so if the current <tt>@format</tt> is <tt>:XML</tt>,
213
+ # The current output format is retained, so if the current <tt>@format</tt> is <tt>:XML</tt>,
411
214
  # the URL will be /kittens/1.xml rather than /kittens/1.
412
215
  #
413
216
  # Note that your controller names might not be loaded if you're calling <tt>R</tt> inside a
414
217
  # view module. In that case you should use the fully qualified name (i.e. Myapp::Controllers::Kittens)
415
- # or include the Controllers module into your view module.
218
+ # or include the Controllers module into your view module.
416
219
  def R(c, *g)
417
- if Controllers.constants.include?(cl = c.class.name.split("::").last.pluralize)
418
- path = "/#{cl.underscore}/#{c.id}"
419
- path << ".#{@format.to_s.downcase}" if @format
420
- path << "/#{g.shift}" unless g.empty?
421
- self / path
422
- elsif c.respond_to?(:restful?) && c.restful?
423
- base = c.name.split("::").last.underscore
424
- id_or_action = g.shift
425
- if id_or_action =~ /\d+/
426
- id = id_or_action
427
- action = g.shift
428
- else
429
- action = id_or_action
430
- end
431
-
432
- path = "/#{base}"
433
- path << "/#{id}" if id
434
- path << "/#{action}" if action
435
- path << ".#{@format.to_s.downcase}" if @format
436
- 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
437
- return path
438
- else
439
- _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
440
262
  end
441
263
  end
442
- end
443
264
 
444
- end
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
445
430
 
446
431
  module Markaby
447
432
  class Builder
@@ -462,4 +447,4 @@ module Markaby
462
447
  tag!(:form, options || args[0]) {inside}
463
448
  end
464
449
  end
465
- end
450
+ end