strelka 0.0.1pre4 → 0.0.1.pre129

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