strelka 0.0.1pre4 → 0.0.1.pre129

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. data/History.rdoc +1 -1
  2. data/IDEAS.rdoc +62 -0
  3. data/Manifest.txt +38 -7
  4. data/README.rdoc +124 -5
  5. data/Rakefile +22 -6
  6. data/bin/leash +102 -157
  7. data/contrib/hoetemplate/.autotest.erb +23 -0
  8. data/contrib/hoetemplate/History.rdoc.erb +4 -0
  9. data/contrib/hoetemplate/Manifest.txt.erb +8 -0
  10. data/contrib/hoetemplate/README.rdoc.erb +17 -0
  11. data/contrib/hoetemplate/Rakefile.erb +24 -0
  12. data/contrib/hoetemplate/data/file_name/apps/file_name_app +36 -0
  13. data/contrib/hoetemplate/data/file_name/templates/layout.tmpl.erb +13 -0
  14. data/contrib/hoetemplate/data/file_name/templates/top.tmpl.erb +8 -0
  15. data/contrib/hoetemplate/lib/file_name.rb.erb +18 -0
  16. data/contrib/hoetemplate/spec/file_name_spec.rb.erb +21 -0
  17. data/data/strelka/apps/hello-world +30 -0
  18. data/lib/strelka/app/defaultrouter.rb +49 -30
  19. data/lib/strelka/app/errors.rb +121 -0
  20. data/lib/strelka/app/exclusiverouter.rb +40 -0
  21. data/lib/strelka/app/filters.rb +18 -7
  22. data/lib/strelka/app/negotiation.rb +122 -0
  23. data/lib/strelka/app/parameters.rb +171 -14
  24. data/lib/strelka/app/paramvalidator.rb +751 -0
  25. data/lib/strelka/app/plugins.rb +66 -46
  26. data/lib/strelka/app/restresources.rb +499 -0
  27. data/lib/strelka/app/router.rb +73 -0
  28. data/lib/strelka/app/routing.rb +140 -18
  29. data/lib/strelka/app/templating.rb +12 -3
  30. data/lib/strelka/app.rb +174 -24
  31. data/lib/strelka/constants.rb +0 -20
  32. data/lib/strelka/exceptions.rb +29 -0
  33. data/lib/strelka/httprequest/acceptparams.rb +377 -0
  34. data/lib/strelka/httprequest/negotiation.rb +257 -0
  35. data/lib/strelka/httprequest.rb +155 -7
  36. data/lib/strelka/httpresponse/negotiation.rb +579 -0
  37. data/lib/strelka/httpresponse.rb +140 -0
  38. data/lib/strelka/logging.rb +4 -1
  39. data/lib/strelka/mixins.rb +53 -0
  40. data/lib/strelka.rb +22 -1
  41. data/spec/data/error.tmpl +1 -0
  42. data/spec/lib/constants.rb +0 -1
  43. data/spec/lib/helpers.rb +21 -0
  44. data/spec/strelka/app/defaultrouter_spec.rb +41 -35
  45. data/spec/strelka/app/errors_spec.rb +212 -0
  46. data/spec/strelka/app/exclusiverouter_spec.rb +220 -0
  47. data/spec/strelka/app/filters_spec.rb +196 -0
  48. data/spec/strelka/app/negotiation_spec.rb +73 -0
  49. data/spec/strelka/app/parameters_spec.rb +149 -0
  50. data/spec/strelka/app/paramvalidator_spec.rb +1059 -0
  51. data/spec/strelka/app/plugins_spec.rb +26 -19
  52. data/spec/strelka/app/restresources_spec.rb +393 -0
  53. data/spec/strelka/app/router_spec.rb +63 -0
  54. data/spec/strelka/app/routing_spec.rb +183 -9
  55. data/spec/strelka/app/templating_spec.rb +1 -2
  56. data/spec/strelka/app_spec.rb +265 -32
  57. data/spec/strelka/exceptions_spec.rb +53 -0
  58. data/spec/strelka/httprequest/acceptparams_spec.rb +282 -0
  59. data/spec/strelka/httprequest/negotiation_spec.rb +246 -0
  60. data/spec/strelka/httprequest_spec.rb +204 -14
  61. data/spec/strelka/httpresponse/negotiation_spec.rb +464 -0
  62. data/spec/strelka/httpresponse_spec.rb +114 -0
  63. data/spec/strelka/mixins_spec.rb +99 -0
  64. data.tar.gz.sig +1 -0
  65. metadata +175 -79
  66. metadata.gz.sig +2 -0
  67. data/IDEAS.textile +0 -174
  68. data/data/strelka/apps/strelka-admin +0 -65
  69. data/data/strelka/apps/strelka-setup +0 -26
  70. data/data/strelka/bootstrap-config.rb +0 -34
  71. data/data/strelka/templates/admin/console.tmpl +0 -21
  72. data/data/strelka/templates/layout.tmpl +0 -30
  73. data/lib/strelka/process.rb +0 -19
