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
@@ -2,26 +2,24 @@
|
|
2
2
|
|
3
3
|
require 'strelka' unless defined?( Strelka )
|
4
4
|
require 'strelka/app' unless defined?( Strelka::App )
|
5
|
+
require 'strelka/app/router'
|
5
6
|
|
6
7
|
# Simple (dumb?) request router for Strelka::App-based applications.
|
7
|
-
class Strelka::App::DefaultRouter
|
8
|
+
class Strelka::App::DefaultRouter < Strelka::App::Router
|
8
9
|
include Strelka::Loggable
|
9
10
|
|
10
|
-
### Create a new router that will route requests according to the specified
|
11
|
+
### Create a new router that will route requests according to the specified
|
11
12
|
### +routes+. Each route is a tuple of the form:
|
12
13
|
###
|
13
14
|
### [
|
14
15
|
### <http_verb>, # The HTTP verb as a Symbol (e.g., :GET, :POST, etc.)
|
15
16
|
### <path_array>, # An Array of the parts of the path, as Strings and Regexps.
|
16
|
-
### <
|
17
|
-
### <options_hash>, # The hash of route config options
|
17
|
+
### <route>, # A hash of routing data built by the Routing plugin
|
18
18
|
### ]
|
19
|
-
def initialize( routes=[] )
|
20
|
-
@routes = Hash.new {|
|
21
|
-
|
22
|
-
|
23
|
-
self.add_route( *tuple )
|
24
|
-
end
|
19
|
+
def initialize( routes=[], options={} )
|
20
|
+
@routes = Hash.new {|hash, verb| hash[verb] = {} }
|
21
|
+
|
22
|
+
super
|
25
23
|
end
|
26
24
|
|
27
25
|
|
@@ -35,27 +33,57 @@ class Strelka::App::DefaultRouter
|
|
35
33
|
|
36
34
|
### Add a route for the specified +verb+, +path+, and +options+ that will return
|
37
35
|
### +action+ when a request matches them.
|
38
|
-
def add_route( verb, path,
|
39
|
-
re = Regexp.compile( path.join('/') )
|
36
|
+
def add_route( verb, path, route )
|
37
|
+
re = Regexp.compile( '^' + path.join('/') )
|
40
38
|
|
41
|
-
#
|
42
|
-
self.routes[ verb ][ re ] =
|
39
|
+
# Add the route keyed by HTTP verb and the path regex
|
40
|
+
self.routes[ verb ][ re ] = route
|
43
41
|
end
|
44
42
|
|
45
43
|
|
46
44
|
### Determine the most-specific route for the specified +request+ and return
|
47
|
-
### the
|
45
|
+
### the UnboundMethod object of the App that should handle it.
|
48
46
|
def route_request( request )
|
47
|
+
route = nil
|
49
48
|
verb = request.verb
|
50
49
|
path = request.app_path || ''
|
51
|
-
|
52
|
-
|
53
|
-
# Strip the leading '/'
|
54
|
-
path.slice!( 0, 1 ) if path.start_with?( '/' )
|
50
|
+
path.slice!( 0, 1 ) if path.start_with?( '/' ) # Strip the leading '/'
|
55
51
|
|
52
|
+
self.log.debug "Looking for a route for: %p %p" % [ verb, path ]
|
56
53
|
verbroutes = @routes[ verb ] or return nil
|
57
|
-
|
58
|
-
|
54
|
+
match = self.find_longest_match( verbroutes.keys, path ) or return nil
|
55
|
+
self.log.debug " longest match result: %p" % [ match ]
|
56
|
+
|
57
|
+
# The best route is the one with the key of the regexp of the
|
58
|
+
# longest match
|
59
|
+
route = verbroutes[ match.regexp ].merge( :match => match )
|
60
|
+
|
61
|
+
# Inject the parameters that are part of the route path (/foo/:id) into
|
62
|
+
# the parameters hash. They'll be the named match-groups in the matching
|
63
|
+
# Regex.
|
64
|
+
route_params = match.names.inject({}) do |hash,name|
|
65
|
+
hash[ name ] = match[ name ]
|
66
|
+
hash
|
67
|
+
end
|
68
|
+
|
69
|
+
# Add routing information to the request, and merge parameters if there are any
|
70
|
+
request.params.merge!( route_params ) unless route_params.empty?
|
71
|
+
|
72
|
+
# Return the routing data that should be used
|
73
|
+
return route
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
#########
|
78
|
+
protected
|
79
|
+
#########
|
80
|
+
|
81
|
+
### Find the longest match in +patterns+ for the given +path+ and return the MatchData
|
82
|
+
### object for it. Returns +nil+ if no match was found.
|
83
|
+
def find_longest_match( patterns, path )
|
84
|
+
|
85
|
+
return patterns.inject( nil ) do |longestmatch, pattern|
|
86
|
+
self.log.debug " trying pattern %p; longest match so far: %p" %
|
59
87
|
[ pattern, longestmatch ]
|
60
88
|
|
61
89
|
# If the pattern doesn't match, keep the longest match and move on to the next
|
@@ -71,15 +99,6 @@ class Strelka::App::DefaultRouter
|
|
71
99
|
longestmatch
|
72
100
|
end
|
73
101
|
|
74
|
-
# If there wasn't a match, abort
|
75
|
-
return nil unless longestmatch
|
76
|
-
|
77
|
-
# The best route is the one with the key of the regexp of the
|
78
|
-
# longest match
|
79
|
-
route = verbroutes[ longestmatch.regexp ]
|
80
|
-
|
81
|
-
# Bind the method to the app and
|
82
|
-
return route[:action]
|
83
102
|
end
|
84
103
|
|
85
104
|
end # class Strelka::App::DefaultRouter
|
@@ -0,0 +1,121 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'strelka' unless defined?( Strelka )
|
4
|
+
require 'strelka/app' unless defined?( Strelka::App )
|
5
|
+
|
6
|
+
|
7
|
+
# Custom error-handling plugin for Strelka::App.
|
8
|
+
#
|
9
|
+
# == Examples
|
10
|
+
#
|
11
|
+
# Handle statuses via a callback:
|
12
|
+
#
|
13
|
+
# class MyApp < Strelka::App
|
14
|
+
# plugins :errors
|
15
|
+
#
|
16
|
+
# on_status HTTP::NOT_FOUND do |res|
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# end # class MyApp
|
20
|
+
#
|
21
|
+
# With the templating plugin, you can also handle it via a custom template.
|
22
|
+
#
|
23
|
+
# class MyApp < Strelka::App
|
24
|
+
# plugins :errors, :templating
|
25
|
+
#
|
26
|
+
# layout 'layout.tmpl'
|
27
|
+
# templates :missing => 'errors/missing.tmpl'
|
28
|
+
#
|
29
|
+
# on_status HTTP::NOT_FOUND, :missing
|
30
|
+
#
|
31
|
+
# end # class MyApp
|
32
|
+
#
|
33
|
+
module Strelka::App::Errors
|
34
|
+
extend Strelka::App::Plugin
|
35
|
+
|
36
|
+
DEFAULT_HANDLER_STATUS_RANGE = 400..599
|
37
|
+
|
38
|
+
run_before :routing
|
39
|
+
|
40
|
+
|
41
|
+
# Class-level functionality
|
42
|
+
module ClassMethods # :nodoc:
|
43
|
+
|
44
|
+
@status_handlers = {}
|
45
|
+
|
46
|
+
# The registered status handler callbacks, keyed by numeric HTTP status code
|
47
|
+
attr_reader :status_handlers
|
48
|
+
|
49
|
+
|
50
|
+
### Register a callback for responses that have the specified +status_code+.
|
51
|
+
### :TODO: Document all the stuff.
|
52
|
+
def on_status( range=DEFAULT_HANDLER_STATUS_RANGE, template=nil, &block )
|
53
|
+
range = Range.new( range, range ) unless range.is_a?( Range )
|
54
|
+
methodname = "for_status_%s" % [ range.begin, range.end ].uniq.join('_to_')
|
55
|
+
|
56
|
+
if template
|
57
|
+
raise ArgumentError, "template-style callbacks don't take a block" if block
|
58
|
+
raise ScriptError, "template-style callbacks require the :templating plugin" unless
|
59
|
+
self.respond_to?( :templates )
|
60
|
+
|
61
|
+
block = Proc.new {|*| template }
|
62
|
+
end
|
63
|
+
|
64
|
+
define_method( methodname, &block )
|
65
|
+
|
66
|
+
self.status_handlers[ range ] = instance_method( methodname )
|
67
|
+
end
|
68
|
+
|
69
|
+
end # module ClassMethods
|
70
|
+
|
71
|
+
|
72
|
+
### Check for a status response that is hooked, and run the hook if one is found.
|
73
|
+
def handle_request( request )
|
74
|
+
response = nil
|
75
|
+
|
76
|
+
# Catch a finish_with; the status_response will only be non-nil
|
77
|
+
status_response = catch( :finish ) do
|
78
|
+
response = super
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
|
82
|
+
# If the app or any plugins threw a finish, look for a handler for the status code
|
83
|
+
# and call it if one is found.
|
84
|
+
if status_response
|
85
|
+
response = request.response
|
86
|
+
status = status_response[:status]
|
87
|
+
self.log.info "Handling a status response: %d" % [ status ]
|
88
|
+
|
89
|
+
# If we can't find a custom handler for this status, re-throw
|
90
|
+
# to the default handler instead
|
91
|
+
handler = self.status_handler_for( status ) or
|
92
|
+
throw( :finish, status_response )
|
93
|
+
|
94
|
+
# The handler is an UnboundMethod, so bind it to the app instance
|
95
|
+
# and call it
|
96
|
+
response.status = status
|
97
|
+
response = handler.bind( self ).call( response, status_response )
|
98
|
+
end
|
99
|
+
|
100
|
+
return response
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
### Find a status handler for the given +status_code+ and return it as an UnboundMethod.
|
105
|
+
def status_handler_for( status_code )
|
106
|
+
self.log.debug "Looking for a status handler for %d responses" % [ status_code ]
|
107
|
+
handlers = self.class.status_handlers
|
108
|
+
ranges = handlers.keys
|
109
|
+
|
110
|
+
ranges.each do |range|
|
111
|
+
return handlers[ range ] if range.include?( status_code )
|
112
|
+
end
|
113
|
+
|
114
|
+
return nil
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
end # module Strelka::App::Errors
|
119
|
+
|
120
|
+
|
121
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'strelka' unless defined?( Strelka )
|
4
|
+
require 'strela/app' unless defined?( Strelka::App )
|
5
|
+
|
6
|
+
require 'strelka/app/defaultrouter'
|
7
|
+
|
8
|
+
# Alternative (stricter) router strategy for Strelka::App::Routing plugin.
|
9
|
+
#
|
10
|
+
# == Examples
|
11
|
+
#
|
12
|
+
# class MyApp < Strelka::App
|
13
|
+
# plugins :routing
|
14
|
+
# router :exclusive
|
15
|
+
#
|
16
|
+
# # Unlike the default router, this route will *only* respond to
|
17
|
+
# # a request that matches the app's route with no additional path.
|
18
|
+
# get '' do
|
19
|
+
# # ...
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# end # class MyApp
|
23
|
+
#
|
24
|
+
class Strelka::App::ExclusiveRouter < Strelka::App::DefaultRouter
|
25
|
+
include Strelka::Loggable
|
26
|
+
|
27
|
+
######
|
28
|
+
public
|
29
|
+
######
|
30
|
+
|
31
|
+
### Add a route for the specified +verb+, +path+, and +options+ that will return
|
32
|
+
### +action+ when a request matches them.
|
33
|
+
def add_route( verb, path, route )
|
34
|
+
re = Regexp.compile( '^' + path.join('/') + '$' )
|
35
|
+
|
36
|
+
# Make the Hash for the specified HTTP verb if it hasn't been created already
|
37
|
+
self.routes[ verb ][ re ] = route
|
38
|
+
end
|
39
|
+
|
40
|
+
end # class Strelka::App::DefaultRouter
|
data/lib/strelka/app/filters.rb
CHANGED
@@ -8,10 +8,11 @@ require 'strelka/app' unless defined?( Strelka::App )
|
|
8
8
|
module Strelka::App::Filters
|
9
9
|
extend Strelka::App::Plugin
|
10
10
|
|
11
|
-
run_before :routing
|
11
|
+
run_before :routing, :templating
|
12
12
|
|
13
|
-
|
14
|
-
|
13
|
+
|
14
|
+
# Class methods to add to classes with routing.
|
15
|
+
module ClassMethods # :nodoc:
|
15
16
|
|
16
17
|
# Default filters hash
|
17
18
|
@filters = { :request => [], :response => [], :both => [] }
|
@@ -38,7 +39,7 @@ module Strelka::App::Filters
|
|
38
39
|
|
39
40
|
### Return filters which should be applied to responses, i.e., those with a +which+ of
|
40
41
|
### :response or :both.
|
41
|
-
def
|
42
|
+
def response_filters
|
42
43
|
return self.filters[ :both ] + self.filters[ :response ]
|
43
44
|
end
|
44
45
|
|
@@ -47,22 +48,32 @@ module Strelka::App::Filters
|
|
47
48
|
|
48
49
|
### Apply filters to the given +request+ before yielding back to the App, then apply
|
49
50
|
### filters to the response that comes back.
|
50
|
-
def
|
51
|
+
def handle_request( request )
|
51
52
|
self.apply_request_filters( request )
|
52
53
|
response = super
|
53
54
|
self.apply_response_filters( response )
|
55
|
+
|
56
|
+
return response
|
54
57
|
end
|
55
58
|
|
56
59
|
|
57
60
|
### Apply :request and :both filters to +request+.
|
58
61
|
def apply_request_filters( request )
|
59
|
-
self.
|
62
|
+
self.log.debug "Applying request filters:"
|
63
|
+
self.class.request_filters.each do |filter|
|
64
|
+
self.log.debug " filter: %p" % [ filter ]
|
65
|
+
filter.call( request )
|
66
|
+
end
|
60
67
|
end
|
61
68
|
|
62
69
|
|
63
70
|
### Apply :both and :response filters to +response+.
|
64
71
|
def apply_response_filters( response )
|
65
|
-
self.
|
72
|
+
self.log.debug "Applying response filters:"
|
73
|
+
self.class.response_filters.each do |filter|
|
74
|
+
self.log.debug " filter: %p" % [ filter ]
|
75
|
+
filter.call( response )
|
76
|
+
end
|
66
77
|
end
|
67
78
|
|
68
79
|
end # module Strelka::App::Filters
|
@@ -0,0 +1,122 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'strelka' unless defined?( Strelka )
|
4
|
+
require 'strelka/app' unless defined?( Strelka::App )
|
5
|
+
|
6
|
+
require 'strelka/constants'
|
7
|
+
require 'strelka/httprequest/negotiation'
|
8
|
+
require 'strelka/httpresponse/negotiation'
|
9
|
+
|
10
|
+
|
11
|
+
# HTTP Content negotiation for Strelka applications.
|
12
|
+
#
|
13
|
+
# The application can test the request for which types are accepted, set
|
14
|
+
# different response blocks for different acceptable content types, provides
|
15
|
+
# tranformations for entity bodies and set transformations for new content
|
16
|
+
# types.
|
17
|
+
#
|
18
|
+
# class UserService < Strelka::App
|
19
|
+
#
|
20
|
+
# plugins :routing, :negotiation
|
21
|
+
#
|
22
|
+
# add_content_type :tnetstring, 'text/x-tnetstring' do |response|
|
23
|
+
# tnetstr = nil
|
24
|
+
# begin
|
25
|
+
# tnetstr = TNetString.dump( response.body )
|
26
|
+
# rescue => err
|
27
|
+
# self.log.error "%p while transforming entity body to a TNetString: %s" %
|
28
|
+
# [ err.class, err.message ]
|
29
|
+
# return false
|
30
|
+
# else
|
31
|
+
# response.body = tnetstr
|
32
|
+
# response.content_type = 'text/x-tnetstring'
|
33
|
+
# return true
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# end # class UserService
|
38
|
+
#
|
39
|
+
module Strelka::App::Negotiation
|
40
|
+
include Strelka::Constants
|
41
|
+
extend Strelka::App::Plugin
|
42
|
+
|
43
|
+
run_before :routing
|
44
|
+
run_after :filters, :templating, :parameters
|
45
|
+
|
46
|
+
|
47
|
+
# Class methods to add to classes with content-negotiation.
|
48
|
+
module ClassMethods # :nodoc:
|
49
|
+
|
50
|
+
# Content-type tranform registry, keyed by name
|
51
|
+
@content_type_transforms = {}
|
52
|
+
attr_reader :content_type_transforms
|
53
|
+
|
54
|
+
# Content-type transform names, keyed by mimetype
|
55
|
+
@transform_names = {}
|
56
|
+
attr_reader :transform_names
|
57
|
+
|
58
|
+
|
59
|
+
### Define a new media-type associated with the specified +name+ and +mimetype+. Responses
|
60
|
+
### whose requests accept content of the given +mimetype+ will pass their response to the
|
61
|
+
### specified +transform_block+, which should re-write the response's entity body if it can
|
62
|
+
### transform it to its mimetype. If it successfully does so, it should return +true+, else
|
63
|
+
### the next-best mimetype's transform will be called, etc.
|
64
|
+
def add_content_type( name, mimetype, &transform_block )
|
65
|
+
self.transform_names[ mimetype ] = name
|
66
|
+
self.content_type_transforms[ name ] = transform_block
|
67
|
+
end
|
68
|
+
|
69
|
+
end # module ClassMethods
|
70
|
+
|
71
|
+
|
72
|
+
### Extension callback -- extend the HTTPRequest and HTTPResponse classes with Negotiation
|
73
|
+
### support when this plugin is loaded.
|
74
|
+
def self::included( object )
|
75
|
+
Strelka.log.debug "Extending Request and Response with Negotiation mixins"
|
76
|
+
Strelka::HTTPRequest.class_eval { include Strelka::HTTPRequest::Negotiation }
|
77
|
+
Strelka::HTTPResponse.class_eval { include Strelka::HTTPResponse::Negotiation }
|
78
|
+
super
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
### Start content-negotiation when the response has returned.
|
83
|
+
def handle_request( request )
|
84
|
+
response = super
|
85
|
+
response.negotiate
|
86
|
+
|
87
|
+
return response
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
### Check to be sure the response is acceptable after the request is handled.
|
92
|
+
def fixup_response( response )
|
93
|
+
response = super
|
94
|
+
|
95
|
+
# Ensure the response is acceptable; if it isn't respond with the appropriate
|
96
|
+
# status.
|
97
|
+
unless response.acceptable?
|
98
|
+
body = self.make_not_acceptable_body( response )
|
99
|
+
finish_with( HTTP::NOT_ACCEPTABLE, body ) # throw
|
100
|
+
end
|
101
|
+
|
102
|
+
return response
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
### Create an HTTP entity body describing the variants of the given response.
|
107
|
+
def make_not_acceptable_body( response )
|
108
|
+
# :TODO: Unless it was a HEAD request, the response SHOULD include
|
109
|
+
# an entity containing a list of available entity characteristics and
|
110
|
+
# location(s) from which the user or user agent can choose the one
|
111
|
+
# most appropriate. The entity format is specified by the media type
|
112
|
+
# given in the Content-Type header field. Depending upon the format
|
113
|
+
# and the capabilities of the user agent, selection of the most
|
114
|
+
# appropriate choice MAY be performed automatically. However, this
|
115
|
+
# specification does not define any standard for such automatic
|
116
|
+
# selection. [RFC2616]
|
117
|
+
return "No way to respond given the requested acceptance criteria."
|
118
|
+
end
|
119
|
+
|
120
|
+
end # module Strelka::App::Negotiation
|
121
|
+
|
122
|
+
|
@@ -2,13 +2,53 @@
|
|
2
2
|
|
3
3
|
require 'strelka' unless defined?( Strelka )
|
4
4
|
require 'strelka/app' unless defined?( Strelka::App )
|
5
|
-
|
6
|
-
|
5
|
+
require 'strelka/app/plugins'
|
6
|
+
require 'strelka/app/paramvalidator'
|
7
|
+
|
8
|
+
|
9
|
+
# Parameter validation and untainting for Strelka apps.
|
10
|
+
#
|
11
|
+
# The application can declare parameters globally, and then override them on a
|
12
|
+
# per-route basis:
|
13
|
+
#
|
14
|
+
# class UserManager < Strelka::App
|
15
|
+
#
|
16
|
+
# plugins :routing, :parameters
|
17
|
+
#
|
18
|
+
# param :username, /\w+/, "User login", :required
|
19
|
+
# param :email
|
20
|
+
# param :id, /\d+/, "The user's numeric ID"
|
21
|
+
# param :mode, ['add', 'remove']
|
22
|
+
#
|
23
|
+
# # :username gets validated and merged into query args; URI parameters
|
24
|
+
# # clobber query params
|
25
|
+
# get '/info/:username', :params => { :id => /[XRT]\d{4}-\d{8}/ } do |req|
|
26
|
+
# req.params.okay?
|
27
|
+
# req.params[:username]
|
28
|
+
# req.params.values_at( :id, :username )
|
29
|
+
# req.params.username
|
30
|
+
#
|
31
|
+
# req.params.error_messages
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# end # class UserManager
|
35
|
+
#
|
36
|
+
#
|
37
|
+
# == To-Do
|
38
|
+
#
|
39
|
+
# _We may add support for other ways of passing parameters later,
|
40
|
+
# e.g., via structured entity bodies like JSON, XML, YAML, etc_.
|
41
|
+
#
|
42
|
+
#
|
7
43
|
module Strelka::App::Parameters
|
8
44
|
extend Strelka::App::Plugin
|
9
45
|
|
10
|
-
|
11
|
-
|
46
|
+
run_before :routing
|
47
|
+
run_after :filters
|
48
|
+
|
49
|
+
|
50
|
+
# Class methods to add to classes with routing.
|
51
|
+
module ClassMethods # :nodoc:
|
12
52
|
|
13
53
|
# Pattern for matching route parameters
|
14
54
|
PARAMETER_PATTERN = %r{/:(?<paramname>[a-z]\w*)}i
|
@@ -17,38 +57,110 @@ module Strelka::App::Parameters
|
|
17
57
|
PARAMETER_DEFAULT_OPTIONS = {
|
18
58
|
:constraint => //,
|
19
59
|
:required => false,
|
20
|
-
:untaint => false,
|
21
60
|
:description => nil,
|
22
61
|
}
|
23
62
|
|
63
|
+
# Pattern to use to strip binding operators from parameter patterns so they
|
64
|
+
# can be used in the middle of routing Regexps.
|
65
|
+
PARAMETER_PATTERN_STRIP_RE = Regexp.union( '^', '$', '\\A', '\\z', '\\Z' )
|
66
|
+
|
67
|
+
# Options that are passed as Symbols to .param
|
68
|
+
FLAGS = [ :required, :untaint ]
|
69
|
+
|
24
70
|
|
25
71
|
# Default parameters hash
|
26
72
|
@parameters = {}
|
73
|
+
@untaint_all_constraints = false
|
27
74
|
|
28
75
|
# The hash of declared parameters
|
29
76
|
attr_reader :parameters
|
30
77
|
|
78
|
+
# The flag for untainting constrained parameters that match their constraints
|
79
|
+
attr_writer :untaint_all_constraints
|
80
|
+
|
31
81
|
|
32
82
|
### Declare a parameter with the specified +name+ that will be validated using the given
|
33
|
-
### +
|
34
|
-
|
83
|
+
### +constraint+. The +constraint+ can be any of the types supported by
|
84
|
+
### Strelka::App::ParamValidator.
|
85
|
+
### :call-seq:
|
86
|
+
# param( name, *flags )
|
87
|
+
# param( name, constraint, *flags )
|
88
|
+
# param( name, description, *flags )
|
89
|
+
# param( name, constraint, description, *flags )
|
90
|
+
def param( name, *args )
|
35
91
|
Strelka.log.debug "New param %p" % [ name ]
|
36
92
|
name = name.to_sym
|
37
93
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
94
|
+
# Consume the arguments
|
95
|
+
constraint = args.shift unless args.first.is_a?( String ) || FLAGS.include?( args.first )
|
96
|
+
constraint ||= name
|
97
|
+
description = args.shift if args.first.is_a?( String )
|
98
|
+
# description ||= name.to_s.capitalize
|
99
|
+
flags = args
|
100
|
+
|
101
|
+
# Give a regexp constraint a named capture group for the constraint name if it
|
102
|
+
# doesn't already have one
|
103
|
+
if constraint.is_a?( Regexp )
|
104
|
+
constraint = Regexp.compile( "(?<#{name}>" + constraint.to_s + ")" ) unless
|
105
|
+
constraint.names.include?( name.to_s )
|
106
|
+
Strelka.log.debug " regex constraint is: %p" % [ constraint ]
|
107
|
+
end
|
108
|
+
|
109
|
+
# Merge the param into the parameters hash
|
42
110
|
options = PARAMETER_DEFAULT_OPTIONS.dup
|
43
|
-
options[ :constraint ]
|
44
|
-
options[ :
|
45
|
-
options[ :
|
111
|
+
options[ :constraint ] = constraint
|
112
|
+
options[ :description ] = description
|
113
|
+
options[ :required ] = true if flags.include?( :required )
|
114
|
+
options[ :untaint ] = true if flags.include?( :untaint )
|
46
115
|
Strelka.log.debug " param options are: %p" % [ options ]
|
47
116
|
|
48
117
|
self.parameters[ name ] = options
|
49
118
|
end
|
50
119
|
|
51
120
|
|
121
|
+
### Get/set the untainting flag. If set, all parameters which match their constraints
|
122
|
+
### will also be untainted.
|
123
|
+
def untaint_all_constraints( newval=nil )
|
124
|
+
Strelka.log.debug "Untaint all constraints: %p:%p" % [ newval, @untaint_all_constraints ]
|
125
|
+
@untaint_all_constraints = newval unless newval.nil?
|
126
|
+
return @untaint_all_constraints
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
### Turn the constraint associated with +name+ into a routing component.
|
131
|
+
def extract_route_from_constraint( name )
|
132
|
+
name.slice!( 0, 1 ) if name.start_with?( ':' )
|
133
|
+
Strelka.log.debug " searching for a param for %p" % [ name ]
|
134
|
+
param = self.parameters[ name.to_sym ] or
|
135
|
+
raise ScriptError, "no parameter %p defined" % [ name ]
|
136
|
+
|
137
|
+
# Munge the constraint into a Regexp
|
138
|
+
constraint = param[ :constraint ]
|
139
|
+
re = case constraint
|
140
|
+
when Regexp
|
141
|
+
constraint
|
142
|
+
when Array
|
143
|
+
sub_res = constraint.map( &self.method(:extract_route_from_constraint) )
|
144
|
+
Regexp.union( sub_res )
|
145
|
+
when Symbol
|
146
|
+
re = Strelka::App::ParamValidator.pattern_for_constraint( constraint ) or
|
147
|
+
raise ScriptError, "no pattern for %p constraint" % [ constraint ]
|
148
|
+
/(?<#{name}>#{re})/
|
149
|
+
else
|
150
|
+
raise ScriptError,
|
151
|
+
"can't route on a parameter with a %p constraint %p" % [ constraint.class ]
|
152
|
+
end
|
153
|
+
|
154
|
+
# Unbind the pattern from beginning or end of line.
|
155
|
+
# :TODO: This is pretty ugly. Find a better way of modifying the regex.
|
156
|
+
re_str = re.to_s.
|
157
|
+
sub( %r{\(\?[\-mix]+:(.*)\)}, '\\1' ).
|
158
|
+
gsub( PARAMETER_PATTERN_STRIP_RE, '' )
|
159
|
+
|
160
|
+
return Regexp.new( re_str, re.options )
|
161
|
+
end
|
162
|
+
|
163
|
+
|
52
164
|
### Inheritance hook -- inheriting classes inherit their parents' parameter
|
53
165
|
### declarations, too.
|
54
166
|
def inherited( subclass )
|
@@ -59,6 +171,51 @@ module Strelka::App::Parameters
|
|
59
171
|
end # module ClassMethods
|
60
172
|
|
61
173
|
|
174
|
+
|
175
|
+
### Add a ParamValidator to the given +request+ before passing it on.
|
176
|
+
def handle_request( request, &block )
|
177
|
+
profile = self.make_validator_profile( request )
|
178
|
+
self.log.debug "Applying validator profile: %p" % [ profile ]
|
179
|
+
validator = Strelka::App::ParamValidator.new( profile, request.params )
|
180
|
+
self.log.debug " validator: %p" % [ validator ]
|
181
|
+
|
182
|
+
request.params = validator
|
183
|
+
super
|
184
|
+
end
|
185
|
+
|
186
|
+
|
187
|
+
|
188
|
+
### Make a validator profile for Strelka::App::ParamValidator for the specified
|
189
|
+
### +request+ using the declared parameters in the App, returning it as a Hash.
|
190
|
+
def make_validator_profile( request )
|
191
|
+
profile = {
|
192
|
+
:required => [],
|
193
|
+
:optional => [],
|
194
|
+
:descriptions => {},
|
195
|
+
:constraints => {},
|
196
|
+
:untaint_constraint_fields => [],
|
197
|
+
:untaint_all_constraints => self.class.untaint_all_constraints,
|
198
|
+
}
|
199
|
+
|
200
|
+
self.log.debug "Validator profile is: %p" % [ profile ]
|
201
|
+
|
202
|
+
return self.class.parameters.inject( profile ) do |accum, (name, opts)|
|
203
|
+
self.log.debug " adding parameter: %p: %p" % [ name, opts ]
|
204
|
+
if opts[:required]
|
205
|
+
accum[:required] << name
|
206
|
+
else
|
207
|
+
accum[:optional] << name
|
208
|
+
end
|
209
|
+
|
210
|
+
accum[:untaint_constraint_fields] << name if opts[:untaint]
|
211
|
+
accum[:descriptions][ name ] = opts[:description] if opts[:description]
|
212
|
+
accum[:constraints][ name ] = opts[:constraint]
|
213
|
+
|
214
|
+
accum
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
|
62
219
|
end # module Strelka::App::Parameters
|
63
220
|
|
64
221
|
|