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.
- data/History.rdoc +1 -1
- data/IDEAS.rdoc +62 -0
- data/Manifest.txt +38 -7
- data/README.rdoc +124 -5
- data/Rakefile +22 -6
- data/bin/leash +102 -157
- data/contrib/hoetemplate/.autotest.erb +23 -0
- data/contrib/hoetemplate/History.rdoc.erb +4 -0
- data/contrib/hoetemplate/Manifest.txt.erb +8 -0
- data/contrib/hoetemplate/README.rdoc.erb +17 -0
- data/contrib/hoetemplate/Rakefile.erb +24 -0
- data/contrib/hoetemplate/data/file_name/apps/file_name_app +36 -0
- data/contrib/hoetemplate/data/file_name/templates/layout.tmpl.erb +13 -0
- data/contrib/hoetemplate/data/file_name/templates/top.tmpl.erb +8 -0
- data/contrib/hoetemplate/lib/file_name.rb.erb +18 -0
- data/contrib/hoetemplate/spec/file_name_spec.rb.erb +21 -0
- data/data/strelka/apps/hello-world +30 -0
- data/lib/strelka/app/defaultrouter.rb +49 -30
- data/lib/strelka/app/errors.rb +121 -0
- data/lib/strelka/app/exclusiverouter.rb +40 -0
- data/lib/strelka/app/filters.rb +18 -7
- data/lib/strelka/app/negotiation.rb +122 -0
- data/lib/strelka/app/parameters.rb +171 -14
- data/lib/strelka/app/paramvalidator.rb +751 -0
- data/lib/strelka/app/plugins.rb +66 -46
- data/lib/strelka/app/restresources.rb +499 -0
- data/lib/strelka/app/router.rb +73 -0
- data/lib/strelka/app/routing.rb +140 -18
- data/lib/strelka/app/templating.rb +12 -3
- data/lib/strelka/app.rb +174 -24
- data/lib/strelka/constants.rb +0 -20
- data/lib/strelka/exceptions.rb +29 -0
- data/lib/strelka/httprequest/acceptparams.rb +377 -0
- data/lib/strelka/httprequest/negotiation.rb +257 -0
- data/lib/strelka/httprequest.rb +155 -7
- data/lib/strelka/httpresponse/negotiation.rb +579 -0
- data/lib/strelka/httpresponse.rb +140 -0
- data/lib/strelka/logging.rb +4 -1
- data/lib/strelka/mixins.rb +53 -0
- data/lib/strelka.rb +22 -1
- data/spec/data/error.tmpl +1 -0
- data/spec/lib/constants.rb +0 -1
- data/spec/lib/helpers.rb +21 -0
- data/spec/strelka/app/defaultrouter_spec.rb +41 -35
- data/spec/strelka/app/errors_spec.rb +212 -0
- data/spec/strelka/app/exclusiverouter_spec.rb +220 -0
- data/spec/strelka/app/filters_spec.rb +196 -0
- data/spec/strelka/app/negotiation_spec.rb +73 -0
- data/spec/strelka/app/parameters_spec.rb +149 -0
- data/spec/strelka/app/paramvalidator_spec.rb +1059 -0
- data/spec/strelka/app/plugins_spec.rb +26 -19
- data/spec/strelka/app/restresources_spec.rb +393 -0
- data/spec/strelka/app/router_spec.rb +63 -0
- data/spec/strelka/app/routing_spec.rb +183 -9
- data/spec/strelka/app/templating_spec.rb +1 -2
- data/spec/strelka/app_spec.rb +265 -32
- data/spec/strelka/exceptions_spec.rb +53 -0
- data/spec/strelka/httprequest/acceptparams_spec.rb +282 -0
- data/spec/strelka/httprequest/negotiation_spec.rb +246 -0
- data/spec/strelka/httprequest_spec.rb +204 -14
- data/spec/strelka/httpresponse/negotiation_spec.rb +464 -0
- data/spec/strelka/httpresponse_spec.rb +114 -0
- data/spec/strelka/mixins_spec.rb +99 -0
- data.tar.gz.sig +1 -0
- metadata +175 -79
- metadata.gz.sig +2 -0
- data/IDEAS.textile +0 -174
- data/data/strelka/apps/strelka-admin +0 -65
- data/data/strelka/apps/strelka-setup +0 -26
- data/data/strelka/bootstrap-config.rb +0 -34
- data/data/strelka/templates/admin/console.tmpl +0 -21
- data/data/strelka/templates/layout.tmpl +0 -30
- data/lib/strelka/process.rb +0 -19
data/lib/strelka/app/plugins.rb
CHANGED
@@ -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,
|
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::
|
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( :@
|
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
|
43
|
-
|
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.
|
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
|
-
|
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
|
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
|
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
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
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+
|
196
|
-
|
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
|