@@ -1,34 +1,56 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ require 'set'
4
+ require 'tsort'
5
+
3
6
  require 'strelka' unless defined?( Strelka )
4
7
  require 'strelka/app' unless defined?( Strelka::App )
5
8
 
6
- # Pluggable functionality mixin for Strelka::App.
7
9
  class Strelka::App
8
10
 
11
+ # A topologically-sorted hash for plugin management
12
+ class PluginRegistry < Hash
13
+ include TSort
14
+ alias_method :tsort_each_node, :each_key
15
+ def tsort_each_child( node, &block )
16
+ mod = fetch( node ) { [] }
17
+ if mod.respond_to?( :successors )
18
+ mod.successors.each( &block )
19
+ else
20
+ mod.each( &block )
21
+ end
22
+ end
23
+ end
24
+
9
25
  # The Hash of loaded plugin modules, keyed by their downcased and symbolified
10
26
  # name (e.g., Strelka::App::Templating => :templating)
11
27
  class << self; attr_reader :loaded_plugins; end
12
- @loaded_plugins = {}
28
+ @loaded_plugins = PluginRegistry.new
13
29
 
14
30
 
15
- # Plugin Module extension -- adds registration, sorting, etc.
31
+ # Plugin Module extension -- adds registration, load-order support, etc.
16
32
  module Plugin
17
- include Comparable
18
33
 
19
34
  ### Mixin hook -- extend including objects instead.
20
35
  def self::included( mod )
21
36
  mod.extend( self )
22
37
  end
23
38
 
39
+
24
40
  ### Extension hook -- Extend the given object with methods for setting it
25
41
  ### up as a plugin for Strelka::Apps.
26
- def self::extend_object( object )
42
+ def self::extended( object )
27
43
  Strelka.log.debug "Extending %p as a Strelka::App::Plugin" % [ object ]
28
44
 
29
45
  super
30
46
  name = object.plugin_name
31
- object.instance_variable_set( :@load_order, {:before => [], :after => []} )
47
+ object.instance_variable_set( :@successors, Set.new )
48
+
49
+ # Register any pending dependencies for the newly-loaded plugin
50
+ if (( deps = Strelka::App.loaded_plugins[name] ))
51
+ Strelka.log.debug " installing deferred deps for %p" % [ name ]
52
+ object.run_after( *deps )
53
+ end
32
54
 
33
55
  Strelka.log.debug " adding %p (%p) to the plugin registry" % [ name, object ]
34
56
  Strelka::App.loaded_plugins[ name ] = object
@@ -39,36 +61,11 @@ class Strelka::App
39
61
  ### A P P E N D E D M E T H O D S
40
62
  #############################################################
41
63
 
42
- # An Array of Arrays that tracks which plugins should be installed before and after
43
- # itself, in that order.
44
- attr_reader :load_order
45
-
64
+ # An Array that tracks which plugins should be installed after itself.
65
+ attr_reader :successors
46
66
 
47
- ### Comparable operator -- use the plugin load_order to compare Plugin modules.
48
- def <=>( other_plugin )
49
- if self.before?( other_plugin ) || other_plugin.after?( self )
50
- return -1
51
- elsif self.after?( other_plugin ) || other_plugin.before?( self )
52
- return 1
53
- else
54
- return 0
55
- end
56
- end
57
67
 
