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