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