58
-
59
- ### Returns true if the receiver has specified that it should run before +other_plugin+.
60
- def before?( other_plugin )
61
- return self.load_order[ :before ].include?( other_plugin.plugin_name )
62
- end
63
-
64
-
65
- ### Returns true if the receiver has specified that it should run after +other_plugin+.
66
- def after?( other_plugin )
67
- return self.load_order[ :after ].include?( other_plugin.plugin_name )
68
- end
69
-
70
-
71
- ### Return the name of the receiving plugin
68
+ ### Return the name of the receiving plugin
72
69
  def plugin_name
73
70
  name = self.name || "anonymous#{self.object_id}"
74
71
  name.sub!( /.*::/, '' )
@@ -79,14 +76,26 @@ class Strelka::App
79
76
  ### Register the receiver as needing to be run before +other_plugins+ for requests, and
80
77
  ### *after* them for responses.
81
78
  def run_before( *other_plugins )
82
- self.load_order[:before] += other_plugins
79
+ name = self.plugin_name
80
+ other_plugins.each do |other_name|
81
+ Strelka::App.loaded_plugins[ other_name ] ||= []
82
+ mod = Strelka::App.loaded_plugins[ other_name ]
83
+
84
+ if mod.respond_to?( :run_after )
85
+ mod.run_after( name )
86
+ else
87
+ Strelka.log.debug "%p plugin not yet loaded; setting up pending deps" % [ other_name ]
88
+ mod << name
89
+ end
90
+ end
83
91
  end
84
92
 
85
93
 
86
94
  ### Register the receiver as needing to be run after +other_plugins+ for requests, and
87
95
  ### *before* them for responses.
88
96
  def run_after( *other_plugins )
89
- self.load_order[:after] += other_plugins
97
+ Strelka.log.debug " %p will run after %p" % [ self, other_plugins ]
98
+ self.successors.merge( other_plugins )
90
99
  end
91
100
 
92
101
  end # module Plugin
@@ -113,13 +122,14 @@ class Strelka::App
113
122
  ### Class methods to add to classes with plugins.
114
123
  module ClassMethods
115
124
 
116
- ### Load the plugin with the given +name+, or nil if
125
+ ### Load the plugin with the given +name+
117
126
  def load_plugin( name )
118
127
 
119
128
  # Just return Modules as-is
120
129
  return name if name.is_a?( Strelka::App::Plugin )
130
+ mod = Strelka::App.loaded_plugins[ name.to_sym ]
121
131
 
122
- unless mod = Strelka::App.loaded_plugins[ name.to_sym ]
132
+ unless mod.is_a?( Module )
123
133
  Strelka.log.debug "Loading plugin from strelka/app/#{name}"
124
134
  require "strelka/app/#{name}"
125
135
  mod = Strelka::App.loaded_plugins[ name.to_sym ] or
@@ -159,11 +169,22 @@ class Strelka::App
159
169
  ### Load the plugins with the given +names+ and install them.
160
170
  def plugins( *names )
161
171
  # Load the associated Plugin Module objects
162
- mods = names.flatten.collect {|name| self.load_plugin(name) }.sort
163
-
164
- # Now install them in reverse order, as the ancestry array should have them
165
- # in LIFO order
166
- mods.reverse.each {|mod| self.install_plugin(mod) }
172
+ names.flatten.each {|name| self.load_plugin(name) }
173
+
174
+ sorted_plugins = Strelka::App.loaded_plugins.tsort.reverse
175
+ Strelka.log.debug "Sorted plugins: app -> %p <- Mongrel2" % [ sorted_plugins ]
176
+
177
+ # Install the plugins in reverse-sorted order
178
+ sorted_plugins.each do |name|
179
+ plugin = Strelka::App.loaded_plugins[ name ]
180
+ Strelka.log.debug "Considering %p" % [ name ]
181
+ if names.include?( name ) || names.include?( plugin )
182
+ Strelka.log.debug " installing"
183
+ self.install_plugin( plugin )
184
+ else
185
+ Strelka.log.debug " not used by this app; skipping"
186
+ end
187
+ end
167
188
  end
168
189
  alias_method :plugin, :plugins
169
190
 
@@ -192,9 +213,8 @@ class Strelka::App
192
213
 
193
214
 
194
215
  ### An alternate extension-point for the plugin system. Plugins can implement this method
