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