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.
Files changed (73) hide show
  1. data/History.rdoc +1 -1
  2. data/IDEAS.rdoc +62 -0
  3. data/Manifest.txt +38 -7
  4. data/README.rdoc +124 -5
  5. data/Rakefile +22 -6
  6. data/bin/leash +102 -157
  7. data/contrib/hoetemplate/.autotest.erb +23 -0
  8. data/contrib/hoetemplate/History.rdoc.erb +4 -0
  9. data/contrib/hoetemplate/Manifest.txt.erb +8 -0
  10. data/contrib/hoetemplate/README.rdoc.erb +17 -0
  11. data/contrib/hoetemplate/Rakefile.erb +24 -0
  12. data/contrib/hoetemplate/data/file_name/apps/file_name_app +36 -0
  13. data/contrib/hoetemplate/data/file_name/templates/layout.tmpl.erb +13 -0
  14. data/contrib/hoetemplate/data/file_name/templates/top.tmpl.erb +8 -0
  15. data/contrib/hoetemplate/lib/file_name.rb.erb +18 -0
  16. data/contrib/hoetemplate/spec/file_name_spec.rb.erb +21 -0
  17. data/data/strelka/apps/hello-world +30 -0
  18. data/lib/strelka/app/defaultrouter.rb +49 -30
  19. data/lib/strelka/app/errors.rb +121 -0
  20. data/lib/strelka/app/exclusiverouter.rb +40 -0
  21. data/lib/strelka/app/filters.rb +18 -7
  22. data/lib/strelka/app/negotiation.rb +122 -0
  23. data/lib/strelka/app/parameters.rb +171 -14
  24. data/lib/strelka/app/paramvalidator.rb +751 -0
  25. data/lib/strelka/app/plugins.rb +66 -46
  26. data/lib/strelka/app/restresources.rb +499 -0
  27. data/lib/strelka/app/router.rb +73 -0
  28. data/lib/strelka/app/routing.rb +140 -18
  29. data/lib/strelka/app/templating.rb +12 -3
  30. data/lib/strelka/app.rb +174 -24
  31. data/lib/strelka/constants.rb +0 -20
  32. data/lib/strelka/exceptions.rb +29 -0
  33. data/lib/strelka/httprequest/acceptparams.rb +377 -0
  34. data/lib/strelka/httprequest/negotiation.rb +257 -0
  35. data/lib/strelka/httprequest.rb +155 -7
  36. data/lib/strelka/httpresponse/negotiation.rb +579 -0
  37. data/lib/strelka/httpresponse.rb +140 -0
  38. data/lib/strelka/logging.rb +4 -1
  39. data/lib/strelka/mixins.rb +53 -0
  40. data/lib/strelka.rb +22 -1
  41. data/spec/data/error.tmpl +1 -0
  42. data/spec/lib/constants.rb +0 -1
  43. data/spec/lib/helpers.rb +21 -0
  44. data/spec/strelka/app/defaultrouter_spec.rb +41 -35
  45. data/spec/strelka/app/errors_spec.rb +212 -0
  46. data/spec/strelka/app/exclusiverouter_spec.rb +220 -0
  47. data/spec/strelka/app/filters_spec.rb +196 -0
  48. data/spec/strelka/app/negotiation_spec.rb +73 -0
  49. data/spec/strelka/app/parameters_spec.rb +149 -0
  50. data/spec/strelka/app/paramvalidator_spec.rb +1059 -0
  51. data/spec/strelka/app/plugins_spec.rb +26 -19
  52. data/spec/strelka/app/restresources_spec.rb +393 -0
  53. data/spec/strelka/app/router_spec.rb +63 -0
  54. data/spec/strelka/app/routing_spec.rb +183 -9
  55. data/spec/strelka/app/templating_spec.rb +1 -2
  56. data/spec/strelka/app_spec.rb +265 -32
  57. data/spec/strelka/exceptions_spec.rb +53 -0
  58. data/spec/strelka/httprequest/acceptparams_spec.rb +282 -0
  59. data/spec/strelka/httprequest/negotiation_spec.rb +246 -0
  60. data/spec/strelka/httprequest_spec.rb +204 -14
  61. data/spec/strelka/httpresponse/negotiation_spec.rb +464 -0
  62. data/spec/strelka/httpresponse_spec.rb +114 -0
  63. data/spec/strelka/mixins_spec.rb +99 -0
  64. data.tar.gz.sig +1 -0
  65. metadata +175 -79
  66. metadata.gz.sig +2 -0
  67. data/IDEAS.textile +0 -174
  68. data/data/strelka/apps/strelka-admin +0 -65
  69. data/data/strelka/apps/strelka-setup +0 -26
  70. data/data/strelka/bootstrap-config.rb +0 -34
  71. data/data/strelka/templates/admin/console.tmpl +0 -21
  72. data/data/strelka/templates/layout.tmpl +0 -30
  73. data/lib/strelka/process.rb +0 -19
@@ -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
- ### <action>, # A #to_proc-able object to invoke when the route is matched
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 {|routes, verb| routes[verb] = {} }
21
- routes.each do |tuple|
22
- self.log.debug " adding route: %p" % [ tuple ]
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, action, options={} )
39
- re = Regexp.compile( path.join('/') )
36
+ def add_route( verb, path, route )
37
+ re = Regexp.compile( '^' + path.join('/') )
40
38
 
41
- # Make the Hash for the specified HTTP verb if it hasn't been
42
- self.routes[ verb ][ re ] = { :options => options, :action => action }
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 #to_proc-able object that handles it.
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
- route = nil
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
- longestmatch = verbroutes.keys.inject( nil ) do |longestmatch, pattern|
58
- self.log.debug "Matching pattern %p; longest match so far: %p" %
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
@@ -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
- ### Class methods to add to classes with routing.
14
- module ClassMethods
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 request_filters
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 handler( request )
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.class.request_filters.each {|filter| filter.call(request) }
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.class.response_filters.each {|filter| filter.call(response) }
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
- # Parameter declaration for Strelka::Apps
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
- ### Class methods to add to classes with routing.
11
- module ClassMethods
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
- ### +regexp+.
34
- def param( name, regexp=nil, *flags )
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
- regexp = Regexp.compile( "(?<#{name}>" + regexp.to_s + ")" ) unless
39
- regexp.names.include?( name.to_s )
40
- Strelka.log.debug " param constraint is: %p" % [ regexp ]
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 ] = regexp
44
- options[ :required ] = true if flags.include?( :required )
45
- options[ :untaint ] = true if flags.include?( :untaint )
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