195
- ### to alter or replace the +response+ to the specified +request+ after the regular
196
- ### request/response cycle is finished.
197
- def fixup_response( request, response )
216
+ ### to alter or replace the +response+ after the regular request/response cycle is finished.
217
+ def fixup_response( response )
198
218
  return response
199
219
  end
200
220
 
@@ -0,0 +1,499 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'set'
4
+ require 'sequel'
5
+
6
+ require 'strelka' unless defined?( Strelka )
7
+ require 'strelka/app' unless defined?( Strelka::App )
8
+
9
+
10
+ # RESTful resource utilities for Strelka::App.
11
+ #
12
+ # This plugin allows you to automatically set up RESTful service resources
13
+ # for Sequel::Model-derived classes.
14
+ #
15
+ # For example, if you have a model class called ACME::Customer for tracking
16
+ # customer data, you can set up a RESTful resource in your Strelka app like this:
17
+ #
18
+ # require 'strelka'
19
+ # require 'acme/customer'
20
+ #
21
+ # class ACME::RestServices < Strelka::App
22
+ # plugins :restresources
23
+ # resource ACME::Customer
24
+ # end
25
+ #
26
+ # Assuming the primary key for customers is a column called 'id', this will install
27
+ # the following routes:
28
+ #
29
+ # options 'customers'
30
+ # get 'customers'
31
+ # get 'customers/:id'
32
+ # post 'customers'
33
+ # put 'customers'
34
+ # put 'customers/:id'
35
+ # delete 'customers'
36
+ # delete 'customers/:id'
37
+ #
38
+ # The restresources plugin depends on the routing[Strelka::App::Routing],
39
+ # negotiation[Strelka::App::Negotiation], and
40
+ # parameters[Strelka::App::Parameters] plugins, and will load them
41
+ # automatically if they haven't been already.
42
+ #
43
+ # Stuff left to do:
44
+ #
45
+ # * Composite resources generated from associations
46
+ # * Honor If-unmodified-since and If-match headers
47
+ # * Caching support (ETag, If-modified-since)
48
+ # * Means of tailoring responses for requests for which the response
49
+ # isn't clearly specified in the RFC (DELETE /resource)
50
+ # * Sequel plugin for adding links to serialized representations
51
+ module Strelka::App::RestResources
52
+ extend Strelka::App::Plugin
53
+
54
+ # Resource route option defaults
55
+ DEFAULTS = {
56
+ prefix: '',
57
+ name: nil,
58
+ readonly: false,
59
+ use_transactions: true,
60
+ }.freeze
61
+
62
+
63
+ ### Inclusion callback -- overridden to also install dependencies.
64
+ def self::included( mod )
65
+ super
66
+
67
+ # Load the plugins this one depends on if they aren't already
68
+ mod.plugins :routing, :negotiation, :parameters
69
+
70
+ # Add validations for the limit and offset parameters
71
+ mod.param :limit, :integer
72
+ mod.param :offset, :integer
73
+
74
+ # Use the 'exclusive' router instead of the more-flexible
75
+ # Mongrel2-style default one
76
+ mod.router :exclusive
77
+ end
78
+
79
+
80
+ # Class methods to add to classes with REST resources.
81
+ module ClassMethods # :nodoc:
82
+ include Sequel::Inflections
83
+
84
+ # Set of verbs that are valid for a resource, keyed by the resource path
85
+ @resource_verbs = Hash.new {|h,k| h[k] = Set.new }
86
+
87
+ # Global options
88
+ @global_options = DEFAULTS.dup
89
+
90
+ # The list of REST routes assigned to Sequel::Model objects
91
+ attr_reader :resource_verbs
92
+
93
+ # The global resource options hash
94
+ attr_reader :global_options
95
+
96
+
97
+ ### Set the prefix for all following resource routes to +route+.
98
+ def resource_prefix( route )
99
+ self.global_options[ :prefix ] = route
100
+ end
101
+
102
+
103
+ ### Expose the specified +rsrcobj+ (which should be an object that responds to #dataset
104
+ ### and returns a Sequel::Dataset)
105
+ def resource( rsrcobj, options={} )
106
+ Strelka.log.debug "Adding REST resource for %p" % [ rsrcobj ]
107
+ options = self.global_options.merge( options )
108
+
109
+ # Figure out what the resource name is, and make the route from it
110
+ name = options[:name] || rsrcobj.implicit_table_name
111
+ route = [ options[:prefix], name ].compact.join( '/' )
112
+
113
+ # Set up parameters
114
+ self.add_parameters( rsrcobj, options )
115
+
116
+ # Make and install handler methods
117
+ Strelka.log.debug " adding readers"
118
+ self.add_options_handler( route, rsrcobj, options )
119
+ self.add_read_handler( route, rsrcobj, options )
120
+ self.add_collection_read_handler( route, rsrcobj, options )
121
+
122
+ # Add handler methods for the mutator parts of the API unless
123
+ # the resource is read-only
124
+ if options[:readonly]
125
+ Strelka.log.debug " skipping mutators (read-only set)"
126
+ else
127
+ self.add_collection_create_handler( route, rsrcobj, options )
128
+ self.add_update_handler( route, rsrcobj, options )
129
+ self.add_collection_update_handler( route, rsrcobj, options )
130
+ self.add_delete_handler( route, rsrcobj, options )
131
+ self.add_collection_deletion_handler( route, rsrcobj, options )
132
+ end
133
+
134
+ # Add any composite resources based on the +rsrcobj+'s associations
135
+ self.add_composite_resource_handlers( route, rsrcobj, options )
136
+ end
137
+
138
+
139
+ ### Add parameter declarations for parameters related to +rsrcobj+.
140
+ def add_parameters( rsrcobj, options )
141
+ Strelka.log.debug "Declaring validations for columns from %p" % [ rsrcobj ]
142
+ self.untaint_all_constraints = true
143
+ rsrcobj.db_schema.each do |col, config|
144
+ Strelka.log.debug " %s (%p)" % [ col, config[:type] ]
145
+ param col, config[:type]
146
+ end
147
+ end
148
+
149
+
150
+ ### Add a handler method for discovery for the specified +rsrcobj+.
151
+ ### OPTIONS /resources
152
+ def add_options_handler( route, rsrcobj, options )
153
+ # :TODO: Documentation for HTML mode (possibly using http://swagger.wordnik.com/)
154
+ Strelka.log.debug "Adding OPTIONS handler for %p" % [ route, rsrcobj ]
155
+ self.add_route( :OPTIONS, route, options ) do |req|
156
+ verbs = self.class.resource_verbs[ route ].sort
157
+ res = req.response
158
+
159
+ res.header.allowed = verbs.join(', ')
160
+ res.content_type = 'text/plain'
161
+ res.body = ''
162
+ res.status = HTTP::OK
163
+
164
+ return res
165
+ end
166
+
167
+ self.resource_verbs[ route ] << :OPTIONS
168
+ end
169
+
170
+
171
+ ### Add a handler method for reading a single instance of the specified +rsrcobj+, which should be a
172
+ ### Sequel::Model class or a ducktype-alike.
173
+ ### GET /resources/{id}
174
+ def add_read_handler( route_prefix, rsrcobj, options )
175
+ pkey = rsrcobj.primary_key
176
+ route = "#{route_prefix}/:#{pkey}"
177
+
178
+ Strelka.log.debug "Creating handler for reading a single %p: GET %s" % [ rsrcobj, route ]
179
+ self.add_route( :GET, route, options ) do |req|
180
+ finish_with( HTTP::BAD_REQUEST, req.params.error_messages.join("\n") ) unless
181
+ req.params.okay?
182
+
183
+ id = req.params[ pkey ]
184
+ resource = rsrcobj[ id ] or
185
+ finish_with( HTTP::NOT_FOUND, "No such %s [%p]" % [rsrcobj.table_name, id] )
186
+
187
+ res = req.response
188
+ res.for( :json, :yaml ) { resource }
189
+
190
+ return res
191
+ end
192
+
193
+ self.resource_verbs[ route_prefix ] << :GET << :HEAD
194
+ end
195
+
196
+
197
+ ### Add a handler method for reading a collection of the specified +rsrcobj+, which should be a
198
+ ### Sequel::Model class or a ducktype-alike.
199
+ ### GET /resources
200
+ def add_collection_read_handler( route, rsrcobj, options )
201
+ Strelka.log.debug "Creating handler for reading collections of %p: GET %s" % [ rsrcobj, route ]
202
+ self.add_route( :GET, route, options ) do |req|
203
+ finish_with( HTTP::BAD_REQUEST, req.params.error_messages.join("\n") ) unless
204
+ req.params.okay?
205
+
206
+ limit, offset = req.params.values_at( :limit, :offset )
207
+ res = req.response
208
+
209
+ dataset = rsrcobj.dataset
210
+ if limit
211
+ self.log.debug "Limiting result set to %p records" % [ limit ]
212
+ dataset = dataset.limit( limit, offset )
213
+ end
214
+
215
+ self.log.debug "Returning collection: %s" % [ dataset.sql ]
216
+ res.for( :json, :yaml ) { dataset.all }
217
+
218
+ return res
219
+ end
220
+
221
+ self.resource_verbs[ route ] << :GET << :HEAD
222
+ end
223
+
224
+
225
+ ### Add a handler method for creating a new instance of +rsrcobj+.
226
+ ### POST /resources
227
+ def add_collection_create_handler( route, rsrcobj, options )
228
+ Strelka.log.debug "Creating handler for creating %p resources: POST %s" % [ rsrcobj, route ]
229
+
230
+ self.add_route( :POST, route, options ) do |req|
231
+ finish_with( HTTP::BAD_REQUEST, req.params.error_messages.join(", ") ) unless
232
+ req.params.okay?
233
+
234
+ resource = rsrcobj.new( req.params.valid )
235
+
236
+ # Save it in a transaction, erroring if any of 'em fail validations
237
+ begin
238
+ resource.save
239
+ rescue Sequel::ValidationFailed => err
240
+ finish_with( HTTP::BAD_REQUEST, err.message )
241
+ end
242
+
243
+ # :TODO: Eventually, this should be factored out into the Sequel plugin
244
+ resuri = [ req.base_uri, route, resource.pk ].join( '/' )
245
+
246
+ res = req.response
247
+ res.status = HTTP::CREATED
248
+ res.headers.location = resuri
249
+ res.headers.content_location = resuri
250
+
251
+ res.for( :json, :yaml ) { resource }
252
+
253
+ return res
254
+ end
255
+
256
+ self.resource_verbs[ route ] << :POST
257
+ end
258
+
259
+
260
+ ### Add a handler method for updating an instance of +rsrcobj+.
261
+ ### PUT /resources/{id}
262
+ def add_update_handler( route_prefix, rsrcobj, options )
263
+ pkey = rsrcobj.primary_key
264
+ route = "#{route_prefix}/:#{pkey}"
265
+
266
+ Strelka.log.debug "Creating handler for creating %p resources: POST %s" % [ rsrcobj, route ]
267
+ self.add_route( :PUT, route, options ) do |req|
268
+ finish_with( HTTP::BAD_REQUEST, req.params.error_messages.join(", ") ) unless
269
+ req.params.okay?
270
+
271
+ id = req.params[ pkey ]
272
+ resource = rsrcobj[ id ] or
273
+ finish_with( HTTP::NOT_FOUND, "no such %s [%p]" % [ rsrcobj.name, id ] )
274
+
275
+ newvals = req.params.valid
276
+ newvals.delete( pkey.to_s )
277
+ self.log.debug "Updating %p with new values: %p" % [ resource, newvals ]
278
+
279
+ begin
280
+ resource.update( newvals )
281
+ rescue Sequel::Error => err
282
+ finish_with( HTTP::BAD_REQUEST, err.message )
283
+ end
284
+
285
+ res = req.response
286
+ res.status = HTTP::NO_CONTENT
287
+
288
+ return res
289
+ end
290
+
291
+ self.resource_verbs[ route_prefix ] << :PUT
292
+ end
293
+
294
+
295
+ ### Add a handler method for updating all instances of +rsrcobj+ collection.
296
+ ### PUT /resources
297
+ def add_collection_update_handler( route, rsrcobj, options )
298
+ pkey = rsrcobj.primary_key
299
+ Strelka.log.debug "Creating handler for updating every %p resources: PUT %s" % [ rsrcobj, route ]
300
+
301
+ self.add_route( :PUT, route, options ) do |req|
302
+ finish_with( HTTP::BAD_REQUEST, req.params.error_messages.join(", ") ) unless
303
+ req.params.okay?
304
+
305
+ newvals = req.params.valid
306
+ newvals.delete( pkey.to_s )
307
+ self.log.debug "Updating %p with new values: %p" % [ rsrcobj, newvals ]
308
+
309
+ # Save it in a transaction, erroring if any of 'em fail validations
310
+ begin
311
+ rsrcobj.db.transaction do
312
+ rsrcobj.update( newvals )
313
+ end
314
+ rescue Sequel::ValidationFailed => err
315
+ finish_with( HTTP::BAD_REQUEST, err.message )
316
+ end
317
+
318
+ res = req.response
319
+ res.status = HTTP::NO_CONTENT
320
+
321
+ return res
322
+ end
323
+
324
+ self.resource_verbs[ route ] << :PUT
325
+ end
326
+
327
+
328
+ ### Add a handler method for deleting an instance of +rsrcobj+ with +route_prefix+ as the base
329
+ ### URI path.
330
+ ### DELETE /resources/{id}
331
+ def add_delete_handler( route_prefix, rsrcobj, options )
332
+ pkey = rsrcobj.primary_key
333
+ route = "#{route_prefix}/:#{pkey}"
334
+
335
+ self.add_route( :DELETE, route, options ) do |req|
336
+ finish_with( HTTP::BAD_REQUEST, req.params.error_messages.join(", ") ) unless
337
+ req.params.okay?
338
+
339
+ id = req.params[ pkey ]
340
+
341
+ if resource = rsrcobj[ id ]
342
+ self.log.debug "Deleting %p [%p]" % [ resource.class, id ]
343
+
344
+ begin
345
+ resource.destroy
346
+ rescue Sequel::Error => err
347
+ finish_with( HTTP::BAD_REQUEST, err.message )
348
+ end
349
+ end
350
+
351
+ res = req.response
352
+ res.status = HTTP::NO_CONTENT
353
+
354
+ return res
355
+ end
356
+
357
+ self.resource_verbs[ route_prefix ] << :DELETE
358
+ end
359
+
360
+
361
+ ### Add a handler method for deleting all instances of +rsrcobj+ collection with +route+
362
+ ### as the base URI path.
363
+ ### DELETE /resources
364
+ def add_collection_deletion_handler( route, rsrcobj, options )
365
+ pkey = rsrcobj.primary_key
366
+ Strelka.log.debug "Creating handler for deleting every %p resources: DELETE %s" %
367
+ [ rsrcobj, route ]
368
+
369
+ self.add_route( :DELETE, route, options ) do |req|
370
+ self.log.debug "Deleting all %p objects" % [ rsrcobj ]
371
+
372
+ # Save it in a transaction, erroring if any of 'em fail validations
373
+ begin
374
+ rsrcobj.db.transaction do
375
+ rsrcobj.each {|obj| obj.destroy }
376
+ end
377
+ rescue Sequel::Error => err
378
+ finish_with( HTTP::BAD_REQUEST, err.message )
379
+ end
380
+
381
+ res = req.response
382
+ res.status = HTTP::NO_CONTENT
383
+
384
+ return res
385
+ end
386
+
387
+ self.resource_verbs[ route ] << :DELETE
388
+ end
389
+
390
+
391
+ ### Add routes for any associations +rsrcobj+ has as composite resources.
392
+ def add_composite_resource_handlers( route_prefix, rsrcobj, options )
393
+
394
+ # Add a method for each dataset method that only has a single argument
395
+ # :TODO: Support multiple args? (customers/by_city_state/{city}/{state})
396
+ rsrcobj.dataset_methods.each do |name, proc|
397
+ if proc.parameters.length > 1
398
+ Strelka.log.debug " skipping dataset method %p: more than 1 argument" % [ name ]
399
+ next
400
+ end
401
+
402
+ # Use the name of the dataset block's parameter
403
+ # :TODO: Handle the case where the parameter name doesn't match a column
404
+ # or a parameter-type more gracefully.
405
+ param = proc.parameters.first[1]
406
+ route = "%s/%s/:%s" % [ route_prefix, name, param ]
407
+ Strelka.log.debug " route for dataset method %s: %s" % [ name, route ]
408
+ self.add_dataset_read_handler( route, rsrcobj, name, param, options )
409
+ end
410
+
411
+ # Add composite service routes for each association
412
+ Strelka.log.debug "Adding composite resource routes for %p" % [ rsrcobj ]
413
+ rsrcobj.association_reflections.each do |name, refl|
414
+ pkey = rsrcobj.primary_key
415
+ route = "%s/:%s/%s" % [ route_prefix, pkey, name ]
416
+ Strelka.log.debug " route for associated %p objects via the %s association: %s" %
417
+ [ refl[:class_name], name, route ]
418
+ self.add_composite_read_handler( route, rsrcobj, name, options )
419
+ end
420
+
421
+ end
422
+
423
+
424
+ ### Add a GET route for the dataset method +dsname+ for the given +rsrcobj+ at the
425
+ ### given +path+.
426
+ def add_dataset_read_handler( path, rsrcobj, dsname, param, options )
427
+ Strelka.log.debug "Adding dataset method read handler: %s" % [ path ]
428
+
429
+ self.add_route( :GET, path, options ) do |req|
430
+ self.log.debug "Resource dataset GET request for dataset %s on %p" % [ dsname, rsrcobj ]
431
+ finish_with( HTTP::BAD_REQUEST, req.params.error_messages.join("\n") ) unless
432
+ req.params.okay?
433
+
434
+ # Get the parameter and make the dataset
435
+ res = req.response
436
+ arg = req.params[ param ]
437
+ dataset = rsrcobj.send( dsname, arg )
438
+ self.log.debug " dataset is: %p" % [ dataset ]
439
+
440
+ # Apply offset and limit if they're present
441
+ limit, offset = req.params.values_at( :limit, :offset )
442
+ if limit
443
+ self.log.debug " limiting result set to %p records" % [ limit ]
444
+ dataset = dataset.limit( limit, offset )
445
+ end
446
+
447
+ # Fetch and return the records as JSON or YAML
448
+ # :TODO: Handle other mediatypes
449
+ self.log.debug " returning collection: %s" % [ dataset.sql ]
450
+ res.for( :json, :yaml ) { dataset.all }
451
+
452
+ return res
453
+ end
454
+ end
455
+
456
+
457
+ ### Add a GET route for the specified +association+ of the +rsrcobj+ at the given
458
+ ### +path+.
459
+ def add_composite_read_handler( path, rsrcobj, association, options )
460
+ pkey = rsrcobj.primary_key
461
+ Strelka.log.debug "Adding composite read handler for association: %s" % [ association ]
462
+
463
+ self.add_route( :GET, path, options ) do |req|
464
+ finish_with( HTTP::BAD_REQUEST, req.params.error_messages.join("\n") ) unless
465
+ req.params.okay?
466
+
467
+ # Fetch the primary key from the parameters
468
+ res = req.response
469
+ id = req.params[ pkey ]
470
+
471
+ # Look up the resource, and if it exists, use it to fetch its associated
472
+ # objects
473
+ rsrcobj.db.transaction do
474
+ resource = rsrcobj[ id ] or
475
+ finish_with( HTTP::NOT_FOUND, "No such %s [%p]" % [rsrcobj.table_name, id] )
476
+
477
+ # Apply limit and offset parameters if they exist
478
+ limit, offset = req.params.values_at( :limit, :offset )
479
+ dataset = resource.send( "#{association}_dataset" )
480
+ if limit
481
+ self.log.debug "Limiting result set to %p records" % [ limit ]
482
+ dataset = dataset.limit( limit, offset )
483
+ end
484
+
485
+ # Fetch and return the records as JSON or YAML
486
+ # :TODO: Handle other mediatypes
487
+ self.log.debug "Returning collection: %s" % [ dataset.sql ]
488
+ res.for( :json, :yaml ) { dataset.all }
489
+ end
490
+
491
+ return res
492
+ end
493
+ end
494
+
495
+
496
+ end # module ClassMethods
497
+
498
+
499
+ end # module Strelka::App::RestResources