reststop 0.4.1 → 0.5.1

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,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